直接用contenteditable配合localStorage保存会丢内容、存错格式甚至XSS,因input事件漏监听右键粘贴、拖拽、撤销及中文输入法上屏等操作,须组合input+compositionend+blur;读取时禁用innerHTML,改用DOMParser清洗+textContent校验,并同步管理光标位置。

直接用 contenteditable 配合 localStorage 保存,会丢内容、存错格式、甚至触发 XSS —— 不是 localStorage 的问题,而是 contenteditable 的 DOM 变更没被正确捕获或清洗。
为什么 input 事件监听不到所有编辑动作
用户右键粘贴、拖拽文本、Ctrl+Z/Y 撤销、中文输入法上屏(compositionend 之后才真正落字),这些都不会触发 input 事件。只监听它,等于默认“信任”浏览器的变更节奏,结果就是:用户以为存了,刷新后发现最后一段文字没了。
- 必须组合监听:
input+compositionend+blur,三者缺一不可 -
compositionend是中文输入法上屏的关键信号,漏掉它,拼音打完回车,内容就卡在未提交状态 -
blur是兜底:用户编辑完直接关页,靠它触发最后一次保存 - 别在回调里直接读
el.innerHTML—— 若之前粘贴过未过滤的 HTML,innerHTML会执行其中的<script>标签
保存前不清洗 innerHTML 就写入 localStorage 是高危操作
剪贴板带进来的 Word 内容、微信消息、网页片段,常含 <style>、class="Apple-*、data-*、零宽字符(\u200b),直接存进 localStorage,下次读出来渲染时照样执行、照样崩样式。
- 清洗不是删标签那么简单:
innerHTML.replace(/<[^>]*>/g, '')会漏掉注释、CDATA、自闭合标签里的危险属性 - 安全做法:用
DOMParser解析el.innerHTML,遍历节点只保留p、br、strong、em,再调用removeAttribute('style')和removeAttribute('class') - 最后统一清理不可见字符:
.replace(/\u200b/g, '').replace(/\u00ad/g, '') - 若业务只要纯文本,直接用
el.textContent,但要注意它不保留语义换行 —— 需手动把<p>、<div>节点映射为\n
光标跳转导致保存内容与用户所见不一致
动态更新 DOM(比如 Vue/React 重渲染、或插入图片后调用 el.innerHTML = ...)会重置光标位置,用户正在中间编辑,保存的却是末尾或开头的内容。
立即学习“前端免费学习笔记(深入)”;
- 每次修改前,先用
getSelection().getRangeAt(0).cloneRange()保存光标位置 - 修改完成后,用
selection.removeAllRanges()+selection.addRange(savedRange)恢复 - IE11 不支持
cloneRange(),需 fallback 到记录 offset + node 的坐标方式 - 如果只是保存文本(非富文本),可跳过光标管理,但务必确保保存的是
textContent而非渲染后的innerHTML
最易被忽略的点:tabindex="0" 缺失会导致 el.focus() 失效,进而使 getSelection() 拿不到有效 range —— 所有光标保存/恢复逻辑都建立在这个前提上。漏掉它,整个即时保存链就断在第一步。



















