基于React的Lexical Editor类Grammarly功能及词位置检测方案咨询
React + Lexical 实现类Grammarly编辑器:单词索引捕获与Socket发送方案
核心思路
利用Lexical的内容变更监听和节点定位能力,精准捕捉每个单词的绝对位置(包括修改前后的索引),再通过Socket增量发送数据,避免冗余传输——重点解决句子中间单词修改时的位置检测问题。
一、Lexical 端关键实现
1. 监听内容变更
通过Lexical的registerUpdateListener监听编辑器状态变化,对比前后状态找出变更节点:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useEffect } from 'react'; import { TextNode } from 'lexical'; function EditorChangeHandler() { const [editor] = useLexicalComposerContext(); useEffect(() => { return editor.registerUpdateListener(({ editorState, prevEditorState }) => { const changes = editorState.getDifference(prevEditorState); if (changes.length === 0) return; // 处理变更逻辑 processContentChanges(changes, editorState); }); }, [editor]); return null; }
2. 精准定位修改的中间单词
Lexical的TextNode自带偏移量方法,能直接获取节点在文档中的绝对起始位置,结合文本分割就能算出单个单词的索引:
function processContentChanges(changes, editorState) { editorState.read(() => { changes.forEach((change) => { const { node, type, prevNode } = change; if (!(node instanceof TextNode)) return; const prevText = prevNode?.getTextContent() || ''; const currText = node.getTextContent(); const nodeStart = node.getStartOffset(); // 当前节点在整个文档的起始位置 // 处理单词修改场景(比如改句子中间的某个词) if (type === 'update') { const prevWords = prevText.split(/\s+/); const currWords = currText.split(/\s+/); prevWords.forEach((oldWord, idx) => { const newWord = currWords[idx]; if (oldWord !== newWord) { // 计算该单词的绝对起始索引:节点起始位置 + 前面所有单词的长度+空格总和 const wordStart = nodeStart + prevWords.slice(0, idx).reduce((total, word) => total + word.length + 1, 0); const wordEnd = wordStart + newWord.length; // 准备发送给Socket的数据 const payload = { word: newWord, startIndex: wordStart, endIndex: wordEnd, action: 'update' }; sendToSocket(payload); } }); } // 新增/删除单词的逻辑类似,只需调整payload的action字段和索引计算方式 }); }); }
3. 实时捕获输入中的单词
如果是用户正在实时输入(而非批量修改),可以结合选区监听,实时抓取当前输入的单词:
import { useSelection } from '@lexical/react/useSelection'; function RealTimeWordCapture() { const [selection] = useSelection(); const [editor] = useLexicalComposerContext(); useEffect(() => { if (!selection || !selection.isCollapsed()) return; editor.read(() => { const activeNode = selection.getNodes()[0]; if (!(activeNode instanceof TextNode)) return; const fullText = activeNode.getTextContent(); const cursorPos = selection.anchor.offset; // 从光标位置向前截取完整单词(支持带缩写的词,比如don't) const wordMatch = fullText.slice(0, cursorPos).match(/([\w']+)$/) || []; const currentWord = wordMatch[0] || ''; if (currentWord) { const wordStart = activeNode.getStartOffset() + (cursorPos - currentWord.length); sendToSocket({ word: currentWord, startIndex: wordStart, endIndex: wordStart + currentWord.length, action: 'input' }); } }); }, [selection, editor]); return null; }
二、Socket 发送优化
- 防抖处理:给Socket请求加300ms防抖,避免用户快速输入时频繁发请求,合并同一单词的多次修改
- 增量发送:只传变更的单词数据,不要发整个文档,减少带宽消耗
- 容错机制:发送失败时本地缓存未发送的payload,等Socket重连后自动补发
三、边界情况处理
- 特殊字符:调整单词分割正则为
/([\w']+)/g,兼容缩写、连字符单词 - 多行文本:遍历Lexical的根节点所有子节点,累加每个节点的长度,计算跨节点单词的绝对索引
- 撤销/重做:通过
editorState.getEditorHistory()获取操作类型,给payload加undo/redo标识
内容的提问来源于stack exchange,提问作者Noor Shaker
相关产品推荐
相关产品推荐

