关键是在文本节点内字符级操作range实现富文本选中修饰:准确定位文本节点并设字符边界,用extractcontents+insertnode替代surroundcontents避免dom破坏,修饰后重置选区并处理unicode等边界细节。

要在原始文本节点中实现高精度的富文本选中与修饰,关键不是“包裹整个元素”,而是直接操作文本节点内部的字符级位置,并确保修饰不破坏原有 DOM 结构。Range 正是为此设计的底层工具——它能精确定位到某个 #text 节点的第 3 个字符起、第 12 个字符止,哪怕这段文字横跨两个 标签。
定位到目标文本节点并设定字符级边界
不能依赖 innerHTML 字符索引,必须遍历 DOM 找到真实的文本节点。常见做法是:
- 用
node.textContent累计偏移量,逐个比对目标关键词在全文本中的起始/结束位置 - 一旦定位到承载该段文字的
#text节点,调用range.setStart(textNode, startChar)和range.setEnd(textNode, endChar) - 若关键词跨节点(如“前端开发”中“开”在 em 内),需分别计算各段偏移,用
setStartBefore()/setEndAfter()或手动拆分 Range
修饰时不污染结构:避免 surroundContents 的陷阱
range.surroundContents(wrapper) 看似方便,但要求 Range 完全落在单个元素内,否则报错。真实场景中,用户选区常跨标签、含空格或嵌套格式。稳妥做法是:
- 调用
range.extractContents()获取 DocumentFragment(保留原有样式和子节点) - 新建一个语义化容器(如
<mark class="highlight"></mark>或<span data-format="bold"></span>) - 将 fragment 追加进该容器,再用
range.insertNode(container)插入原位 - 这样既保留了内联样式、事件绑定,又不会意外打乱父级布局
保持光标/选区可恢复,支持连续编辑
修饰后若直接丢弃 Range,用户下次操作可能丢失上下文。应主动维护可还原的锚点:
- 修饰完成立即调用
selection.removeAllRanges(),再用selection.addRange(range)把光标定在新插入节点末尾 - 若需后续撤销,可记录原始
startContainer、startOffset及修饰后生成的 wrapper 元素引用 - 注意:DOM 修改后旧 Range 的 offset 可能漂移,关键操作前建议用
range.cloneRange()备份或重新计算
处理混合内容时的边界细节
原始文本节点常夹杂换行、零宽空格、Unicode 组合字符等,导致 offset 计算偏差:
- 用
textNode.wholeText替代textContent获取原始字符串,避免 normalize 带来的偏移失真 - 对中文、emoji 或带变音符号的字母,使用
Array.from(textNode.data).length获取真实字符数,而非.length - 遇到
white-space: pre-wrap元素时,需把\n视为实际可见字符参与 offset 计算
不复杂但容易忽略。核心就三点:准确定位到文本节点内部、用 extract+insert 替代 surround、每次修改后重置选区位置。真正难的是把这三步串成稳定链路,而不是某一行代码写对了就行。










