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

基于Lexical JS实现Messenger式@提及功能遇报错求助

错误原因分析
  1. 节点引用跨状态快照:你在onChange的editorState快照中获取了selectedNode,随后在editor.update闭包里复用该引用。但editor.update操作的是最新编辑器状态,快照中的节点已不属于当前活跃状态,因此触发Lexical的节点有效性校验错误。
  2. 重复插入节点:同时调用$insertNodes([menuNode])和selectedNode.insertAfter(menuNode),属于重复插入操作,会导致状态冲突。
  3. 无触发条件:每次onChange都会插入MenuNode,引发循环状态更新和节点冗余。
修正后的代码示例

1. 完善MenuNode类

export class MenuNode extends DecoratorNode<ReactNode> {
  __searchText: string;

  static getType(): string {
    return 'menu';
  }

  static clone(node: MenuNode): MenuNode {
    return new MenuNode(node.__searchText, node.__key);
  }

  constructor(searchText: string, key?: string) {
    super(key);
    this.__searchText = searchText;
  }

  // 用于更新搜索文本的方法
  updateSearchText(newSearchText: string): void {
    const writableNode = this.getWritable();
    writableNode.__searchText = newSearchText;
  }

  createDOM(): HTMLElement {
    const div = document.createElement('div');
    div.className = 'mention-autocomplete-menu';
    return div;
  }

  updateDOM(prevNode: MenuNode): boolean {
    // 搜索文本变化时触发重新渲染
    return prevNode.__searchText !== this.__searchText;
  }

  decorate(): ReactNode {
    return <WordOptions searchText={this.__searchText} />;
  }

  // 设置菜单不可选中,避免干扰文本编辑
  isSelectable(): boolean {
    return false;
  }
}

2. 修正onChange逻辑

function onChange(editorState: EditorState, editor: LexicalEditor) {
  editor.update(() => {
    const selection = $getSelection();
    // 仅处理文本选区场景
    if (!(selection instanceof RangeSelection)) return;

    const anchorNode = selection.anchor.getNode();
    // 确保当前节点是文本节点
    if (!(anchorNode instanceof TextNode)) return;

    const textContent = anchorNode.getTextContent();
    const atIndex = textContent.lastIndexOf('@');
    
    // 没有@符号时移除现有菜单
    if (atIndex === -1) {
      $getNodesByType(MenuNode).forEach(node => $removeNode(node));
      return;
    }

    const searchText = textContent.slice(atIndex + 1);
    const existingMenuNodes = $getNodesByType(MenuNode);

    // 复用现有菜单节点,更新搜索文本
    if (existingMenuNodes.length > 0) {
      existingMenuNodes[0].updateSearchText(searchText);
      return;
    }

    // 无现有菜单时插入新节点
    const menuNode = new MenuNode(searchText);
    anchorNode.insertAfter(menuNode);
  });
}

3. 编辑器组件(保持原有结构)

export default function Editor() {
  const initialConfig = {
    namespace: 'MyEditor', 
    theme,
    editable: true,
    onError,
    nodes: [MenuNode]
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>输入@开始提及...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <OnChangePlugin onChange={onChange} />
      <HistoryPlugin />
      <MyCustomAutoFocusPlugin />
    </LexicalComposer>
  );
}
核心修正说明
  • 状态内操作节点:所有节点的获取、修改、插入操作都在editor.update闭包内完成,确保使用的是当前活跃状态的节点引用。
  • 条件触发与清理:只有输入@时才显示菜单,无@时自动移除,避免无效状态变更。
  • 节点复用:检测到已有菜单时直接更新搜索文本,而非重复插入新节点,减少状态波动。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 17:01:19