怎样实现一个支持撤销重做(Undo/Redo)功能的富文本编辑器?

怎样实现一个支持撤销重做(Undo/Redo)功能的富文本编辑器?
最新回答
我在时光深处等你

2026-03-15 11:16:46

实现一个支持撤销重做功能的富文本编辑器,需结合contenteditable、事件监听、状态管理和DOM操作,核心步骤如下:

1. 基础结构与事件监听
  • HTML结构:使用contenteditable="true"的容器作为编辑区域。<div id="editor" contenteditable="true"></div>
  • 关键事件:

    input:捕获内容变更(但需过滤无效事件)。

    keydown:监听快捷键(如Ctrl+Z/Ctrl+Y)。

    beforeinput/compositionstart/end:处理中文输入法等复合输入,避免中间状态误记录。

    MutationObserver:监听DOM结构变化,辅助判断真实内容变更。

2. 状态管理(Undo/Redo栈)
  • 数据结构:维护两个栈(undoStack和redoStack),每个栈存储状态快照,包含:

    内容:innerHTML或序列化后的DOM结构。

    选区信息:光标位置(start/end)和节点路径(如[0, 0]表示第1个子节点的第1个子节点)。

    const stateSnapshot = { content: "<p>Hello</p>", selection: { start: 5, end: 5, nodePath: [0, 0] }};
  • 操作规则:

    每次用户操作触发可感知变更时,将当前状态压入undoStack,并清空redoStack。

    撤销时从undoStack弹出状态,恢复内容与光标,并压入redoStack;重做逻辑相反。

3. 智能记录变更(优化性能)
  • 节流与合并:

    连续输入:首次输入和停止1-2秒后记录一次,避免频繁快照。

    格式化操作(如加粗、对齐):立即生成新快照。

    MutationObserver:监听DOM变化,过滤无实际内容的变更(如属性修改)。

  • 示例逻辑:let isComposing = false; // 中文输入法状态let inputTimeout;editor.addEventListener('input', () => { if (isComposing) return; clearTimeout(inputTimeout); inputTimeout = setTimeout(() => saveState(), 1000); // 节流1秒});editor.addEventListener('compositionstart', () => isComposing = true);editor.addEventListener('compositionend', () => isComposing = false);
4. 执行撤销与重做
  • 快捷键绑定:document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'z') { e.preventDefault(); undo(); } if (e.ctrlKey && e.key === 'y') { e.preventDefault(); redo(); }});
  • 撤销/重做函数:

    还原内容:直接设置editor.innerHTML为快照内容。

    还原光标:通过nodePath定位节点,使用Range重建选区。

    function undo() { if (undoStack.length === 0) return; const state = undoStack.pop(); redoStack.push(getCurrentState()); editor.innerHTML = state.content; restoreSelection(state.selection);}function restoreSelection({ start, end, nodePath }) { const range = document.createRange(); let node = editor; nodePath.forEach(index => node = node.childNodes[index]); range.setStart(node, start); range.setEnd(node, end); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range);}
5. 边界情况处理
  • 中文输入法:通过compositionstart/end事件屏蔽中间状态。
  • 复杂节点:图片、表格等需完整序列化(如存储为Base64或引用ID)。
  • 内存限制:设置栈最大长度(如50步),超限时丢弃最旧状态。
  • 协作编辑:若需支持多用户,需结合OT(Operational Transformation)或CRDT算法(本地单人编辑无需)。
6. 性能优化
  • 防抖/节流:减少不必要的快照。
  • 差异对比:仅保存内容变更部分(如使用Diff算法)。
  • 虚拟DOM:大规模内容时可用虚拟DOM减少重绘。
7. 替代方案(基于现有框架)

若不想从零实现,可选用内置撤销重做的富文本框架:

  • Slate.js:插件化设计,支持自定义操作历史。
  • ProseMirror:基于事务的撤销重做,适合复杂文档。
  • Quill.js:提供模块化扩展,可集成撤销栈。
总结

实现撤销重做的关键在于:

  1. 精准捕获变更:结合事件监听与DOM观察。
  2. 合理管理状态:通过栈结构存储有意义的快照。
  3. 优化用户体验:平衡性能与操作粒度,处理边界情况。

理解底层原理后,可根据需求选择自行开发或基于框架扩展。