题目重点:固定盒尺寸、flex换行、分页状态与原生按钮语义,用React实际DOM交互对照验证。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 每个方块固定100×100px,边框/padding含在border-box;gap12px和pageSize10是训练选择。
- 组件用React.createElement避免需要JSX转译,只作独立教学示例,不修改网站组件。编号当稳定项目ID,真实数据应以业务ID为key。
- jsdom测试状态/DOM/声明样式,没有浏览器像素布局或screen reader完整验收。
解题路线
- 容器flex并允许wrap,item flex不收缩、width/height100px与border-box。不能只写width100却让flex默认shrink后变小;viewport窄于100时须定义overflow/尺寸合同,而非无声破坏固定尺寸。
- 100个先测render/布局/交互再选择分页、无限滚动或窗口化;分页有可定位状态与可导航页,无限滚动要考虑返回位置/键盘访问/末尾可达,不只谈DOM数量。
- page为零基、pages=ceil(total/pageSize),合法范围约束;每页输出[first,first+count)并保稳定编号。total=0独立显示empty状态,最后不足一页处理,不生成undefined项。
- nav有可识别名称,Prev/Next为原生button且边界disabled,数字按钮有Page N名称、当前页aria-current=page,aria-controls关联列表;不要用div click模拟而漏键盘。
- 状态更新后用polite live文本报告范围,focus不在每次render强行跳到顶;真实交互根据用户动作决定焦点策略。URL页码/历史、异步load取消/失败是产品扩展,不是样例隐式支持。
- 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如何反映页码?
- 一百万项目页码按钮怎样窗口化?
技术参考
- React createRoot 官方文档:隔离DOM挂载/卸载API参考。
- React act 官方文档:测试更新flush合同参考,实际React19.3.0/jsdom执行。
- WAI Button Pattern:按钮交互语义参考,示例未宣称完整WCAG/屏幕阅读器验收。
