用 Lexical 搭一套富文本图片管线:上传、预览与持久化全走通 📅 发布时间:2026/9/5 21:51:48 👁 浏览次数: 用 Lexical 搭一套富文本图片管线上传、预览与持久化全走通【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical富文本编辑器图片上传、预览、持久化与协同Lexical 的图片处理并不是一套开箱即用的功能包而是四个需要自己接上的工程问题。本文沿着一张图片节点在编辑器里的生命周期——从 File 到序列化 JSON——讲清每个环节该用哪个机制给你一套可以直接搬进内容管理系统的实现路径。能力全景Lexical 图片处理的四个环节各在哪先明确一件事Lexical 只负责节点、命令和序列化这些机制上传一张图本身要靠你在插件层组装。把四个能力拆开看各自的位置是环节落在哪一层依赖的核心机制解决的问题上传自研插件工具栏按钮 / 拖放区装饰器节点 命令系统把本地 File 变成文档里的节点预览装饰器节点渲染的 React 组件节点更新 React 渲染图片显示、加载占位、断图兜底持久化Lexical 序列化 文件导入导出模块exportJSON / importJSON文档可保存、可重载、图片不丢协同lexical/yjs 包Yjs CRDT 节点字段同步多人编辑同一文档图片变更不冲突官方 Playground 里的图片就是按这个思路实现的ImageNode 是一个自己写的装饰器节点而不是内置功能。下面的三个章节按一张图从进入文档到离开浏览器的顺序展开。第一站从本地文件到 ImageNode 节点这一节解决用户选中或拖入的图片数据最终去了哪。装饰器节点DecoratorNode是 Lexical 挂载自定义 UI 的标准机制节点本身只定义数据和一个占位 DOM真正的图片和交互由 React 组件负责渲染。Playground 的 ImageNode 继承 DecoratorNode数据只保留三个字段——src、altText、width这也是后续序列化和协同同步的边界字段越少越好维护class ImageNode extends DecoratorNodeJSX.Element { __src ; __altText ; __width: number | null null; static getType() { return image; } exportJSON(): SerializedImageNode { return { type: image, version: 1, src: this.__src, altText: this.__altText, width: this.__width, }; } createDOM(): HTMLElement { // 只建一个占位容器真正的 img 交给 React 组件渲染 const container document.createElement(div); container.style.maxWidth this.__width ?? 100%; return container; } }importJSON 与 exportJSON 成对出现是文档重新加载后节点能被完整还原的前提。这三个方法的完整定义可以直接对照 Playground 的 ImageNode 源码。上传入口通常有两个工具栏按钮和拖放区。推荐让两者都收敛到一个自定义命令上这样从哪进来的图和怎么插入节点彻底解耦const INSERT_IMAGE_COMMAND createCommandImagePayload(INSERT_IMAGE); editor.registerCommand( INSERT_IMAGE_COMMAND, (payload) { editor.update(() { $getRoot() .selectEnd() .insertNodes([ImageNode.fromPayload(payload)]); }); return true; }, COMMAND_PRIORITY_EDITOR, );自定义命令用 createCommand 创建注册和监听的方式与内置命令完全一致整个命令体系可以在 Lexical 的命令定义文件 里查到模式参考。第二站预览组件如何渲染图片这一节解决节点进入文档后图片怎么又快又稳地显示出来。装饰器节点渲染的是一个 React 组件Playground 的 ImageComponent 在这里做了两件值得抄的事。第一用 Suspense 处理加载图片就绪前组件抛出一个 Promise界面上先出现占位加载完成后自动换成真图不需要你手写 loading 状态机。第二用模块级 Map 缓存已加载图片的尺寸同一张图在文档里被引用多次时只读一次网络const imageCache new Mapstring, ImageStatus(); function useSuspenseImage(src: string): ImageStatus { let status imageCache.get(src); if (!status) { status new Promise((resolve) { const img new Image(); img.onload () resolve({ error: false, width: img.naturalWidth, height: img.naturalHeight, }); img.onerror () resolve({error: true, width: 0, height: 0}); img.src src; }); imageCache.set(src, status); } return use(status); // React 的数据钩子未就绪时自动挂起 } 当文档里图片超过 20 张时这类尺寸缓存的收益很明显首帧布局不再依赖每张图的 onLoad编辑器滚动位置也不会被图片加载挤得跳动。按容器宽度切换分辨率是同一思路的延伸对装饰器容器挂一个 ResizeObserver宽度变化时重新选取 src。注意 width 应作为节点字段保存而不是只写在 DOM 上——节点字段的变化会自然进入后续的序列化与协同通道DOM 上的变化不会。第三站持久化与协同节点序列化数据的一生这一节解决页面关闭之后图片去哪了。一张图在系统里的完整数据流如下持久化分两层。节点层就是上面的 exportJSON / importJSON文档层则把整个编辑器状态打包成一份 JSON 文档交给后端存储或直接导出为 .lexical 文件。文件导入导出模块 提供了这层封装核心就是一个序列化函数import {serializedDocumentFromEditorState} from lexical/file; const doc serializedDocumentFromEditorState( editor.getEditorState(), ); // 把 doc 存到后端重新加载时解析回来再 setEditorState多人协同场景下图片节点不需要任何特殊待遇lexical/yjs 把 Lexical 文档绑定到 Yjs 的 XmlFragment 上src 这类字段的每次修改都作为普通 CRDT 更新在各方之间收敛图片 URL 的替换、尺寸调整天然走同一条同步通道。调试时建议装上 DevTools图片节点在文档树里是独立的装饰器节点可以展开看到它的完整 JSON 字段排查图为什么没加载出来时先从这里看节点数据而不是猜网络。最小可用示例选文件、显示、保存三步串起来这一节把前面三站拼成一个能跑的最小闭环只保留关键代码样式与错误处理从略。第一步节点定义与命令注册即前两节的 ImageNode 和 INSERT_IMAGE_COMMAND。第二步选文件入口——本地预览可以先用 Object URL但插入节点时必须是上传后的稳定地址function handlePick(e: React.ChangeEventHTMLInputElement) { const file e.target.files?.[0]; if (!file || !file.type.startsWith(image/)) return; const previewUrl URL.createObjectURL(file); // 仅本地预览用 onUpload(file).then((cdnUrl) { URL.revokeObjectURL(previewUrl); editor.dispatchCommand(INSERT_IMAGE_COMMAND, { src: cdnUrl, // 进入节点的是稳定 URL不是 objectUrl altText: file.name, }); }); }第三步预览渲染用第二节的 ImageComponent第四步保存时调用 serializedDocumentFromEditorState 拿到文档 JSON 提交后端。整条链路没有任何一处需要直接操作 contentEditable这也是把图片功能做成 Lexical 插件而非 DOM 补丁的价值所在。避坑清单这些是上面环节里最容易翻车的点集中在这一节方便对照排查拖放上传时onDragOver里必须调用preventDefault()。否则浏览器会接管 drop 事件把文件当导航打开onDrop根本不会触发——这是拖放功能点了没反应的第一嫌疑。Object URL 用完要URL.revokeObjectURL释放且绝不能写入节点它只在当前会话有效页面一刷新就是满屏裂图。大图的 base64 不要塞进节点 JSON。一张 5MB 的图会把文档体积撑到数 MB之后每次协同同步、每次保存都带着它走base64 只适合纯本地临时预览。图片尺寸优先取naturalWidth/naturalHeight缓存下来做布局不要在渲染期反复读 DOM 属性否则布局会被加载时序牵动。MIME 类型校验不可尽信部分图片会报出错误的 MIME建议同时兜底判断扩展名且后端再做一次校验。写在最后上传、预览、持久化看似三件事本质是同一份数据在三个时刻的形态变化File、节点字段、JSON。抓住这条主线裁剪、浮动排版这类扩展都只是插入节点前多一步处理不会动摇已有链路。想继续深入可以看 Playground 中 ImageNode 的完整实现 和 lexical-yjs 包的 README。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考