Frontend Engineer · Phone Frontend + Follow-ups

Amazon Frontend:10 个 100px 方块,扩到 100 个并做可访问分页

固定盒尺寸、flex换行、分页状态与原生按钮语义,用React实际DOM交互对照验证。

题目重点:固定盒尺寸、flex换行、分页状态与原生按钮语义,用React实际DOM交互对照验证。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 每个方块固定100×100px,边框/padding含在border-box;gap12px和pageSize10是训练选择。
  • 组件用React.createElement避免需要JSX转译,只作独立教学示例,不修改网站组件。编号当稳定项目ID,真实数据应以业务ID为key。
  • jsdom测试状态/DOM/声明样式,没有浏览器像素布局或screen reader完整验收。

解题路线

  1. 容器flex并允许wrap,item flex不收缩、width/height100px与border-box。不能只写width100却让flex默认shrink后变小;viewport窄于100时须定义overflow/尺寸合同,而非无声破坏固定尺寸。
  2. 100个先测render/布局/交互再选择分页、无限滚动或窗口化;分页有可定位状态与可导航页,无限滚动要考虑返回位置/键盘访问/末尾可达,不只谈DOM数量。
  3. page为零基、pages=ceil(total/pageSize),合法范围约束;每页输出[first,first+count)并保稳定编号。total=0独立显示empty状态,最后不足一页处理,不生成undefined项。
  4. nav有可识别名称,Prev/Next为原生button且边界disabled,数字按钮有Page N名称、当前页aria-current=page,aria-controls关联列表;不要用div click模拟而漏键盘。
  5. 状态更新后用polite live文本报告范围,focus不在每次render强行跳到顶;真实交互根据用户动作决定焦点策略。URL页码/历史、异步load取消/失败是产品扩展,不是样例隐式支持。
  6. React/DOM检查1,204状态:随机prev/next/数字导航,10/100/末页/空集,核列表编号、当前页、控件边界/关联和100px样式声明。JSdom无法证明真实flex行数,布局公式与浏览器检查分开。

手动走一遍

宽375px、gap12时可容纳floor((375+12)/(100+12))=3个方块/行,10个需4行;未扣容器padding时这是自拟content-width估算。100个按10一页共10页,第10页编号91–100,Next disabled,上一页仍可用。

JavaScript 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

import React, { useId, useState } from "react";


export function Boxes({ total = 10, pageSize = 10 }) {
  if (!Number.isInteger(total) || total < 0 || !Number.isInteger(pageSize) || pageSize <= 0) {
    throw new Error("nonnegative total and positive page size required");
  }
  const [storedPage, setPage] = useState(0);
  const listId = useId();
  const pages = Math.max(1, Math.ceil(total / pageSize));
  const page = Math.min(storedPage, pages - 1);
  const first = page * pageSize;
  const count = Math.min(pageSize, total - first);
  const h = React.createElement;
  const controls = Array.from({ length: pages }, (_, i) => h("button", {
    key: i, type: "button", "aria-controls": listId,
    "aria-current": i === page ? "page" : undefined,
    "aria-label": `Page ${i + 1}`, onClick: () => setPage(i),
  }, i + 1));
  return h("section", { "aria-label": "Numbered boxes" },
    h("p", { "aria-live": "polite" }, total === 0 ? "No boxes" : `Showing ${first + 1}–${first + count} of ${total}`),
    h("ul", { id: listId, style: { display: "flex", flexWrap: "wrap", gap: "12px", padding: 0, listStyle: "none" } },
      ...Array.from({ length: count }, (_, i) => h("li", {
        key: first + i, style: { width: "100px", height: "100px", flex: "0 0 100px", boxSizing: "border-box" },
      }, `Box ${first + i + 1}`))),
    h("nav", { "aria-label": "Box pagination" },
      h("button", { type: "button", "aria-controls": listId, disabled: page === 0,
        onClick: () => setPage(p => Math.max(0, Math.min(p, pages - 1) - 1)),
      }, "Previous"),
      ...controls,
      h("button", { type: "button", "aria-controls": listId, disabled: page === pages - 1,
        onClick: () => setPage(p => Math.min(Math.min(p, pages - 1) + 1, pages - 1)),
      }, "Next")));
}

复杂度

当前样例每页P个方块、C=ceil(N/P)数字页按钮,DOM/一次render O(P+C),状态O(1)。N特别大时数字页控件须窗口化或只Prev/Next,不能声称当前全页按钮方案恒定DOM。

易错点与追问

容易失分的边界

  • flex shrink让100px缩水。
  • index key在重排数据中复用错误。
  • page越界/末页空洞。
  • 只有视觉当前页没有语义。
  • div click没有键盘。
  • jsdom样式声明当像素布局证明。

继续深挖

  • 异步请求连续换页如何取消旧响应?
  • 键盘换页后焦点怎样安排?
  • URL/history如何反映页码?
  • 一百万项目页码按钮怎样窗口化?

技术参考

✓已复制微信号!