ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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/utilsUI 层使用 React 19 与 react-dom 19构建工具链为 Vite 7 与 TypeScript 5.9详见 examples/react-rich/package.json。Vite 配置本身非常标准仅注册了 React 插件examples/react-rich/vite.config.tsimport 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 classNameeditor-container ToolbarPlugin / div classNameeditor-inner RichTextPlugin contentEditable{ ContentEditable classNameeditor-input aria-placeholder{placeholder} placeholder{ div classNameeditor-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-placeholder与placeholder同时声明无障碍占位文本与可视占位符占位文案为Enter some rich text...见 examples/react-rich/src/App.tsxErrorBoundary{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, };各字段含义namespaceReact.js Demo编辑器的命名空间标识用于区分同一页面上的多个编辑器实例。nodes声明参与编辑的节点类此处仅为ParagraphNode与TextNode这两个最基础的内置节点。相比lexical-playground这类完整演示会注册标题、列表、引用、代码等大量节点这正是最小富文本的体现——需要更多能力时只需在数组中加入对应节点类。themeExampleTheme负责把 Lexical 节点格式映射为 CSS class详见下文主题映射一节。onError错误处理回调示例直接抛出异常便于开发期暴露问题。html.export / html.importHTML 导出与导入的映射配置是本示例区别于其他示例的最大亮点下面展开讲解。导出映射剔除内联样式与 classexportMap的类型是DOMExportOutputMap它将ParagraphNode和TextNode都映射到removeStylesExportDOM处理器examples/react-rich/src/App.tsxconst 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]属性的子孙元素逐一移除class与style属性。注释中特别说明由于 TextNode 可能嵌套在i、b、strong等标签内部因此需要连同子孙元素一起清理。也就是说导出的 HTML 是语义干净的不携带任何内联样式与自定义 class。导入映射对粘贴内容做样式白名单与导出对应constructImportMap()构建一个DOMConversionMap其目的是包装TextNode.importDOM的所有默认导入器在导入粘贴/拖拽/HTML 序列化反解时额外解析并恢复允许的白名单样式examples/react-rich/src/App.tsx。实现要点有三先触发TextNode.getType()代码注释解释了原因——Lexical 采用$config()协议节点的静态方法包括importDOM在首次静态访问时才生成因此要先调用getType()确保TextNode.importDOM已被填充。逐个包装默认导入器遍历importDOMFn()返回的{tag: importer}表对每个 tag 的导入器包一层conversion在保留原转换结果的基础上通过getExtraStyles提取额外样式。通过forChild注入样式只有当原转换结果是forChild风格返回的output.node为 null、没有after时才注入注入方式是重写forChild在子节点转换结果上调用textNode.setStyle(textNode.getStyle() extraStyles)。getExtraStylesexamples/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.tsconst 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 : ; }可以看到字号只接受8px到72px之间的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-paragraph、editor-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_COMMANDbold斜体FORMAT_TEXT_COMMANDitalic下划线FORMAT_TEXT_COMMANDunderline删除线FORMAT_TEXT_COMMANDstrikethrough左对齐FORMAT_ELEMENT_COMMANDleft居中FORMAT_ELEMENT_COMMANDcenter右对齐FORMAT_ELEMENT_COMMANDright两端对齐FORMAT_ELEMENT_COMMANDjustify命令常量如UNDO_COMMAND、FORMAT_TEXT_COMMAND、COMMAND_PRIORITY_LOW均直接导入自lexical包见 examples/react-rich/src/plugins/ToolbarPlugin.tsx。按钮的图标由src/icons目录下的 SVG 精灵配合 CSS class如format bold渲染工具栏中段用Divider /分隔撤销/重做区、文本格式区与对齐区。调试利器TreeViewPluginexamples/react-rich/src/plugins/TreeViewPlugin.tsx 仅十几行直接复用lexical/react提供的TreeView组件export default function TreeViewPlugin(): JSX.Element { const [editor] useLexicalComposerContext(); return ( TreeView viewClassNametree-view-output treeTypeButtonClassNamedebug-treetype-button timeTravelPanelClassNamedebug-timetravel-panel timeTravelButtonClassNamedebug-timetravel-button timeTravelPanelSliderClassNamedebug-timetravel-panel-slider timeTravelPanelButtonClassNamedebug-timetravel-panel-button editor{editor} / ); }它在编辑区下方渲染一个实时更新的节点树tree-view-output并自带时间旅行time travel调试面板可通过滑块回放到历史任意一步的编辑器状态。对于学习 Lexical 内部数据结构、排查自定义节点问题这个插件是极佳的观察窗口因此被很多官方示例复用。小结一个可复用的最小富文本模板综合来看react-rich示例演示了一条清晰的 Lexical 接入路径用LexicalComposereditorConfignamespace/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),仅供参考
返回列表