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():创建文档片段,可解析 HTMLcollapse(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 应用中有广泛的应用:
- 富文本编辑器:Notion、Google Docs、语雀等
- 代码编辑器:VS Code Web、CodeMirror、Monaco Editor
- PDF 标注工具:文本高亮、批注、标记
- 阅读辅助工具:划词翻译、生词本、阅读进度追踪
- 协同编辑:多人光标位置显示、实时编辑同步
- 文本分析工具:语法检查、关键词提取、敏感词过滤
总结
Range API 的强大之处在于它能够精确控制 DOM 中的任意片段,而不需要破坏原有结构。通过合理使用 Range API,我们可以实现复杂的文本编辑和交互功能。
核心优势:
- 精确定位:可以精确到字符级别的位置控制
- 非破坏性操作:不会破坏原有 DOM 结构
- 灵活性高:支持复杂的选区操作和内容提取
- 浏览器原生支持:无需引入第三方库
最佳实践:
- 始终检查
selection.rangeCount避免空指针错误 - 大量 DOM 操作时使用
DocumentFragment提升性能 - 注意内存泄漏,及时清除事件监听器
- 考虑浏览器兼容性,必要时使用 polyfill
Range API 完全指南:精确控制文档片段的 10 个实用技巧
https://fuwari.vercel.app/posts/range/