直接用innerhtml赋值会出问题,因为浏览器默认优先使用剪贴板中的text/html格式,导致带style、font、script等标签的富文本被原样插入;且text/plain可能混入零宽字符导致换行丢失,故须劫持paste事件、preventdefault后解析html提取语义化文本。

粘贴时为什么直接用 innerHTML 赋值会出问题
因为浏览器默认把 Word、微信、网页复制的内容原样塞进 DOM,带 <style></style>、<font></font>、嵌套 <span></span>、甚至残留的 <script></script> 标签。这不是 bug,是规范行为——剪贴板里存的是多格式数据,contenteditable 优先用了 text/html 版本。
常见错误现象:innerHTML = clipboardData.getData('text/plain') 看似安全,但 macOS Notes、企业微信等应用会在 text/plain 里混入零宽字符(\u200b)、软连字符(\u00ad),导致段落塌陷、换行丢失;直接 innerHTML = htmlString 则可能执行脚本或污染样式体系。
- 必须拦截
paste事件并调用event.preventDefault(),否则浏览器仍按默认逻辑插入 - 别依赖后端清洗——前端显示已错乱,用户感知已受损
-
innerText会抹掉换行和缩进,且不反映真实 DOM 结构,不能用于值比对
怎么安全提取纯文本并保留换行
核心思路:不靠 text/plain 直接赋值,而是用 text/html 做中间载体,解析后提取语义化文本。这样能绕过 text/plain 的转义失真问题,同时避免正则删标签漏掉注释或自闭合标签。
- 优先取
event.clipboardData.getData('text/html'),用DOMParser解析成文档片段 - 遍历节点,对每个
<p></p>、<div>(display: block)取 <code>node.textContent,再拼接'\n' - 对
<br>、<div> 后加换行,<code><span></span>内部不加,保持语义结构 - Safari 常返回空字符串,需降级 fallback 到
text/plain,并加.replace(/\u200b/g, '')清零宽字符 - 只允许保留
p、br、strong、em、ul、ol、li标签 - 删除所有
style属性、class属性、data-属性(除非你主动管理) - 对
href属性做基础校验(仅允许http://、https://、/开头) - 用
DOMPurify.sanitize(html, {ALLOWED_TAGS: [...], ALLOWED_ATTR: ['href']})是最稳方案,比手写遍历更可靠 - 在
paste事件里先调用getSelection().getRangeAt(0)获取当前光标位置 - 用
range.deleteContents()清除选区内容(如有) - 创建
DocumentFragment插入清洗后的节点,再用range.insertNode(fragment) - IE11 不支持
insertNode,需 fallback 到range.insertNode(textNode)+range.collapse(false) - 插入后手动恢复焦点:
editor.focus(),否则光标可能停留在奇怪位置
如何过滤富文本只留基础结构(如 <p></p>、<strong></strong>)
白名单不是靠正则匹配标签名,而是用 DOMParser 构建干净 DOM 树后再递归清理。正则处理 HTML 字符串极易出错,比如漏掉 <!-- comment --> 或 <img> 自闭合写法。
execCommand 已废,但光标位置怎么保
老代码用 document.execCommand('insertText', false, text) 插入纯文本,问题在于它无视光标位置,总插到末尾。现代替代必须用 Selection + Range API 手动定位。
真正麻烦的不是清洗逻辑本身,而是不同浏览器对 clipboardData.getData('text/html') 的返回差异——Chrome 返回完整 HTML,Firefox 可能简化为 <p></p> 堆叠,Safari 经常为空。所以 fallback 链必须完整,且每个环节都要做空值判断和字符清洗。别省略 \u200b 和 \u00ad 这类不可见字符的清理,它们在真实业务中高频出现,且极难 debug。











