不能直接用 innerHTML.replace() 高亮关键词,因其会重建 DOM 导致表单值丢失、事件失效、标签截断和 XSS;必须遍历文本节点,用正则匹配并替换为 mark 元素,同时排除 script/style/textarea、处理中文边界、调用 normalize() 防重复高亮。

直接用 innerHTML.replace() 做关键词高亮,90% 的情况下会破坏 DOM 结构、丢失事件、截断标签,甚至执行 XSS。必须只操作文本节点,配合正则转义和节点级替换。
为什么不能直接改 innerHTML
浏览器解析 innerHTML 时会重新构建子树,导致:
• 表单输入值(<input value="xxx">)清空
• 绑定的事件监听器全部丢失
• <script></script> 或 <textarea></textarea> 内容被误匹配并污染
• 关键词卡在标签中间,比如把 <b>关键词</b> 拆成 <b><mark>关</mark></b> + 键词,DOM 报错或样式失效
怎么安全遍历文本节点并高亮
核心是过滤掉非文本节点和禁用区域:
- 用
document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false)遍历,比递归childNodes更可靠 - 循环中跳过空文本节点:
if (!node.textContent.trim()) continue - 手动排除
<script></script>、<style></style>、<textarea></textarea>父节点:node.parentElement?.tagName === 'SCRIPT' - 对每个有效文本节点,用
node.nodeValue做正则匹配,再用document.createElement('mark')包裹后调用node.parentNode.replaceChild() - 每次替换后立即调用
node.parentNode.normalize(),合并相邻文本节点,防止重复高亮
中文关键词怎么避免“服务”匹配到“服务器”
中文无空格,/服务/gi 会跨词匹配。不能依赖 (对中文无效),得用 Unicode 边界模拟:
立即学习“前端免费学习笔记(深入)”;
- 构造正则:
new RegExp(`(? (? 是负向先行断言,确保前面不是汉字-
([^\u4e00-\u9fa5]|$)确保后面是非汉字或结尾,覆盖空格、标点、换行 - 注意:该方案不解决“微服务”这种复合词,但能拦住绝大多数误匹配
- 如果用户搜的是“web api”,需把空格转为
\s+:keyword.replace(/\s+/g, '\s+')
React/Vue 里高亮后为什么一闪就消失
框架的响应式更新会覆盖你手动插入的 <mark></mark> 节点:
- React 中别碰
ref.current.innerHTML,应在useEffect里用ref.current.childNodes遍历并替换,且每次执行前先清除旧mark:el.querySelectorAll('mark').forEach(m => m.replaceWith(m.textContent)) - Vue 中同理,避免在模板里写
v-html后再 JS 高亮;推荐用自定义指令,在mounted和updated钩子中触发节点级高亮 - 更稳妥的做法:把高亮逻辑下沉到纯函数,返回带
mark的 HTML 字符串,由框架渲染 —— 但要注意 XSS,必须先textContent提取原始文本,再安全拼接
最易被忽略的一点:高亮完不调用 normalize(),会导致后续搜索反复叠加 mark;而忘了排除 textarea,用户在输入框里打字都可能被高亮。这些都不是边缘 case,是上线后必现的问题。



















