<mark>是纯语义容器,需手动定位关键词并插入,不可依赖自动匹配;必须遍历文本节点、双重转义关键词、用DOM方法安全插入;默认样式不可靠,须显式声明背景色、文字色和内边距,并适配暗色模式。

mark 标签不会自动匹配关键词,必须手动插入
mark 是纯语义容器,不是搜索引擎。写 <mark>React</mark> 能高亮,但前提是“React”这段文本你已经从原文里精准定位出来了。浏览器不会扫描页面去找 “React”,也不会根据 URL 参数 q=React 自动包裹——它只负责渲染你给它的节点。
常见错误是以为加了 mark 就等于完成了高亮逻辑,结果发现页面没变化,或者高亮错位(比如把 <img src="react.png"> 里的 react 也包进去了)。真正要做的,是先提取关键词、再遍历文本节点、最后动态创建并插入 mark 元素。
别用 innerHTML.replace(),它会破坏 DOM 结构
这条路径看似简单:el.innerHTML = el.innerHTML.replace(/keyword/g, '<mark>$</mark>'),但它在真实场景中几乎必然出问题:
- 用户搜
[a-z],正则未转义 →new RegExp('[a-z]')报错,脚本中断 - 用户搜
<script>alert(1)</script>,拼进去后可能触发 XSS - 原文含
&name,未 HTML 转义就替换 → 解析成实体&name,后续标签错位 -
<code>console.log()</code>里搜log,结果变成<code>console.<mark>log</mark>()</code>,mark被撕开,DOM 树损坏
根本问题在于:你在拿整个 HTML 字符串当纯文本处理,而它本质是结构化树。安全做法只操作 NodeFilter.SHOW_TEXT 类型的节点。
立即学习“前端免费学习笔记(深入)”;
安全插入 mark 的最小可行路径
核心是绕过字符串拼接,用原生 DOM 方法组装节点:
- 用
document.createTreeWalker(element, NodeFilter.SHOW_TEXT)获取所有文本节点 - 跳过空节点:
node.textContent.trim() === '' - 对关键词做双重转义:
keyword.replace(/[.*+?^${}()|[]]/g, '\$&')(正则元字符),再按需 HTML 转义(如&→&) - 构造正则:
new RegExp(`(${escaped})`, 'gi'),确保全局 + 不区分大小写 - 匹配后不返回字符串,而是:
const mark = document.createElement('mark'); mark.textContent = matchedText;,再用node.replaceWith(mark)
注意:如果原文是 JavaScript,用户搜 javascript,要用捕获组保留原始大小写,否则会丢失语义。
mark 的样式必须手写,浏览器默认值不可靠
默认黄色背景(#ff0)在以下场景基本失效:
- Safari 阅读模式下直接剥离 UA 样式
- Windows 高对比度模式强制重置未声明的
background-color - 深色主题中,
#ff0在#121212上对比度仅 ~1.4:1,远低于 WCAG AA 要求(≥4.5:1) - Outlook 等邮件客户端完全忽略 UA 默认值
至少声明这三项:background-color、color、padding。推荐组合:mark { background-color: #ffeb3b; color: #212121; padding: 0.1em 0.2em; },并加 @media (prefers-color-scheme: dark) 适配暗色模式。
真正难的不是让文字变黄,而是确保 mark 只出现在文本内容里、不污染属性值、不干扰 screen reader 的朗读顺序、也不让正则跨词匹配(比如搜 cat 匹配到 education)。这些都藏在节点遍历的判断条件和正则边界控制里,而不是 mark 标签本身。



















