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"
相关推荐
passerby606119 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了26 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅29 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc