mark标签是唯一语义正确、可访问且被搜索引擎和读屏器识别的高亮方式,专用于标识与当前上下文(如搜索、引用、代码修改)强相关的文本,不可用span+class替代;它不是视觉装饰,而是传达“上下文相关高亮”的语义化元素,误用会损害可访问性、引发XSS风险、导致DOM结构破坏或样式失效。

mark 标签是唯一语义正确、可访问、且被搜索引擎和读屏器识别的高亮方式,不能用 span + class 替代——它不是“加个黄底”的视觉技巧,而是告诉浏览器“这段文本与当前上下文(比如搜索)强相关”。
为什么直接 innerHTML.replace() 是最危险的写法
很多开发者一上来就写 element.innerHTML = element.innerHTML.replace(/keyword/g, '<mark>$&</mark>'),这会立刻触发三类问题:
- 用户输入含
<script>或onerror时,拼进字符串后执行 XSS - 关键词含
&、"、等字符,导致 DOM 解析中断(比如 <code>&变成&,后续标签错位) - 正则没转义元字符,用户搜
[a-z]就让new RegExp(keyword)直接报错 - 匹配破坏 HTML 结构:在
<img src="logo.png">里搜logo,结果变成<img src="<mark>logo</mark>.png">,图片挂掉
必须只操作文本节点,而不是整个 innerHTML
安全高亮的本质是绕过字符串拼接,只在纯文本内容上做切分与替换。关键步骤如下:
- 用
document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false)遍历所有文本节点 - 对每个
node.textContent执行匹配,跳过空文本或仅空白符的节点 - 用
keyword.replace(/[.*+?^${}()|[]\]/g, '\$&')转义正则特殊字符 - 匹配后不直接拼 HTML 字符串,而是:
document.createElement('mark')→mark.textContent = matchedText→parentNode.replaceChild(mark, textNode) - 若需保留大小写(如原文是
React,用户搜react),用捕获组还原:text.replace(new RegExp(`(${escaped})`, 'gi'), (_, p1) => { const mark = document.createElement('mark'); mark.textContent = p1; return mark.outerHTML; })——但注意,outerHTML仍含字符串风险,更稳妥的是用insertAdjacentElement插入
样式必须显式声明,别信浏览器默认值
默认黄色背景在以下场景会完全失效:
立即学习“前端免费学习笔记(深入)”;
- Safari 阅读模式下忽略
mark样式 - Windows 高对比度模式强制重置为黑底白字,黄底消失
- 深色主题中默认黄底黑字对比度低于 WCAG AA 标准
- 某些邮件客户端(如 Outlook)彻底禁用 UA 样式
生产环境至少写死这些 CSS:
mark {
background-color: #ffeb3b;
color: #212121;
padding: 0.1em 0.25em;
border-radius: 2px;
}
@media (prefers-color-scheme: dark) {
mark {
background-color: #66bb6a;
color: #fff;
}
}
多关键词、跨标签、中文分词的边界处理
真实搜索场景远比单词替换复杂:
- 用户搜 “前端 工程化”,要分别高亮 “前端” 和 “工程化”,不能拼成一个正则
/前端|工程化/gi—— 否则 “前端工程化” 连续出现时,第二个词可能被第一个mark的闭合标签截断 - 关键词重叠(如 “JavaScript” 和 “Script”)必须先匹配长词再短词,否则 “Script” 会被包进已闭合的
<mark>JavaScript</mark>内部,变成无效嵌套 - 中文无空格分隔,“搜索关键词” 中搜 “搜索” 不能误命中 “搜” 或 “索”,得加单词边界
—— 但对中文无效,实际要用(?<=^|\s)和(?=$|\s)模拟 - 跨标签匹配(如 “前” 在
<strong>前</strong>端中)无法靠正则解决,必须提取整段连续文本(合并相邻文本节点)、记录原始位置、再映射回 DOM 插入mark
真正难的不是让字变黄,而是确保高亮后链接还能点、屏幕阅读器能准确播报、DOM 结构不塌、正则不炸、深色模式不糊——这些全藏在遍历逻辑和边界判断里,不在 mark 标签本身。



















