代表性面试主题

前端面试:如何用 CSS Custom Highlight API 实现搜索高亮?

前端困难
Offer.cc 编辑团队发布 更新

题干

文档阅读器需要同时高亮数百个匹配片段,且不能包裹原文节点。你会如何使用 CSS Custom Highlight API,并处理 Range 失效、选择状态、性能和兼容?

题干与适用场景

在线文档阅读器要在长文本中显示搜索结果、批注和当前命中项。文本来自富文本渲染,不能为每个命中插入 span,否则会破坏选择、编辑和布局。请设计 CSS Custom Highlight API 方案,说明 Range 管理、注册表更新、层叠、兼容回退和性能验收。核心考察浏览器文本渲染与渐进增强,归为 frontend

面试官考察点

  1. 能否区分 JavaScript Range、Highlight 对象和 CSS ::highlight() 伪元素。
  2. 能否处理文本重渲染后 Range 失效与增量更新。
  3. 能否理解高亮不改变 DOM,且与 ::selection 等伪元素存在层叠关系。
  4. 能否设计可访问的颜色、当前命中和键盘导航。
  5. 能否提供不支持浏览器的 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。

javascript
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、内存和复制行为,并测试编辑与虚拟化场景。

公开来源

同类题目