You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Lexical编辑器中实现修订追踪(Track Changes)功能?

在Lexical中实现修订追踪(Track Changes)的正确方式

问题根源

你之前用registerUpdateListener事后对比全文本再修改编辑器状态的方式,会额外触发一次更新,直接污染历史栈;而且通过全文本对比定位变更的逻辑精度不足,没法精准对应到具体的插入/删除位置。

正确实现思路

Lexical的修订追踪应该在变更发生时直接处理,利用自定义Mark节点和Mutation监听来实现,同时避免干扰编辑器的正常历史记录。


步骤1:定义自定义Mark节点

先创建对应插入、删除状态的Mark节点,绑定需求的CSS类:

class InsertedMark extends MarkNode {
  static getType() {
    return 'inserted';
  }

  static clone(node) {
    return new InsertedMark(node.__key);
  }

  createDOM() {
    const dom = document.createElement('span');
    dom.className = 'inserted';
    dom.style.textDecoration = 'underline';
    dom.style.textDecorationColor = 'green';
    return dom;
  }

  static importDOM() {
    return {
      span: (domNode) => {
        if (domNode.className === 'inserted') {
          return { node: $createInsertedMark() };
        }
        return null;
      },
    };
  }
}

class DeletedMark extends MarkNode {
  static getType() {
    return 'deleted';
  }

  static clone(node) {
    return new DeletedMark(node.__key);
  }

  createDOM() {
    const dom = document.createElement('span');
    dom.className = 'deleted';
    dom.style.textDecoration = 'line-through';
    dom.style.textDecorationColor = 'red';
    return dom;
  }

  static importDOM() {
    return {
      span: (domNode) => {
        if (domNode.className === 'deleted') {
          return { node: $createDeletedMark() };
        }
        return null;
      },
    };
  }
}

// 注册自定义节点
function registerCustomMarks(editor) {
  editor.registerNode(InsertedMark);
  editor.registerNode(DeletedMark);
}

步骤2:用Mutation监听处理变更

通过registerMutationListener精准捕获文本节点的创建、更新、删除事件,实时标记变更内容:

function TrackChangesPlugin() {
  const [editor] = useLexicalComposerContext();

  useLayoutEffect(() => {
    registerCustomMarks(editor);

    const unregisterMutation = editor.registerMutationListener(
      TextNode,
      (mutations) => {
        editor.update(() => {
          for (const mutation of mutations) {
            const { target, type, payload } = mutation;

            // 处理插入操作:新文本节点或文本内容增加
            if (type === 'create' || (type === 'update' && payload.text.length > target.__text.length)) {
              const start = target.__text.length;
              const end = payload.text.length;
              const selection = $createRangeSelection();
              selection.anchor = $createPoint(target.__key, start);
              selection.focus = $createPoint(target.__key, end);
              $wrapSelectionInMarkNode(selection, false, InsertedMark);
            }

            // 处理删除操作:文本内容减少或节点被删除
            if ((type === 'update' && payload.text.length < target.__text.length) || type === 'delete') {
              if (type === 'update') {
                const start = payload.text.length;
                const end = target.__text.length;
                const selection = $createRangeSelection();
                selection.anchor = $createPoint(target.__key, start);
                selection.focus = $createPoint(target.__key, end);
                $wrapSelectionInMarkNode(selection, false, DeletedMark);
              } else if (type === 'delete') {
                // 保留删除痕迹:把删除的文本用DeletedMark包裹后插入原位置
                const deletedNode = $createTextNode(target.__text);
                deletedNode.addMark($createDeletedMark());
                const parent = target.getParent();
                parent?.insertBefore(deletedNode, null);
              }
            }
          }
        }, { tag: 'track-changes' }); // 合并到同一历史条目,避免污染栈
      }
    );

    return unregisterMutation;
  }, [editor]);

  return null;
}

关键细节

  • 在editor.update中添加tag参数,Lexical会将标记操作与用户原始操作合并为同一个历史记录条目,不会额外生成历史栈节点。
  • 用registerMutationListener替代registerUpdateListener,可以直接捕获节点级别的变更,比全文本对比效率更高、精度更准。
  • 如果需要区分多用户修订,可以给Mark节点添加userId等自定义属性,在创建时记录相关信息。

内容的提问来源于stack exchange,提问作者Sai Krishnan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 18:23:23