📚 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 提供了:
- 模块化架构 - 按需加载插件
- 完全控制 - 自定义节点和命令
- 高性能 - 基于不可变数据
- 扩展性强 - 支持复杂需求
推荐使用模式:
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"