浏览器原生selection不支持多重选区,getrangeat(i)最多返回1个range;所谓多选需手动维护range数组并克隆保存,但dom变更易致range失效,移动端兼容性更差。

Window.getSelection().getRangeAt(0) 只能取第一个 Range,根本不存在“多重选区”
浏览器原生 Selection 对象不支持真正的多重选区(比如 Ctrl+Click 选中多个不连续区域)。getRangeAt(i) 看似能取多个,但实际最多只返回 1 个 Range —— 即使用户按住 Ctrl 点击,selection.rangeCount 永远是 1,后续调用 getRangeAt(1) 会直接抛出 IndexSizeError: Failed to execute 'getRangeAt' on 'Selection': The index provided (1) is larger than the maximum bound (0)。
所谓“多重选区”在 HTML 编辑器里全是模拟的:靠记录鼠标事件 + 手动维护一个 ranges 数组,再用 document.createRange() 和 Selection.addRange()(注意不是覆盖)来视觉叠加。但必须清楚:addRange() 在现代 Chrome/Firefox 中已被弃用且无效果,仅 Safari 旧版部分支持;真实可用的只有单 Range 渲染。
想实现类多重选区效果,必须自己管理 Range 列表
编辑器需拦截 mousedown、ctrlKey 状态、mouseup,并在每次点击后手动构造 Range 并存入数组,例如:
const storedRanges = [];
document.addEventListener('mouseup', (e) => {
if (e.ctrlKey && window.getSelection().rangeCount > 0) {
const sel = window.getSelection();
const range = sel.getRangeAt(0).cloneRange(); // 必须 clone,否则后续 selection 改变会失效
storedRanges.push(range);
}
});
关键点:
-
cloneRange()不可省略 —— 原 Range 是活引用,selection 变化后它自动失效 - 不能依赖
sel.removeAllRanges()后再addRange()模拟多选 —— 多数浏览器忽略第二次及以后的addRange() - 高亮显示需手动遍历
storedRanges,用range.cloneContents()或插入临时<mark></mark>元素实现,但要注意 DOM 变更后所有 stored Range 都可能失效
处理 Range 重叠或嵌套时,DOM 结构变更极易导致 Range 折叠或异常
当对某个 stored Range 执行 range.deleteContents() 或插入节点后,其他未更新的 stored Range 的 startContainer / endContainer 可能已不存在,调用 range.toString() 会返回空,range.getBoundingClientRect() 报错 TypeError: Illegal invocation。
稳妥做法:
- 每次操作前用
range.intersectsNode(node)或range.compareBoundaryPoints(Range.START_TO_START, otherRange) !== 0做粗略校验 - 真正安全的方式是操作前把所有 stored Range 序列化为路径+偏移(如
{path: [0,1,2], startOffset: 5, endOffset: 12}),操作后再用getNodeFromPath()重建 —— 但成本高,仅适合轻量编辑 - 更现实的选择是:放弃“持久多选”,改为每次操作只作用于当前 selection,把“多选”降级为 UI 上的视觉标记(如加背景色 class),底层仍走单 Range 流程
移动端和 Safari 下 getSelection() 行为差异极大
iOS Safari 中,长按选择后 selection.rangeCount 可能为 0(尤其在 contenteditable 内部有 iframe 或 shadow DOM 时);Android Chrome 则可能触发两次 selectionchange,中间夹着一个空 selection。这些都会让基于 getRangeAt(0) 的逻辑直接断裂。
建议底线策略:
- 始终用
if (sel.rangeCount > 0)包裹所有getRangeAt(0)调用 - 避免在
selectionchange里直接操作 DOM —— 改用requestIdleCallback()延迟读取,避开 iOS 的 selection 闪动期 - 对移动端,干脆禁用“多选”UI,只保留单选逻辑,因为真实多 Range 场景在触屏上几乎不可控
Range 的边界不是像素坐标,而是 DOM 树中的位置锚点;一旦编辑器动态渲染、虚拟滚动或局部更新,所有未刷新的 stored Range 就成了悬空指针 —— 这点比想象中更脆弱。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











