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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 04:05:35