必须为 contenteditable 元素显式添加 tabindex="0",否则焦点不可控、Selection API 失效、协作状态无法初始化;粘贴应优先用纯文本,回车需归一化为 <p>,sanitize 仅限 paste/enter/blur 三处执行。

contenteditable="true" 不加 tabindex="0" 就别想协作
协作编辑器里多人光标、实时同步、焦点移交全依赖可预测的焦点链。只写 contenteditable="true",浏览器根本不把它当可聚焦元素——el.focus() 失效、Tab 切不进去、Selection API 拿不到 range,协作状态根本无法初始化。
必须显式加 tabindex="0",这是协作能力的启动开关。漏掉这步,后续所有光标同步、操作广播、冲突检测都是空中楼阁。
-
tabindex="-1"只支持 JS 主动聚焦,用户无法用键盘自然进入,协作流断裂 - 某些旧版 Safari 在
tabindex缺失时,点击后光标闪一下就消失,协作端看到“对方已进入但无光标” - 父容器有
overflow: hidden且内容溢出时,tabindex="0"可能触发意外滚动,建议加scroll-behavior: smooth缓解
粘贴 HTML 是协作场景下的性能雪崩点
用户从 Word、Notion 或其他编辑器粘贴内容时,event.clipboardData.getData('text/html') 返回的 DOM 片段常含几十层嵌套、内联 style、data- 属性、注释节点。协作编辑器每收到一次粘贴,就要解析 + 渲染 + 样式计算 + diff + 广播变更,主线程卡顿直接导致光标不同步、操作延迟、本地输入失焦。
真正可行的协作策略是:默认走纯文本路径,仅在必要语义处重建结构。
立即学习“前端免费学习笔记(深入)”;
- 优先用
e.clipboardData.getData('text/plain')获取干净字符串,插入成本近乎为零 - 若需保留段落/加粗等基础语义,先用
DOMPurify.sanitize(html, {ALLOWED_TAGS: ['b','i','u','p','br']})过滤,比全量解析快 3–5 倍 - 绝对禁止
innerHTML = e.clipboardData.getData('text/html')—— 这是 XSS 和协作延迟双坑
回车行为不统一让协同 DOM Diff 失效
Chrome 插 <div>,Safari 插 <p>,Firefox 两者都可能。协作编辑器靠 DOM 结构一致性做 diff 和 merge,混杂的块级标签会让本地和远端的树结构快速偏离,轻则格式错乱,重则操作被丢弃或重复应用。
必须拦截回车并归一化插入逻辑,不能依赖浏览器默认行为。
- 监听
keydown,检测e.key === 'Enter'后立即e.preventDefault() - 用
document.getSelection()+range.insertNode(document.createElement('p'))统一插<p> - CSS 补
white-space: pre-wrap防连续空格塌缩,避免额外空格处理逻辑干扰 diff
高频 input 下 DOMPurify.sanitize() 是协作延迟主因
每敲一个字都跑一遍 DOMPurify.sanitize(),单次耗时 5–20ms,在低配安卓机上极易掉帧。协作编辑器里,这个操作还会触发本地变更广播、远程 diff 计算、光标位置重同步,三重开销叠加,输入延迟立刻超过 300ms,用户感知就是“卡住”“光标不动”“别人改了我看不到”。
sanitize 应严格限定在三个节点执行:粘贴(paste)、回车(keydown.enter)、失焦(blur)。其余时间保持原始 DOM 流通。
- 用正则预筛:先
el.innerHTML.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')快速剔除 script 标签,再进 DOMPurify - 启用
RETURN_DOM模式并缓存 purified node,但注意不能直接插入 document,需cloneNode(true) - 纯文本协作场景(如评论、聊天)彻底放弃 sanitize,改用
textContent读写,零成本防 XSS
contenteditable 子节点独立维护编辑上下文——子节点越多,光标同步、Selection 更新、事件调度的开销越指数级增长。精简可编辑节点层级,比优化单次 sanitize 更关键。



















