news 2026/9/12 9:27:13

Lexical React 富文本编辑器最小示例解析:从 RichTextPlugin 到自定义工具栏与 HTML 导入导出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Lexical React 富文本编辑器最小示例解析:从 RichTextPlugin 到自定义工具栏与 HTML 导入导出

Lexical React 富文本编辑器最小示例解析:从 RichTextPlugin 到自定义工具栏与 HTML 导入导出

【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical

本篇文章以 Lexical 仓库中 examples/react-rich/README.md 所描述的 React 富文本示例为核心,逐步拆解一个"最小可用"的 Lexical 富文本编辑器是如何从零搭建的:包括@lexical/rich-text富文本配置、@lexical/history历史记录、@lexical/dragon无障碍特性,以及工具栏插件、调试树视图插件和一套完整的 HTML 导入/导出双向映射机制。读完本文,你将掌握 LexicalComposer 的组合方式、常用命令与插件的挂载套路,并理解示例中对粘贴内容样式做白名单过滤的底层实现。

示例概览与运行方式

react-rich是 Lexical 官方仓库中最精简的富文本示例之一。按 examples/react-rich/README.md 的描述,它在 rich text 配置(@lexical/rich-text)基础上,同时开启了历史记录(@lexical/history)与无障碍(@lexical/dragon)能力。README 给出的本地运行命令是:

pnpm i && pnpm run dev

该命令在 examples/react-rich/package.json 中对应的脚本是dev: vite,即使用 Vite 启动开发服务器;同一文件中还提供了buildtsc && vite build)、previewvite preview)以及面向 monorepo 内部开发的monorepo:devvite -c vite.config.monorepo.ts)脚本。项目的依赖锁定在 Lexical 0.50.0 版本,核心依赖为lexical@lexical/react@lexical/utils,UI 层使用 React 19 与 react-dom 19,构建工具链为 Vite 7 与 TypeScript 5.9(详见 examples/react-rich/package.json)。

Vite 配置本身非常标准,仅注册了 React 插件(examples/react-rich/vite.config.ts):

import react from '@vitejs/plugin-react'; import {defineConfig} from 'vite'; export default defineConfig({ plugins: [react()], });

页面入口 examples/react-rich/index.html 挂载#root节点并加载/src/main.tsx,而 examples/react-rich/src/main.tsx 使用ReactDOM.createRoot渲染<App />,页面标题为 "React.js Rich Text Lexical Example"。

应用骨架:LexicalComposer 与三大核心插件

整个编辑器的核心骨架位于 examples/react-rich/src/App.tsx。App组件通过<LexicalComposer initialConfig={editorConfig}>创建编辑器实例,内部依次挂载了四个插件:

<LexicalComposer initialConfig={editorConfig}> <div className="editor-container"> <ToolbarPlugin /> <div className="editor-inner"> <RichTextPlugin contentEditable={ <ContentEditable className="editor-input" aria-placeholder={placeholder} placeholder={ <div className="editor-placeholder">{placeholder}</div> } /> } ErrorBoundary={LexicalErrorBoundary} /> <HistoryPlugin /> <AutoFocusPlugin /> <TreeViewPlugin /> </div> </div> </LexicalComposer>

这四个插件各司其职:

  • RichTextPlugin:来自@lexical/react/LexicalRichTextPlugin(对应源码 packages/lexical-react/src/LexicalRichTextPlugin.tsx),它内部会挂载@lexical/rich-text提供的富文本命令处理(如回车分段、斜杠命令、格式化快捷键等),是"富文本"能力的关键载体。它接收一个contentEditable属性,示例中使用<ContentEditable>组件渲染可编辑区域,并通过aria-placeholderplaceholder同时声明无障碍占位文本与可视占位符(占位文案为'Enter some rich text...',见 examples/react-rich/src/App.tsx);ErrorBoundary={LexicalErrorBoundary}则指定渲染错误边界。
  • HistoryPlugin:来自@lexical/react/LexicalHistoryPlugin,内部封装@lexical/history的撤销/重做栈,为工具栏的 Undo/Redo 按钮提供底层支持。
  • AutoFocusPlugin:来自@lexical/react/LexicalAutoFocusPlugin,挂载后编辑器获得焦点。
  • TreeViewPlugin:来自@lexical/react/LexicalTreeView,渲染一个实时的编辑器节点树调试面板,方便观察每次更新后的内部状态(详见下文"调试利器"一节)。

README 提到的@lexical/dragon(无障碍)能力则由@lexical/react内部在富文本场景中启用,README 将其列为该示例已开启的特性之一。

editorConfig 配置剖析:namespace、theme 与 HTML 双向映射

editorConfig是 LexicalComposer 的初始化配置,examples/react-rich/src/App.tsx 中声明如下:

const editorConfig = { html: { export: exportMap, import: constructImportMap(), }, namespace: 'React.js Demo', nodes: [ParagraphNode, TextNode], onError(error: Error) { throw error; }, theme: ExampleTheme, };

各字段含义:

  • namespace'React.js Demo',编辑器的命名空间标识,用于区分同一页面上的多个编辑器实例。
  • nodes:声明参与编辑的节点类,此处仅为ParagraphNodeTextNode这两个最基础的内置节点。相比lexical-playground这类完整演示(会注册标题、列表、引用、代码等大量节点),这正是"最小富文本"的体现——需要更多能力时,只需在数组中加入对应节点类。
  • themeExampleTheme,负责把 Lexical 节点格式映射为 CSS class(详见下文"主题映射"一节)。
  • onError:错误处理回调,示例直接抛出异常,便于开发期暴露问题。
  • html.export / html.import:HTML 导出与导入的映射配置,是本示例区别于其他示例的最大亮点,下面展开讲解。

导出映射:剔除内联样式与 class

exportMap的类型是DOMExportOutputMap,它将ParagraphNodeTextNode都映射到removeStylesExportDOM处理器(examples/react-rich/src/App.tsx):

const removeStylesExportDOM = ( editor: LexicalEditor, target: LexicalNode, ): DOMExportOutput => { const output = target.exportDOM(editor); if (output && isHTMLElement(output.element)) { // Remove all inline styles and classes if the element is an HTMLElement // Children are checked as well since TextNode can be nested // in i, b, and strong tags. for (const el of [ output.element, ...output.element.querySelectorAll('[style],[class]'), ]) { el.removeAttribute('class'); el.removeAttribute('style'); } } return output; };

该处理器先调用节点的默认exportDOM得到 HTML 元素,随后遍历该元素及其所有带[style][class]属性的子孙元素,逐一移除classstyle属性。注释中特别说明:由于 TextNode 可能嵌套在ibstrong等标签内部,因此需要连同子孙元素一起清理。也就是说,导出的 HTML 是"语义干净"的,不携带任何内联样式与自定义 class。

导入映射:对粘贴内容做样式白名单

与导出对应,constructImportMap()构建一个DOMConversionMap,其目的是"包装"TextNode.importDOM的所有默认导入器,在导入(粘贴/拖拽/HTML 序列化反解)时额外解析并恢复允许的白名单样式(examples/react-rich/src/App.tsx)。实现要点有三:

  1. 先触发TextNode.getType():代码注释解释了原因——Lexical 采用$config()协议,节点的静态方法(包括importDOM)在首次静态访问时才生成,因此要先调用getType()确保TextNode.importDOM已被填充。
  2. 逐个包装默认导入器:遍历importDOMFn()返回的{tag: importer}表,对每个 tag 的导入器包一层conversion,在保留原转换结果的基础上,通过getExtraStyles提取额外样式。
  3. 通过forChild注入样式:只有当原转换结果是forChild风格(返回的output.node为 null、没有after)时才注入;注入方式是重写forChild,在子节点转换结果上调用textNode.setStyle(textNode.getStyle() + extraStyles)

getExtraStyles(examples/react-rich/src/App.tsx)是样式白名单的核心,它只接受三种样式,且必须与导出时生成的样式形态完全一致:

const getExtraStyles = (element: HTMLElement): string => { let extraStyles = ''; const fontSize = parseAllowedFontSize(element.style.fontSize); const backgroundColor = parseAllowedColor(element.style.backgroundColor); const color = parseAllowedColor(element.style.color); if (fontSize !== '' && fontSize !== '15px') { extraStyles += `font-size: ${fontSize};`; } if (backgroundColor !== '' && backgroundColor !== 'rgb(255, 255, 255)') { extraStyles += `background-color: ${backgroundColor};`; } if (color !== '' && color !== 'rgb(0, 0, 0)') { extraStyles += `color: ${color};`; } return extraStyles; };

默认值(15px 字号、白色背景、黑色文字)会被忽略,不写入额外样式。两个解析函数定义在 examples/react-rich/src/styleConfig.ts:

const MIN_ALLOWED_FONT_SIZE = 8; const MAX_ALLOWED_FONT_SIZE = 72; export const parseAllowedFontSize = (input: string): string => { const match = input.match(/^(\d+(?:\.\d+)?)px$/); if (match) { const n = Number(match[1]); if (n >= MIN_ALLOWED_FONT_SIZE && n <= MAX_ALLOWED_FONT_SIZE) { return input; } } return ''; }; export function parseAllowedColor(input: string) { return /^rgb\(\d+, \d+, \d+\)$/.test(input) ? input : ''; }

可以看到:字号只接受8px72px之间的px值(含小数),颜色只接受标准rgb(r, g, b)格式,其余一律返回空字符串,从而将粘贴内容中的任意样式过滤在编辑器之外——这是生产环境中防止"粘贴污染"的一种轻量且有效的实现范式。

主题映射:ExampleTheme 的 CSS class 约定

examples/react-rich/src/ExampleTheme.ts 定义了编辑器主题对象,将每种节点/格式映射为 CSS 类名:

export default { code: 'editor-code', heading: {h1: 'editor-heading-h1', /* h2 ~ h5 同理 */}, image: 'editor-image', link: 'editor-link', list: { listitem: 'editor-listitem', nested: {listitem: 'editor-nested-listitem'}, ol: 'editor-list-ol', ul: 'editor-list-ul', }, paragraph: 'editor-paragraph', placeholder: 'editor-placeholder', quote: 'editor-quote', text: { bold: 'editor-text-bold', code: 'editor-text-code', hashtag: 'editor-text-hashtag', italic: 'editor-text-italic', overflowed: 'editor-text-overflowed', strikethrough: 'editor-text-strikethrough', underline: 'editor-text-underline', underlineStrikethrough: 'editor-text-underlineStrikethrough', }, };

这些类名(如editor-paragrapheditor-text-bold)与 examples/react-rich/src/styles.css 中的选择器一一对应。styles.css 同时定义了.editor-container(最大宽度 600px 的居中容器)、.editor-input(最小高度 150px、outline: 0、无 resize 的可编辑区域)以及工具栏按钮、TreeView 调试面板等样式。

工具栏插件:命令驱动的格式化实战

examples/react-rich/src/plugins/ToolbarPlugin.tsx 是本示例中最能体现 Lexical 命令体系的部分。它通过useLexicalComposerContext()拿到editor实例,然后用mergeRegister(来自@lexical/utils)一次性注册四类监听:

useEffect(() => { return mergeRegister( editor.registerUpdateListener(({editorState}) => { editorState.read(() => { $updateToolbar(); }, {editor}); }), editor.registerCommand( SELECTION_CHANGE_COMMAND, (_payload, _newEditor) => { $updateToolbar(); return false; }, COMMAND_PRIORITY_LOW, ), editor.registerCommand(CAN_UNDO_COMMAND, payload => { setCanUndo(payload); return false; }, COMMAND_PRIORITY_LOW), editor.registerCommand(CAN_REDO_COMMAND, payload => { setCanRedo(payload); return false; }, COMMAND_PRIORITY_LOW), ); }, [editor, $updateToolbar]);
  • registerUpdateListener在每次编辑器状态更新后,于只读上下文中调用$updateToolbar,用selection.hasFormat('bold')等 API 同步按钮的激活态(examples/react-rich/src/plugins/ToolbarPlugin.tsx)。
  • SELECTION_CHANGE_COMMAND保证选区移动时工具栏状态即时刷新。
  • CAN_UNDO_COMMAND/CAN_REDO_COMMAND由 HistoryPlugin 派发,驱动 Undo/Redo 按钮的disabled状态。

按钮的点击动作全部通过editor.dispatchCommand触发 Lexical 内置命令,形成"UI 只发命令、核心处理命令"的松耦合结构:

功能派发命令载荷
撤销UNDO_COMMANDundefined
重做REDO_COMMANDundefined
粗体FORMAT_TEXT_COMMAND'bold'
斜体FORMAT_TEXT_COMMAND'italic'
下划线FORMAT_TEXT_COMMAND'underline'
删除线FORMAT_TEXT_COMMAND'strikethrough'
左对齐FORMAT_ELEMENT_COMMAND'left'
居中FORMAT_ELEMENT_COMMAND'center'
右对齐FORMAT_ELEMENT_COMMAND'right'
两端对齐FORMAT_ELEMENT_COMMAND'justify'

命令常量(如UNDO_COMMANDFORMAT_TEXT_COMMANDCOMMAND_PRIORITY_LOW)均直接导入自lexical包(见 examples/react-rich/src/plugins/ToolbarPlugin.tsx)。按钮的图标由src/icons目录下的 SVG 精灵配合 CSS class(如format bold)渲染,工具栏中段用<Divider />分隔撤销/重做区、文本格式区与对齐区。

调试利器:TreeViewPlugin

examples/react-rich/src/plugins/TreeViewPlugin.tsx 仅十几行,直接复用@lexical/react提供的TreeView组件:

export default function TreeViewPlugin(): JSX.Element { const [editor] = useLexicalComposerContext(); return ( <TreeView viewClassName="tree-view-output" treeTypeButtonClassName="debug-treetype-button" timeTravelPanelClassName="debug-timetravel-panel" timeTravelButtonClassName="debug-timetravel-button" timeTravelPanelSliderClassName="debug-timetravel-panel-slider" timeTravelPanelButtonClassName="debug-timetravel-panel-button" editor={editor} /> ); }

它在编辑区下方渲染一个实时更新的节点树(tree-view-output),并自带时间旅行(time travel)调试面板,可通过滑块回放到历史任意一步的编辑器状态。对于学习 Lexical 内部数据结构、排查自定义节点问题,这个插件是极佳的观察窗口,因此被很多官方示例复用。

小结:一个可复用的最小富文本模板

综合来看,react-rich示例演示了一条清晰的 Lexical 接入路径:用LexicalComposer+editorConfig(namespace/nodes/theme/onError)搭建编辑器内核,用RichTextPlugin提供富文本语义,用HistoryPlugin提供撤销重做,用AutoFocusPlugin处理焦点,再以命令(dispatchCommand)+ 监听(registerCommand)的方式扩展自己的工具栏;同时在 HTML 层通过html.export/html.import建立"导出干净语义、导入白名单样式"的双向转换管线。若要从这个最小示例继续深入,可参考仓库内lexical-playground的完整节点集(标题、列表、表格、代码块等),以及 packages/lexical-react/src/LexicalRichTextPlugin.tsx 中富文本命令的底层注册逻辑。

【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 9:27:11

SadTalker 安装与配置:5 步跑通音频驱动面部动画生成

SadTalker 安装与配置&#xff1a;5 步跑通音频驱动面部动画生成 【免费下载链接】SadTalker [CVPR 2023] SadTalker&#xff1a;Learning Realistic 3D Motion Coefficients for Stylized Audio-Driven Single Image Talking Face Animation 项目地址: https://gitcode.com/…

作者头像 李华
网站建设 2026/9/12 9:26:39

网文IP改编短剧AI工具选型:Runway与小云雀工作流深度对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 9:26:36

Java Swing管理系统源码解析与实战应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 9:26:16

Spring Boot+Vue火锅店管理系统开发实践

1. 项目背景与核心价值 在餐饮行业数字化转型浪潮中&#xff0c;火锅店因其独特的经营模式面临着特殊的管理挑战。传统纸质点单和人工统计的方式常导致高峰期服务效率低下、库存管理混乱、经营数据分析滞后等问题。这套基于Spring BootVue的火锅店管理系统&#xff0c;正是为解…

作者头像 李华
网站建设 2026/9/12 9:26:14

AI原生游戏开发实战:Godot 4 + MCP打造自动化编码闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华