1864 字
9 分钟
Range API 完全指南:精确控制文档片段的 10 个实用技巧

前言#

Range API 是浏览器提供的强大接口,用于表示文档中的一个片段(可以包含节点与文本节点的一部分)。它能够精确控制 DOM 中的任意片段,而不需要破坏原有结构,是实现富文本编辑器、代码编辑器、PDF 标注工具等应用的核心技术。

本文将通过 10 个实用示例,带你掌握 Range API 的核心用法。

1. 精确插入内容到光标位置#

在可编辑元素中插入内容时,使用 Range API 可以实现精确的光标位置控制,支持插入 HTML 格式的内容。

// 在光标位置插入内容(支持 HTML)
function insertAtCursor(html) {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents(); // 删除选中内容
const fragment = range.createContextualFragment(html);
range.insertNode(fragment);
// 移动光标到插入内容后面
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
// 使用示例
insertAtCursor('<strong>插入的粗体文本</strong>');

核心方法说明:

  • getRangeAt(0):获取当前选区的第一个 Range 对象
  • deleteContents():删除选中的内容
  • createContextualFragment():创建文档片段,可解析 HTML
  • collapse(false):将光标移动到插入内容之后

2. 获取选中文本的精确位置坐标#

获取选中文本的边界矩形信息,常用于显示悬浮工具栏(如 Medium 编辑器的工具条)。

// 获取选中文本的边界矩形
function getSelectionCoords() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height
};
}
}
// 用途:在选中文本旁边显示悬浮工具栏
function showToolbar() {
const coords = getSelectionCoords();
const toolbar = document.getElementById('toolbar');
toolbar.style.top = (coords.top - 40) + 'px';
toolbar.style.left = coords.left + 'px';
toolbar.style.display = 'block';
}

应用场景:

  • 富文本编辑器的格式化工具栏
  • 划词翻译弹窗定位
  • 批注气泡定位

3. 高亮搜索关键词(不破坏 DOM 结构)#

使用 Range API 可以精确地包裹文本节点,实现搜索高亮功能。

function highlightText(searchText) {
const selection = window.getSelection();
selection.removeAllRanges();
const range = document.createRange();
const textNode = document.querySelector('#content').firstChild;
// 遍历文本节点查找匹配
let startPos = 0;
const text = textNode.textContent;
while ((startPos = text.indexOf(searchText, startPos)) !== -1) {
range.setStart(textNode, startPos);
range.setEnd(textNode, startPos + searchText.length);
const span = document.createElement('span');
span.style.backgroundColor = 'yellow';
range.surroundContents(span);
startPos += searchText.length;
}
}

注意事项:

  • surroundContents() 方法会将选区内容包裹在指定元素中
  • 适用于纯文本节点,复杂 DOM 结构需使用 extractContents()insertNode()

4. 提取选中内容的纯文本或 HTML#

获取用户选中内容的多种格式,用于复制、引用等功能。

function getSelectedContent() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// 获取纯文本
const text = range.toString();
// 获取 HTML(包含标签)
const fragment = range.cloneContents();
const div = document.createElement('div');
div.appendChild(fragment);
const html = div.innerHTML;
return { text, html };
}
}

方法对比:

  • toString():返回纯文本,忽略 HTML 标签
  • cloneContents():返回包含所有节点和样式的文档片段

5. 智能选择 - 扩展选区到单词边界#

实现双击或快捷键选择完整单词的功能。

// 扩展选区到完整单词
function selectWord() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// 向前扩展到单词边界
while (range.startOffset > 0) {
range.setStart(range.startContainer, range.startOffset - 1);
if (/\s/.test(range.toString()[0])) {
range.setStart(range.startContainer, range.startOffset + 1);
break;
}
}
// 向后扩展到单词边界
const container = range.endContainer;
while (range.endOffset < container.textContent.length) {
range.setEnd(container, range.endOffset + 1);
if (/\s/.test(range.toString().slice(-1))) {
range.setEnd(container, range.endOffset - 1);
break;
}
}
selection.removeAllRanges();
selection.addRange(range);
}
}

扩展思路:

  • 可进一步扩展为选择句子:以句号、问号、感叹号为边界
  • 选择段落:以换行符或 <p> 标签为边界

6. 虚拟光标 - 在不可编辑区域显示光标#

在只读元素中创建视觉光标,常用于自定义输入框或代码编辑器。

function createVirtualCursor(element, offset) {
const range = document.createRange();
range.setStart(element.firstChild, offset);
range.collapse(true);
// 创建一个零宽度的 span 作为光标
const cursor = document.createElement('span');
cursor.className = 'virtual-cursor';
cursor.style.cssText = `
display: inline-block;
width: 2px;
height: 1.2em;
background: black;
animation: blink 1s infinite;
`;
range.insertNode(cursor);
}

配套 CSS 动画:

@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}

7. 比较两个选区是否相交#

用于判断多个选区之间的关系,常见于协同编辑场景。

function rangesIntersect(range1, range2) {
// 使用 compareBoundaryPoints 比较边界
return range1.compareBoundaryPoints(Range.START_TO_END, range2) > 0 &&
range1.compareBoundaryPoints(Range.END_TO_START, range2) < 0;
}

compareBoundaryPoints 常量:

  • Range.START_TO_START (0):比较两个 Range 的起点
  • Range.START_TO_END (1):比较第一个 Range 的起点和第二个的终点
  • Range.END_TO_END (2):比较两个 Range 的终点
  • Range.END_TO_START (3):比较第一个 Range 的终点和第二个的起点

8. 文本批注/评论功能#

实现类似 Google Docs 的文本批注功能。

function addComment(commentText) {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const mark = document.createElement('mark');
mark.className = 'comment-highlight';
mark.dataset.comment = commentText;
mark.style.backgroundColor = '#ffeb3b';
mark.style.cursor = 'pointer';
// 保存原始文本位置信息
mark.dataset.startOffset = range.startOffset;
mark.dataset.endOffset = range.endOffset;
range.surroundContents(mark);
// 添加点击事件显示评论
mark.addEventListener('click', function() {
alert('评论: ' + this.dataset.comment);
});
}
}

生产环境优化:

  • 使用 UUID 作为批注 ID,关联到数据库
  • 支持批注的回复和删除
  • 使用 Popover API 或 Tooltip 替代 alert

9. 计算文本行数和换行位置#

获取文本的实际渲染行数,用于实现行号显示或精确定位。

function getLineBreaks(element) {
const range = document.createRange();
const lines = [];
let lastTop = -1;
const text = element.textContent;
for (let i = 0; i < text.length; i++) {
range.setStart(element.firstChild, i);
range.setEnd(element.firstChild, i + 1);
const rect = range.getBoundingClientRect();
if (rect.top !== lastTop) {
lines.push({ index: i, top: rect.top });
lastTop = rect.top;
}
}
return lines;
}

性能优化建议:

  • 对于长文本,可使用二分查找减少计算次数
  • 缓存结果,仅在窗口 resize 时重新计算

10. 实现代码编辑器的括号匹配高亮#

在代码编辑器中高亮显示匹配的括号对。

function highlightMatchingBrackets() {
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const offset = range.startOffset;
const text = range.startContainer.textContent;
const char = text[offset];
const pairs = { '(': ')', '[': ']', '{': '}' };
if (pairs[char]) {
// 查找匹配的右括号
let count = 1;
for (let i = offset + 1; i < text.length; i++) {
if (text[i] === char) count++;
if (text[i] === pairs[char]) count--;
if (count === 0) {
// 高亮两个括号
highlightRange(range.startContainer, offset, offset + 1);
highlightRange(range.startContainer, i, i + 1);
break;
}
}
}
}
function highlightRange(node, start, end) {
const range = document.createRange();
range.setStart(node, start);
range.setEnd(node, end);
const span = document.createElement('span');
span.style.backgroundColor = '#e0e0e0';
range.surroundContents(span);
}

完善建议:

  • 添加反向括号匹配(从右括号找左括号)
  • 支持多行跨节点匹配
  • 添加未匹配括号的错误提示

实际应用场景#

Range API 在现代 Web 应用中有广泛的应用:

  1. 富文本编辑器:Notion、Google Docs、语雀等
  2. 代码编辑器:VS Code Web、CodeMirror、Monaco Editor
  3. PDF 标注工具:文本高亮、批注、标记
  4. 阅读辅助工具:划词翻译、生词本、阅读进度追踪
  5. 协同编辑:多人光标位置显示、实时编辑同步
  6. 文本分析工具:语法检查、关键词提取、敏感词过滤

总结#

Range API 的强大之处在于它能够精确控制 DOM 中的任意片段,而不需要破坏原有结构。通过合理使用 Range API,我们可以实现复杂的文本编辑和交互功能。

核心优势:

  • 精确定位:可以精确到字符级别的位置控制
  • 非破坏性操作:不会破坏原有 DOM 结构
  • 灵活性高:支持复杂的选区操作和内容提取
  • 浏览器原生支持:无需引入第三方库

最佳实践:

  • 始终检查 selection.rangeCount 避免空指针错误
  • 大量 DOM 操作时使用 DocumentFragment 提升性能
  • 注意内存泄漏,及时清除事件监听器
  • 考虑浏览器兼容性,必要时使用 polyfill
Range API 完全指南:精确控制文档片段的 10 个实用技巧
https://fuwari.vercel.app/posts/range/
作者
Lorem Ipsum
发布于
2025-11-10
许可协议
CC BY-NC-SA 4.0