Vue.js更新v-html时打字输入光标焦点异常问题求助
Vue中contenteditable div实现链接自动高亮并解决光标跳转问题
问题根源
你遇到的光标跳转问题核心原因是:每次更新v-html绑定的html变量时,Vue会重新渲染整个contenteditable节点,原有选区/光标位置会被清空重置,所以出现光标跳转到节点开头/末尾的异常。
可行解决方案
核心思路是:更新HTML前先保存当前光标位置,HTML更新完成后手动还原光标位置,同时补充链接自动高亮的逻辑。
实现步骤
- 用原生
Range、SelectionAPI实现光标位置的读写能力 - 在
updateHtml方法中补充「保存光标→处理内容匹配链接→更新HTML→还原光标」的完整逻辑 - 新增链接匹配规则,将纯文本链接转为带样式的蓝色超链接
完整代码实现
<div id="app"> <div class="flex"> <div class="message" @input="updateHtml" v-html="html" contenteditable="true" style="border:1px solid #ccc; min-height: 40px; padding: 8px;" ></div> <br> <div class="message">{{ html }}</div> </div> </div> <style> /* 高亮链接样式 */ .message a { color: #1677ff; text-decoration: underline; } </style> <script> let app = new Vue({ el: '#app', data: { html: 'some text', }, methods: { // 获取当前光标在contenteditable节点中的偏移量 getCaretPosition(editableDiv) { let caretOffset = 0; const doc = editableDiv.ownerDocument || document; const win = doc.defaultView || window; const sel = win.getSelection(); if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(editableDiv); preCaretRange.setEnd(range.endContainer, range.endOffset); caretOffset = preCaretRange.toString().length; } return caretOffset; }, // 设置光标到指定偏移量位置 setCaretPosition(editableDiv, offset) { const doc = editableDiv.ownerDocument || document; const win = doc.defaultView || window; const sel = win.getSelection(); const range = doc.createRange(); range.selectNodeContents(editableDiv); let currentOffset = 0; let nodeStack = [editableDiv]; let node, foundStart = false, stop = false; while (!stop && (node = nodeStack.pop())) { if (node.nodeType === Node.TEXT_NODE) { const nextOffset = currentOffset + node.length; if (!foundStart && offset >= currentOffset && offset <= nextOffset) { range.setStart(node, offset - currentOffset); foundStart = true; } if (foundStart && offset >= currentOffset && offset <= nextOffset) { range.setEnd(node, offset - currentOffset); stop = true; } currentOffset = nextOffset; } else { let i = node.childNodes.length; while (i--) { nodeStack.push(node.childNodes[i]); } } } range.collapse(true); sel.removeAllRanges(); sel.addRange(range); }, // 处理文本中的链接,转为高亮a标签 parseLinks(text) { // 匹配http/https开头的链接,排除已经包裹在a标签内的内容 const urlRegex = /(?<!<a[^>]*>)(https?:\/\/[^\s<]+)/g; return text.replace(urlRegex, '<a href="$1" target="_blank">$1</a>'); }, updateHtml: function(e) { const editableDiv = e.target; // 1. 保存当前光标位置 const caretPos = this.getCaretPosition(editableDiv); // 2. 获取原始内容并处理链接高亮 const rawHtml = editableDiv.innerHTML; const processedHtml = this.parseLinks(rawHtml); // 3. 只有内容变化时才更新,减少不必要重渲 if (this.html !== processedHtml) { this.html = processedHtml; // 4. 等Vue完成DOM更新后还原光标位置 this.$nextTick(() => { this.setCaretPosition(editableDiv, caretPos); }); } }, renderHtml: function(){ this.html += '<img src="https://cdn-images-1.medium.com/max/853/1*FH12a2fX61aHOn39pff9vA.jpeg" alt="" width=200px>'; } } }); </script>
额外优化建议
- 可以给
updateHtml方法加100-300ms的防抖,减少频繁重渲带来的性能损耗,同时不影响用户输入体验 - 如果需要兼容低版本浏览器(如IE),可以替换正则中的零宽断言逻辑,改用DOM遍历的方式处理链接,避免匹配到已有的a标签
- 如果需要支持更复杂的富文本能力,建议直接引入成熟的富文本编辑器组件,避免重复造轮子
- 链接匹配正则可以根据业务需求调整,比如支持不带http前缀的域名、邮箱等内容的高亮
内容的提问来源于stack exchange,提问作者Soju Varughese
相关产品推荐
相关产品推荐

