Pinia 通过语义化操作快照核心业务状态(如 content、selection)实现富文本编辑器的高效撤销/重做,避免镜像 DOM;借助 pinia-undo 插件统一管理,并在状态变更后同步更新编辑器实例。

Pinia 本身不直接管理富文本编辑器的 DOM 状态,但可以高效托管其「可撤销的业务状态」——比如编辑内容、光标位置、选区信息、格式元数据等。关键不是监听 every keystroke,而是把编辑器操作语义化后,由 Pinia 统一记录快照、驱动回退逻辑。
只快照核心业务状态,不镜像整个 DOM
富文本编辑器(如 Quill、Tiptap、或者原生 contenteditable)内部状态复杂,直接深拷贝 innerHTML 或 selection 对象既低效又易出错。正确做法是:
- 定义一个精简 state 结构,例如:content: string(纯 HTML 片段)、selection: { anchorOffset, focusOffset, anchorNodePath }(序列化后的光标路径)
- 避免保存 loading、isFocused、scrollTop 等 UI 衍生状态;它们应在组件内响应式更新,而非进入历史栈
- 使用 toRaw() + structuredClone() 获取纯净快照(现代浏览器),或 JSON.parse(JSON.stringify()) 作降级兼容
在编辑器事件中触发语义化 action
不要在 input 或 keydown 里调用 track(),而应封装成用户可感知的操作单元:
- 当用户点击「加粗」按钮 → 触发 toggleBold(),action 内先 this.track('切换加粗'),再调用编辑器 API 执行格式变更
- 粘贴一段文字 → pasteHtml(html),track 后插入并 normalize 选区
- 输入中文时需注意:compositionstart / compositionend 事件后才真正落笔,此时再 track,避免记录中间拼音状态
用插件统一注入 undo/redo 能力
推荐使用 pinia-undo 插件,它开箱即用且支持精细配置:
- 安装后一行注册:pinia.use(PiniaUndo)
- 在 store 中声明忽略字段,例如跳过临时高亮标记:undo: { omit: ['highlightId', 'isPreviewing'] }
- 组件中直接访问 store.canUndo 控制按钮状态,调用 store.undo() 恢复上一步内容与光标
还原时同步编辑器真实 DOM
Pinia 回退只是更新了 state,必须主动将快照同步到编辑器实例:
- watch store.content 和 store.selection,变化时调用编辑器的 setContents() 或 restoreSelection()
- 若用 Tiptap,可用 editor.commands.setContent(snapshot.content) + editor.view.dispatch(editor.state.tr.setSelection(...))
- 务必在 nextTick 或 requestIdleCallback 中执行 DOM 更新,避免冲突和卡顿


















