基于Lexical JS实现Messenger式@提及功能遇报错求助
错误原因分析
- 节点引用跨状态快照:你在
onChange的editorState快照中获取了selectedNode,随后在editor.update闭包里复用该引用。但editor.update操作的是最新编辑器状态,快照中的节点已不属于当前活跃状态,因此触发Lexical的节点有效性校验错误。 - 重复插入节点:同时调用
$insertNodes([menuNode])和selectedNode.insertAfter(menuNode),属于重复插入操作,会导致状态冲突。 - 无触发条件:每次
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
相关产品推荐
相关产品推荐

