题干与适用场景
在线文档阅读器要在长文本中显示搜索结果、批注和当前命中项。文本来自富文本渲染,不能为每个命中插入 span,否则会破坏选择、编辑和布局。请设计 CSS Custom Highlight API 方案,说明 Range 管理、注册表更新、层叠、兼容回退和性能验收。核心考察浏览器文本渲染与渐进增强,归为 frontend。
面试官考察点
- 能否区分 JavaScript Range、Highlight 对象和 CSS
::highlight()伪元素。 - 能否处理文本重渲染后 Range 失效与增量更新。
- 能否理解高亮不改变 DOM,且与
::selection等伪元素存在层叠关系。 - 能否设计可访问的颜色、当前命中和键盘导航。
- 能否提供不支持浏览器的 DOM 标记或 Canvas 回退,并量化性能。
回答前需要澄清的问题
- 目标浏览器和 WebView 是否支持 CSS Custom Highlight API?
- 文本是否会编辑、虚拟化或频繁替换节点?
- 同时最多多少命中,是否需要跨文本节点匹配?
- 高亮是否必须支持暗色主题、强对比度和屏幕阅读器?
- 不支持 API 时能否接受服务端标记或降低视觉效果?
30 秒回答框架
“我把每个命中建成 Range,放入 Highlight,再通过 CSS.highlights 注册名称,由 ::highlight(name) 统一绘制。文本更新后销毁旧 Range 并按稳定文本 ID 重建,避免保留脱离 DOM 的节点。颜色遵循对比度和主题变量,当前命中另设样式与键盘导航。不支持浏览器时回退到安全的标记节点,并用命中数、更新耗时、布局抖动和内存做基准。”
分步骤深入解答
第一步:建立 Range 与 Highlight
API 允许 JavaScript 创建任意文本 Range,再把一个或多个 Range 组成 Highlight 并注册到 HighlightRegistry。CSS 通过 ::highlight(name) 设置颜色,不需要改变原文 DOM。
const range = new Range();
range.setStart(textNode, start);
range.setEnd(textNode, end);
const hit = new Highlight(range);
CSS.highlights.set("search", hit);示例展示基本关系,真实实现要处理跨节点 Range 和节点生命周期。
第二步:处理跨节点和更新
搜索结果应保存文档版本、文本节点路径和字符偏移。富文本重渲染或编辑后,旧 Range 可能指向脱离树的节点;更新时先删除注册表中的旧 Highlight,再基于新文本索引重建。不要只保存 DOM 节点引用。
第三步:管理多个层级
为搜索、批注、当前命中和拼写提示使用不同 Highlight 名称。将当前命中放在明确的层叠顺序,并验证与 ::selection、::spelling-error 等 UA 高亮的叠加。不要用一个对象覆盖所有颜色,否则键盘移动时会产生全量重绘。
第四步:设计无障碍反馈
颜色不能是唯一信息。当前命中要有文本状态、计数、可聚焦导航和清晰的焦点环;高对比度模式下提供边框或背景替代。高亮本身不一定被屏幕阅读器朗读,搜索结果列表需要同步暴露语义。
第五步:控制性能
数百 Range 可能增加匹配、样式计算和绘制成本。对输入做 debounce,批量更新 HighlightRegistry,限制可见窗口,并记录长文本扫描时间、更新 p95、样式计算、滚动帧率和内存。不要在每个按键事件中逐个 set。
第六步:渐进增强回退
能力检测后选择 Custom Highlight、DOM 包裹或纯文本结果列表。DOM 回退必须避免注入未转义文本、破坏链接和重复嵌套;它可以只覆盖可见片段。服务端渲染应先输出可读原文,客户端再增强高亮。
第七步:测试与验收
覆盖跨节点命中、重渲染、编辑、暗色、强对比度、选择复制、虚拟列表、RTL 和不支持浏览器。比较结果文本、键盘顺序、首屏时间、滚动 FPS、更新 p95 和内存;任一回退都不能丢失搜索结果。
高质量示范回答
“我会用 Range 表示命中,用 Highlight 聚合,再通过 CSS.highlights 注册名称和 ::highlight() 绘制,原文 DOM 保持不变。结果保存文本版本与偏移,重渲染后删除旧注册并重建,避免使用失效节点。搜索、批注和当前命中分层,当前命中配合计数、焦点和可访问文本状态。
先做能力检测,不支持时回退到安全 DOM 标记或只显示结果列表。输入 debounce、批量更新和可见窗口控制成本;验收覆盖跨节点、复制、编辑、暗色、强对比度和虚拟列表,比较更新 p95、FPS、内存和结果一致性。”
常见错误
- 为每个命中插入 span → 破坏文本结构和选择 → 优先 Range/Highlight。
- 长期保存旧 Range → 节点重渲染后失效 → 按文本版本重建。
- 只用颜色表达当前命中 → 强对比度和色盲用户难以辨识 → 提供状态与焦点。
- 每个按键逐个更新注册表 → 样式计算抖动 → debounce 和批量更新。
- 回退直接拼接 HTML → 引入注入和嵌套风险 → 转义并限制标记范围。
- 只测 Chrome 直连 → WebView 和旧浏览器行为不同 → 建立能力矩阵。
追问及应对
追问一:为什么不直接包裹 span?
Custom Highlight 不改变 DOM,减少对选择、编辑和布局的干扰;span 可作为兼容回退,但要处理转义和嵌套。
追问二:文本编辑后 Range 会怎样?
节点可能被替换或脱离文档。应依据新文本版本和稳定偏移重建,而不是继续使用旧引用。
追问三:高亮会被屏幕阅读器读出来吗?
不能假设。搜索结果和当前命中状态要用语义文本、计数和键盘导航表达。
追问四:如何证明没有性能回退?
用固定文档和命中数比较更新 p95、样式计算、滚动 FPS、内存和复制行为,并测试编辑与虚拟化场景。