必须只操作文本节点进行高亮,因innerhtml正则替换会导致标签错包、脚本失效、xss漏洞及中文边界错误;应使用treewalker遍历text_node或配置严格的mark.js。

直接用 innerHTML.replace() 做高亮,页面大概率会崩——标签被错包、脚本失效、XSS 漏洞、中文词边界全乱。必须只操作文本节点,用 TreeWalker 或 mark.js,否则就是给自己埋雷。
为什么不能对 innerHTML 做正则替换
浏览器把 innerHTML 当字符串处理,但里面混着标签和文本。搜 a,可能把 <a href="..."></a> 里的 a 也包进去;搜 或 <code>.,正则直接报错;更糟的是,<p>abc</p> 变成 <p><mark>abc</mark></p> 看似正常,但一旦搜 p,就变成 p>abc<mark>p</mark>>,DOM 结构非法,页面白屏。
这些都不是边缘情况:用户输入带符号、中文无空格分隔、动态内容含事件监听器——全都会出事。
- 所有匹配必须限定在
Node.TEXT_NODE节点内,跳过元素、注释、脚本节点 - 关键词含
.?[等正则特殊字符时,必须先用keyword.replace(/[.*+?^${}()|[]\]/g, '\$&')转义 -
textContent是安全提取纯文本的唯一可靠方式;innerText会吞掉display: none内容,但保留换行缩进,导致位置计算偏移
用 TreeWalker 安全遍历文本节点
这是原生、零依赖、IE9+ 兼容的方案,核心是只让正则碰真正的人类可读文本。
- 调用
document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false)创建遍历器 - 循环中加过滤:
if (!node.textContent.trim()) continue,跳过空文本节点(比如换行、缩进产生的空白) - 构造正则时务必加
g和i标志:new RegExp(escapedKeyword, 'gi') - 匹配后用
document.createElement('mark')包裹,再用node.parentNode.replaceChild(markEl, node)替换,不拼字符串 - 每次替换后立即调用
node.parentNode.normalize(),合并相邻文本节点,防止下次搜索重复高亮
用 mark.js 快速集成但别跳过关键配置
mark.js 确实省事,但它默认行为在生产环境里常踩坑:跨标签匹配、不转义用户输入、样式被重置、中文误匹配。
- 引入后必须传入具体容器,别直接
new Mark(document.body),应限定如new Mark(document.querySelector('.article-content')) - 调用
instance.mark(keyword, { element: 'mark', className: 'highlight', caseSensitive: false }),显式指定标签和类名 - 关键词必须提前 HTML 转义:
keyword.replace(/[&"'`=/]/g, c => `${c.charCodeAt(0)};`),否则用户输<script></script>就执行脚本 - CSS 必须写死:
mark.highlight { background-color: #ffeb3b; padding: 0.1em 0.25em; border-radius: 2px; },别依赖浏览器默认或全局 reset - 中文场景下,加
separateWordSearch: false并配合diacritics: false,避免把“服务”错标在“服务器”中间
高亮后怎么精准滚动到第一个匹配项
用户点了搜索,结果藏在页面底部却没反应,体验直接归零。但 window.scrollTo(0, 0) 太粗暴,得定位到首个 .highlight 或 mark 元素。
- 高亮完成后查
const firstMark = document.querySelector('mark, .highlight'),存在才滚动 - 调用
firstMark.scrollIntoView({ behavior: 'smooth', block: 'center' }) - 但注意:如果它在
<details></details>里且未展开,或父级display: none,scrollIntoView会静默失败。需提前检查:if (firstMark.offsetParent === null) { firstMark.closest('details')?.setAttribute('open', '') } - 防抖很关键:监听
input事件时,用setTimeout+clearTimeout控制只在用户停顿 300ms 后执行高亮+滚动,否则连打 “react” 会触发 5 次无意义滚动
最易被忽略的一点:高亮不是一次性的视觉装饰,而是 DOM 修改操作。每次执行都得清旧标、转义输入、限定范围、检查父容器可见性——漏掉任意一环,轻则样式错位,重则整个页面交互失灵。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











