Lexical 富文本编辑器图片处理实战:上传、裁剪与多端预览的 5 个关键决策
【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical
手机上图片糊、桌面端首屏加载慢、裁剪全靠用户手动拖框。这三个体验问题,是 Lexical 富文本编辑器图片处理里最常见的抱怨,而且它们都不是单点问题,是链路问题。这篇文章以官方 playground 的图片扩展为参照,把「入口 → 预处理 → 渲染」整条链路拆开讲。读完你能把三种上传入口收敛成一条统一管线,搭好入库前的整形处理,并让同一张图在多端都显示正确。
先做选择:3 个自检问题定路线
别急着写上传代码。先回答三个问题,路线就定了:
- 有没有存储后端?没有。只做本地预览。那就用
URL.createObjectURL或 FileReader 把 File 转成临时地址,直接放进节点的 src。 有。就需要「先落库、再换 URL」的两段式流程,节点里永远不出现临时地址。 - 文档要不要持久化?要序列化进 JSON 或 HTML 存库的话,src 必须是稳定地址。object URL、base64 这类临时形态,不能留在节点里。 只做会话内编辑的话,临时地址可以接受,但要预留刷新后补位的逻辑。
- 入口要留几个?面向普通内容创作者,拖拽、点击、粘贴三个入口都保留。嵌入场景(聊天框、表单)留一个粘贴入口就够了,入口越少,测试面越小。
💡 这三个问题其实分别决定了三层代码:存储策略、节点数据结构、入口事件监听。后文三节正好对应入口、预处理、渲染,按顺序做即可。
让图片进编辑器:拖拽、点击、粘贴如何统一成一条管线
最常见的错误写法是三种入口各写一套上传逻辑:拖拽一套、文件选择一套、剪贴板粘贴一套。三处代码的校验、错误处理、进度提示各不相同,修一个 bug 要改三个地方。
正确做法是把三种入口收敛到一条管线。每个入口只干两件事:取出 File 列表、按 MIME 过滤。然后统一交给同一个上传函数。
步骤如下:
- 拖拽入口:在 drop 事件里读
event.dataTransfer.files - 点击入口:在文件选择框的 change 事件里读
input.files - 粘贴入口:在编辑器的 paste 事件里读
event.clipboardData.files - 管线统一负责:预览、插节点、后续上传
下面这段代码是管线尾部。注意它先用临时 object URL 把节点插进去,用户立刻能看到图,不用等上传完成:
const ACCEPTED = ['image/png', 'image/jpeg', 'image/webp']; function pickImageFiles(e: DragEvent): File[] { const list = e.dataTransfer?.files ?? []; return [...list].filter(f => ACCEPTED.includes(f.type)); } function queueUpload(files: File[], editor: LexicalEditor) { for (const file of files) { editor.update(() => { const node = $createImageNode({src: URL.createObjectURL(file), altText: file.name}); $insertNodes([node]); }); } }官方实现可以参考图片扩展插件。它额外处理了「移动已插入图片」的场景:内部节点拖动走 DRAGSTART、DRAGOVER、DROP 三个命令注册,把节点从旧位置移除再插入新位置,而不是重新上传。这个区分值得借鉴:外部文件拖入等于上传,内部节点拖动等于移动,两者走不同分支。
入库前"整形":裁剪、压缩、临时预览的 3 步预处理
用户选完的文件,不要原样上传。顺序是固定的:校验 → 裁剪 → 压缩 → 上传 → 换 URL。
为什么强调「先处理再落库」?上传发生在网络上,网络越慢等待越长。如果先传原图、服务端再处理,用户要等两次。前端先把裁剪和压缩做完,上传的就是最终产物,省掉一次往返,也减轻存储压力。
三步拆开看:
- 校验:拒绝非图片类型。文件体积设上限,8MB 是常见阈值。
- 裁剪:弹窗里让用户框选区域,确认后把该区域画到 canvas 上。裁剪框交互可以选现成的裁剪库,不用自己写拖框逻辑。
- 压缩:长边限制到 1600px,用
canvas.toBlob重编码为 WebP,质量参数取 0.8。
下面是压缩这一步。注意产物是 Blob 而不是字符串,Blob 可以直接作为 FormData 的附件上传:
async function reshapeBeforeUpload(file: File): Promise<Blob> { const bitmap = await createImageBitmap(file); const canvas = document.createElement('canvas'); canvas.width = Math.min(bitmap.width, 1600); canvas.height = (canvas.width * bitmap.height) / bitmap.width; const ctx = canvas.getContext('2d')!; ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height); return new Promise(resolve => canvas.toBlob(b => resolve(b!), 'image/webp', 0.8), ); }💡 这一步执行完,节点指向的还是本地 object URL。等上传成功拿到 CDN 地址,再调用节点的 setSrc 换成正式地址。整个过程对用户是无感的,图先出现、地址后补齐。
只做临时预览、不上传的场景(比如 playground),可以用 FileReader 转 base64 直接存节点。但持久化场景别这么做,文档体积会线性膨胀,后面踩坑清单里会讲。
让同一张图在任何设备都对:响应式加载与懒加载
插入的图片在桌面端很好看,切到手机上就溢出容器或者加载 4K 原图。问题不在图片本身,在三层配置没做齐。
第一层,CSS 约束。节点容器和图片上各一条规则就够:max-width: 100%; height: auto;。这一步保证图片随容器收缩且比例不变。官方 ImageNode 还有一个maxWidth字段,用来在桌面端给图片设宽度上限,避免大图撑满整屏。
第二层,按需换分辨率。如果图片服务支持多档尺寸,用srcset声明候选,或者用 ResizeObserver 监听容器宽度,跨过断点时切换 src。手机取小图,桌面取大图,传输量跟着视口走,而不是跟着原图走。
第三层,懒加载。长文档里十几张图,首屏只加载进入视口的那几张。用 IntersectionObserver 观察占位元素,isIntersecting为 true 时把data-src挪到src,然后立即 disconnect。这个装饰器组件放在 React 装饰器节点里渲染:
function LazyImage({src, alt}: {src: string; alt: string}) { const ref = useRef<HTMLImageElement>(null); useEffect(() => { const io = new IntersectionObserver(es => { if (es[0].isIntersecting) { ref.current!.src = ref.current!.dataset.src!; io.disconnect(); } }); io.observe(ref.current!); return () => io.disconnect(); }, []); return <img ref={ref} />协同编辑。用 Yjs 绑定 把编辑器接到 Y.XmlFragment 上,图片节点的每个字段都参与 CRDT 同步。关键只有一条:src 的每次变更,无论是临时预览还是最终换 URL,都必须走同一个更新通道。这样远端不会看到半加载状态。再配合节点 transform 给远程变更打个标记,UI 上可以渲染一个角标,提示「这张图刚被别人改过」。
收尾:一张可对照执行的速查表
阶段 动作 关键点 入口 拖拽、点击、粘贴收敛到一条管线 按 MIME 过滤,别写三套逻辑 预处理 先裁剪再压缩,然后上传 长边限 1600px,产物用 Blob 插入 本地临时预览先行,上传成功后换 URL dragover 记得 preventDefault 渲染 max-width: 100% 加懒加载 进视口才发起请求 持久化 src 只存稳定地址 object URL、base64 不留档
延伸阅读:
- 图片扩展插件:入口命令与内部拖动的完整实现
- ImageNode:节点字段、exportDOM 与序列化
- 文件导入导出模块:文档序列化工具
- Yjs 协同绑定:Bindings 实现
【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
项目地址: https://gitcode.com/GitHub_Trending/le/lexical
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考