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 / startOffsetRange.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 个 rectgetBoundingClientRect()返回包住这 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:为什么 Selection 和 Range 要分开?
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()。