跳到主要内容

Selection API 与 getClientRects():选中文本后怎么拿范围、方向和屏幕坐标?

面试速答(30 秒版 TL;DR)

  • Selection API 用来描述“当前用户选中的内容”,核心入口是 window.getSelection()
  • 一个 Selection 内部本质上是一个或多个 Range;真正描述 DOM 边界的是 Range,不是 Selection 自己。
  • getClientRects() 会返回 每个可视片段的矩形列表,适合多行高亮、浮层定位、批注系统;getBoundingClientRect() 则是整体包围盒。
  • 做富文本、划词翻译、评论批注时,通常是:Selection -> Range -> rects 这条链路。

心智模型:用户选区分两层

可以这样理解:

  • Selection:当前“选中了什么”,更像全局状态
  • Range:从哪个节点的哪个偏移,到哪个节点的哪个偏移

也就是说:

  • Selection 偏“结果”
  • Range 偏“边界”

一、先拿到当前选区

const selection = window.getSelection();

最常用属性和方法:

成员作用
rangeCount当前有多少个 Range
anchorNode / anchorOffset选区起点
focusNode / focusOffset选区终点
isCollapsed是否折叠成光标
toString()选中的纯文本
getRangeAt(index)取指定 Range
removeAllRanges()清空选区
addRange(range)设置选区

二、anchor / focus 和“开始/结束”不是一回事

这是高频追问。

  • anchor:用户开始拖拽或开始选中的那一端
  • focus:用户结束拖拽时所在的那一端

但文档顺序上的:

  • Range.startContainer / startOffset
  • Range.endContainer / endOffset

是按 DOM 先后顺序排好的。

结论:

  • anchor/focus 表示用户操作方向
  • start/end 表示文档中的逻辑边界

三、从 Selection 拿到 Range

const selection = window.getSelection();

if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
}

Range 常用能力:

方法/属性作用
startContainer / startOffset起点节点与偏移
endContainer / endOffset终点节点与偏移
collapsed是否折叠
cloneRange()拷贝 Range
selectNode(node)选中整个节点
selectNodeContents(node)选中节点内部内容
setStart(...) / setEnd(...)手动设边界
deleteContents()删除范围内内容
extractContents()抽出为 DocumentFragment
cloneContents()拷贝范围内容

四、getClientRects() 到底返回什么

const rects = range.getClientRects();

它返回的是:

  • 一个矩形列表
  • 每个矩形对应选区在页面上的一个可视片段

为什么不是一个矩形?

因为文本可能:

  • 跨多行
  • 跨多个内联元素
  • 被不同盒子打断

所以同一段选区可能对应多个小矩形。


五、getClientRects() vs getBoundingClientRect()

方法返回什么适合场景
getClientRects()每个片段的小矩形集合多行高亮、逐行批注、精细覆盖层
getBoundingClientRect()所有片段的整体包围盒浮层、菜单、工具条定位

例子:

  • 选中了一段跨 3 行的文字
  • getClientRects() 可能返回 3 个 rect
  • getBoundingClientRect() 返回包住这 3 行的大矩形

六、最常见的实战:划词浮层定位

document.addEventListener("mouseup", () => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return;

const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();

toolbar.style.left = `${rect.left + window.scrollX}px`;
toolbar.style.top = `${rect.top + window.scrollY - 40}px`;
});

这里为什么常用 getBoundingClientRect()

  • 工具条通常只需要一个整体位置
  • 不需要逐行精细渲染

七、多行高亮为什么要用 getClientRects()

如果你做的是:

  • 批注高亮
  • 富文本选中覆盖层
  • PDF/文档阅读器中的标记框

那就不能只用整体包围盒,因为它会把空白区域也包进去。

更合理的方式是:

for (const rect of range.getClientRects()) {
// 为每一行/每一段生成一个覆盖层
}

八、和 selectionchange 事件配合

浏览器在选区变化时会触发:

document.addEventListener("selectionchange", () => {
// 响应选区变化
});

适合:

  • 实时显示工具条
  • 同步选中文本
  • 富文本编辑器状态更新

九、常见边界条件

1) 折叠选区

  • 光标插入点也是 Selection
  • isCollapsed === true
  • 此时 getClientRects() 可能为空或接近 0 宽

2) 选区不在可见区域

  • rect 仍然可能存在
  • 但浮层定位要结合滚动偏移、视口边界做修正

3) 跨 iframe / Shadow DOM

  • 选区获取和坐标换算会更复杂
  • 不能简单假设所有坐标都在同一个文档空间

典型题 & 标准答法

Q1:为什么 SelectionRange 要分开?

  • Selection 表示当前选区状态
  • Range 描述精确 DOM 边界
  • 真正进行内容裁剪、坐标计算、节点提取时,核心对象是 Range

Q2:getClientRects()getBoundingClientRect() 有什么区别?

  • 前者返回多个片段矩形,适合精细高亮
  • 后者返回一个整体包围盒,适合浮层定位

Q3:为什么 anchor/focus 不等于 start/end

因为 anchor/focus 反映用户操作方向,而 start/end 是按文档顺序排序后的逻辑边界。


易错点/坑

  • 直接拿 Selection 做几何计算,应该先取 Range
  • 选区跨多行时只用 getBoundingClientRect(),会导致高亮区域过大。
  • 忘记加 window.scrollX / scrollY,定位会在滚动页面后偏移。
  • 光标折叠状态下没判断 isCollapsed,导致工具条乱闪。

速记要点(可背诵)

  • window.getSelection() 拿当前选区。
  • Selection 管状态,Range 管边界。
  • 定位浮层常用 getBoundingClientRect()
  • 多行高亮常用 getClientRects()