LexicalJS拼写检查器插件开发问题求助
LexicalJS拼写检查器插件问题排查与优化方案
核心问题定位
你遇到的两个问题,根源通常在于节点修改时的光标维护不当和检查触发逻辑的循环触发:
- 光标随机跳转:修改文本节点或添加高亮标记时,未正确保存/恢复光标位置,或是节点更新破坏了原有文本偏移
- 请求循环:拼写检查触发时机不合理(如
onChange事件中直接触发),且检查后的节点修改又会再次触发内容变更事件,形成闭环
针对性优化方案
1. 修复光标跳转问题
- 原子操作包裹节点修改:用
$batchUpdate包裹所有拼写标记的修改逻辑,确保节点变更一次性完成,避免多次触发光标重置 - 手动维护光标位置:在修改节点前保存当前Selection,操作完成后强制恢复:
// 拼写标记逻辑中 $batchUpdate(() => { const currentSelection = $getSelection(); // 遍历节点、添加错误标记的逻辑 $setSelection(currentSelection); });
- 避免直接修改文本内容:不要通过替换文本的方式标记错误,而是给文本片段添加自定义格式属性(如
spellError: true),这样不会改变文本的偏移结构
2. 终止请求循环
- 内容变更校验:维护上一次检查的内容快照,只有当当前内容与快照不一致时,才发起拼写检查请求
- 防抖处理请求:对拼写检查请求做防抖(建议300-500ms),避免输入过程中频繁触发请求,同时减少节点更新次数:
const debouncedCheck = useCallback( debounce(async (content) => { const lastContent = editor.getEditorState().read($getRoot).getTextContent(); if (content === lastContent) return; const errors = await fetch('/api/spell-check', { method: 'POST', body: JSON.stringify({ content }) }); // 处理错误标记 }, 300), [editor] );
- 调整触发时机:放弃在
onChange中直接触发检查,改用以下方式:- 编辑器失焦时(
onBlur)触发全量检查 - 监听单词输入完成事件(如输入空格、标点后)触发单单词检查
- 用
useEffect监听editorState变化,结合防抖触发检查
- 编辑器失焦时(
- 标记逻辑不触发内容变更:通过自定义格式属性标记错误,而非修改文本内容,这样不会触发
onChange事件,从根源避免循环
推荐实现示例
拼写检查触发逻辑
function SpellCheckPlugin() { const [lastCheckedContent, setLastCheckedContent] = useState(''); const editor = useLexicalEditor(); const checkSpelling = useCallback(async () => { const content = editor.getEditorState().read($getRoot).getTextContent(); if (content === lastCheckedContent) return; const res = await fetch('/api/spell-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }); const errors = await res.json(); // 应用错误标记 editor.update(() => { $batchUpdate(() => { const selection = $getSelection(); const root = $getRoot(); root.forEachDescendant((node) => { if (node.isTextNode()) { const nodeStart = node.getStartOffset(); const nodeEnd = node.getEndOffset(); errors.forEach((err) => { if (err.start >= nodeStart && err.end <= nodeEnd) { const localStart = err.start - nodeStart; const localEnd = err.end - nodeStart; // 添加自定义格式标记 node.formatText(localStart, localEnd, { spellError: true }); } }); } }); $setSelection(selection); }); }); setLastCheckedContent(content); }, [editor, lastCheckedContent]); // 防抖触发检查 const debouncedCheck = useCallback(debounce(checkSpelling, 300), [checkSpelling]); // 监听编辑器内容变化 useEffect(() => { return editor.registerUpdateListener(({ editorState }) => { const content = editorState.read($getRoot).getTextContent(); if (content !== lastCheckedContent) { debouncedCheck(); } }); }, [editor, debouncedCheck, lastCheckedContent]); return null; }
错误样式渲染
// 在编辑器组件中添加样式 <LexicalComposer> <RichTextPlugin contentEditableClassName="spell-check-editor" // 其他配置... /> <style> {` .spell-check-editor span[data-spell-error="true"] { text-decoration: underline red wavy; text-decoration-thickness: 1px; } `} </style> </LexicalComposer>
内容的提问来源于stack exchange,提问作者peerfunk
相关产品推荐
相关产品推荐

