Lexical 富文本编辑器组件详解

📚 Lexical 简介

Lexical 是 Meta 开源的基于 React 的富文本编辑器框架,用于替代 Draft.js,具有更好的性能、扩展性和稳定性。

🎯 核心特性

  • ✅ 高性能、可扩展
  • ✅ 无依赖、轻量级
  • ✅ 完整的 TypeScript 支持
  • ✅ 插件化架构
  • ✅ 嵌套编辑器支持

🔧 基础组件

1. LexicalComposer - 编辑器容器

jsx

javascript 复制代码
import { LexicalComposer } from '@lexical/react/LexicalComposer';

function App() {
  const initialConfig = {
    namespace: 'MyEditor',
    theme: theme,
    nodes: [],
    onError: (error) => console.error(error),
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      {/* 其他插件组件 */}
    </LexicalComposer>
  );
}

2. RichTextPlugin - 富文本核心

jsx

javascript 复制代码
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';

function Editor() {
  return (
    <>
      <RichTextPlugin
        contentEditable={
          <ContentEditable
            className="editor-input"
            style={{
              minHeight: '150px',
              border: '1px solid #ccc',
              padding: '10px',
            }}
          />
        }
        placeholder={
          <div className="editor-placeholder">输入内容...</div>
        }
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
    </>
  );
}

3. 完整的基础编辑器

jsx

javascript 复制代码
import React from 'react';
import { 
  LexicalComposer, 
  RichTextPlugin,
  ContentEditable,
  HistoryPlugin,
  AutoFocusPlugin
} from '@lexical/react';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
import { ListNode, ListItemNode } from '@lexical/list';
import { CodeHighlightNode, CodeNode } from '@lexical/code';
import { AutoLinkNode, LinkNode } from '@lexical/link';
import { TRANSFORMERS } from '@lexical/markdown';
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';

const theme = {
  // 自定义样式
  text: {
    bold: 'editor-text-bold',
    italic: 'editor-text-italic',
    underline: 'editor-text-underline',
    strikethrough: 'editor-text-strikethrough',
  }
};

const initialConfig = {
  namespace: 'MyEditor',
  theme,
  nodes: [
    HeadingNode,
    QuoteNode,
    TableNode,
    TableCellNode,
    TableRowNode,
    ListNode,
    ListItemNode,
    CodeNode,
    CodeHighlightNode,
    AutoLinkNode,
    LinkNode
  ],
  onError: (error) => console.error(error),
};

function MyEditor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <div className="editor-container">
        <RichTextPlugin
          contentEditable={
            <ContentEditable className="editor-input" />
          }
          placeholder={
            <div className="editor-placeholder">开始写作...</div>
          }
        />
        <HistoryPlugin />
        <AutoFocusPlugin />
        <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
      </div>
    </LexicalComposer>
  );
}

🔌 常用插件组件

1. ToolbarPlugin - 工具栏组件

jsx

ini 复制代码
import { 
  $getSelection, 
  $isRangeSelection, 
  FORMAT_TEXT_COMMAND 
} from 'lexical';

function ToolbarPlugin() {
  const [isBold, setIsBold] = useState(false);
  const [isItalic, setIsItalic] = useState(false);
  const editor = useLexicalComposerContext();

  const updateToolbar = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      setIsBold(selection.hasFormat('bold'));
      setIsItalic(selection.hasFormat('italic'));
    }
  }, []);

  // 监听编辑器状态变化
  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        updateToolbar();
      });
    });
  }, [editor, updateToolbar]);

  return (
    <div className="toolbar">
      <button
        className={isBold ? 'active' : ''}
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
      >
        加粗
      </button>
      <button
        className={isItalic ? 'active' : ''}
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
      >
        斜体
      </button>
      <button
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
      >
        下划线
      </button>
    </div>
  );
}

2. ImagePlugin - 图片上传

jsx

ini 复制代码
import { INSERT_IMAGE_COMMAND } from './ImagePlugin';

function ImagePlugin() {
  const [editor] = useLexicalComposerContext();

  const handleImageUpload = useCallback((files: FileList) => {
    const reader = new FileReader();
    reader.onload = function () {
      if (typeof reader.result === 'string') {
        editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
          altText: '图片',
          src: reader.result,
        });
      }
    };
    reader.readAsDataURL(files[0]);
  }, [editor]);

  return (
    <input
      type="file"
      accept="image/*"
      onChange={(e) => handleImageUpload(e.target.files)}
    />
  );
}

3. LinkPlugin - 链接处理

jsx

javascript 复制代码
import { LinkPlugin as LexicalLinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';

function LinkPlugin() {
  const [editor] = useLexicalComposerContext();
  const [isLink, setIsLink] = useState(false);

  const insertLink = useCallback(() => {
    if (!isLink) {
      const url = prompt('输入链接地址:', 'https://');
      if (url) {
        editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
      }
    } else {
      editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
    }
  }, [editor, isLink]);

  return (
    <>
      <button onClick={insertLink}>
        {isLink ? '取消链接' : '添加链接'}
      </button>
      <LexicalLinkPlugin />
    </>
  );
}

📋 自定义节点组件

1. 自定义节点定义

jsx

javascript 复制代码
// CustomNode.js
import { DecoratorNode } from 'lexical';

export class CustomNode extends DecoratorNode {
  static getType() {
    return 'custom';
  }

  static clone(node) {
    return new CustomNode(node.__key);
  }

  createDOM() {
    const div = document.createElement('div');
    div.className = 'custom-node';
    return div;
  }

  updateDOM() {
    return false;
  }

  decorate() {
    return <CustomComponent nodeKey={this.__key} />;
  }
}

// 自定义组件
function CustomComponent({ nodeKey }) {
  const [value, setValue] = useState('自定义内容');
  
  return (
    <div className="custom-component">
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </div>
  );
}

// 在配置中注册
const initialConfig = {
  nodes: [CustomNode, ...其他节点],
};

2. 自定义插件示例

jsx

javascript 复制代码
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';


// 自定义键盘快捷键插件
function KeyboardShortcutPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    const handleKeyDown = (event) => {
      // Ctrl + S 保存
      if ((event.ctrlKey || event.metaKey) && event.key === 's') {
        event.preventDefault();
        // 保存逻辑
        editor.update(() => {
          const editorState = editor.getEditorState();
          console.log('保存内容:', JSON.stringify(editorState.toJSON()));
        });
      }
      
      // Ctrl + B 加粗
      if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
        event.preventDefault();
        editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
      }
    };

    return editor.registerRootListener((rootElement) => {
      if (rootElement) {
        rootElement.addEventListener('keydown', handleKeyDown);
        return () => {
          rootElement.removeEventListener('keydown', handleKeyDown);
        };
      }
    });
  }, [editor]);

  return null;
}

实际使用

tsx 复制代码
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';


import { VariableTextNode } from './node';
import styles from './style.module.css';

  const initialConfig = {
    namespace: 'prompt-composer',
    nodes: [VariableTextNode],
    onError: (error: Error) => {
      console.error(error);
    },
    theme: {
      paragraph: styles.editorParagraph,
    },
  };



<div className={styles.editorWrapper}>
  <LexicalComposer initialConfig={initialConfig}>
    <div
      className={cx(styles.editor, readOnly && styles.editorReadonly)}
      style={{ height, minHeight }}
    >
      <RichTextPlugin
        contentEditable={<ContentEditable className={styles.contentEditable} />}
        placeholder={<div className={styles.placeholder}>{placeholder || ''}</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <OnChangePlugin onChange={handleEditorChange} />
      <HistoryPlugin />
      <VariablePlugin variables={variables} />
      <VariableTransformerPlugin />
      <EditorUpdatePlugin value={value} />
      <OnBlurPlugin onBlur={onBlur} />
    </div>
  </LexicalComposer>
  {!readOnly && (
    <div className={styles.resizeHandle} onMouseDown={beginResize}>
      <div className={styles.resizeIcon} />
    </div>
  )}
</div>
style.module.css 复制代码
//样式文件
.editorParagraph {
  margin: 0;
  position: relative;
}
node.ts 复制代码
//自定义节点
import { TextNode, SerializedTextNode, Spread, NodeKey, $applyNodeReplacement } from 'lexical';

import styles from './style.module.css';

export type SerializedVariableTextNode = Spread<
  {
    variableName: string;
  },
  SerializedTextNode
>;

// Custom Text Node to handle variable highlighting
export class VariableTextNode extends TextNode {
  __variableName: string;

  constructor(text: string, variableName: string, key?: NodeKey) {
    super(text, key);
    this.__variableName = variableName;
  }

  static getType(): string {
    return 'variable-text';
  }

  isSimpleText(): boolean {
    return false;
  }

  // Make this node behave as a single unit - cannot be partially selected or edited
  isTextEntity(): boolean {
    return true;
  }

  static clone(node: VariableTextNode): VariableTextNode {
    return new VariableTextNode(node.getTextContent(), node.__variableName, node.getKey());
  }

  createDOM(config: any): HTMLElement {
    const element = super.createDOM(config);
    element.className = styles.variableToken;
    element.dataset.variable = this.__variableName;
    return element;
  }

  updateDOM(prevNode: VariableTextNode, dom: HTMLElement, config: any): boolean {
    const isUpdated = super.updateDOM(prevNode as any, dom, config);
    if (prevNode.__variableName !== this.__variableName) {
      dom.dataset.variable = this.__variableName;
      return true;
    }
    return isUpdated;
  }

  exportJSON(): SerializedVariableTextNode {
    return {
      ...super.exportJSON(),
      variableName: this.__variableName,
      type: 'variable-text',
    };
  }

  static importJSON(serializedNode: SerializedVariableTextNode): VariableTextNode {
    const node = new VariableTextNode(serializedNode.text, serializedNode.variableName);
    node.setFormat(serializedNode.format);
    node.setDetail(serializedNode.detail);
    node.setMode(serializedNode.mode);
    node.setStyle(serializedNode.style);
    return node;
  }
}

export function $createVariableTextNode(text: string, variableName: string): VariableTextNode {
  return $applyNodeReplacement(new VariableTextNode(text, variableName));
}//创建并注册一个VariableTextNode节点实例
  • $applyNodeReplacement的作用
ts 复制代码

// 这是 Lexical 的内部工具函数 import { $applyNodeReplacement } from 'lexical';

// 它主要做三件事: function $applyNodeReplacement(newNode) { // 1. ✅ 检查节点是否已存在(通过 key) // 2. ✅ 如果存在,返回已存在的节点(避免重复) // 3. ✅ 如果不存在,注册新节点到编辑器状态 // 4. ✅ 确保节点在编辑器中被正确追踪 }

复制代码

📝 总结

Lexical 提供了:

  1. 模块化架构 - 按需加载插件
  2. 完全控制 - 自定义节点和命令
  3. 高性能 - 基于不可变数据
  4. 扩展性强 - 支持复杂需求

推荐使用模式:

jsx

xml 复制代码
<LexicalComposer>
  <Toolbar />
  <RichTextPlugin />
  <EssentialPlugins />
  <CustomPlugins />
  <UtilityPlugins />
</LexicalComposer>
  • "@lexical/code": "^0.38.2"
  • "@lexical/link": "^0.38.2"
  • "@lexical/list": "^0.38.2"
  • "@lexical/react": "^0.16.0"
  • "@lexical/selection": "^0.38.2"
  • "@lexical/text": "^0.38.2"
  • "@lexical/utils": "^0.38.2"
相关推荐
星_离2 小时前
高德地图-物流路线
前端·vue.js
qq_406176142 小时前
JavaScript中的循环
前端
小皮虾2 小时前
搞全栈还在纠结 POST、GET、RESTful?试试这个,像调用本地函数一样写接口
前端·node.js·全栈
掘金安东尼2 小时前
⏰前端周刊第445期(2025年12月15日–12月21日)
前端
AAA阿giao2 小时前
JavaScript 中 this 的终极解析:从 call、bind 到箭头函数的深度探索
前端·javascript·ecmascript 6
404NotFound3052 小时前
利用 WebMKS 和 Java 实现前端访问虚拟机网页
前端
文心快码BaiduComate2 小时前
插件开发实录:我用Comate在VS Code里造了一场“能被代码融化”的初雪
前端·后端·前端框架
嘻哈baby2 小时前
搞了三年运维,这些脚本我天天在用
前端
inCBle2 小时前
vue2 封装一个自动校验是否溢出的 tooltip 自定义指令
前端·javascript·vue.js