Editor.js富文本编辑器实战踩坑与深度优化经验分享
为什么选 Editor.js?
上个月接了个内容管理系统的需求,客户要一个能自由排版、支持图文混排、还能导出结构化数据的编辑器。一开始我试了 Quill,结果发现它输出的是 HTML,后续处理起来特别麻烦;又看了 Draft.js,学习曲线太陡,项目时间紧根本没空折腾。最后在 GitHub 上翻到 Editor.js,一眼看中它的 JSON 输出格式——每个 block 都是独立对象,类型、数据、配置一清二楚,后端解析起来贼方便。而且插件生态也还行,基础功能都能覆盖,就它了。
快速集成,但别高兴太早
Editor.js 的初始化其实挺简单的,官方文档写得也算清楚。装包、引入、new 一个 Editor 实例,几行代码搞定:
import EditorJS from '@editorjs/editorjs';
import Header from '@editorjs/header';
import Paragraph from '@editorjs/paragraph';
import ImageTool from '@editorjs/image';
const editor = new EditorJS({
holder: 'editorjs',
tools: {
header: Header,
paragraph: Paragraph,
image: {
class: ImageTool,
config: {
endpoints: {
byFile: 'https://jztheme.com/api/upload',
byUrl: 'https://jztheme.com/api/fetch',
}
}
}
},
data: {} // 初始内容
});
跑起来没问题,能打字、加标题、传图,看起来一切顺利。但问题很快就来了——性能。
最大的坑:长内容卡成PPT
当用户写了几十个 block(比如 50+ 段落+图片)之后,页面直接卡住。滚动都掉帧,更别说编辑了。我一开始以为是 React 渲染的问题,后来用 Performance 面板一看,发现每次输入都会触发整个 editor 的 re-render,而且每个 block 都是独立 DOM 节点,数量一多,浏览器直接吃不消。
折腾了半天,发现 Editor.js 本身没有虚拟滚动,所有 block 都是真实渲染的。官方 issue 里也有人提过,但作者说“这是设计取舍,为了保持 DOM 结构清晰”。行吧,那只能自己优化。
我试了两种方案:
- 第一种:限制最大 block 数量,比如最多 30 个,超出就提示“内容过长建议分页”。但客户不接受,说他们就是要写长文。
- 第二种:给编辑器容器加
overflow: hidden,配合 Intersection Observer 只渲染可视区域内的 block。但 Editor.js 的 block 是动态生成的,DOM 结构嵌套深,observer 写起来特别麻烦,而且容易破坏编辑焦点。
最后折中了一下:在非编辑状态下(比如预览模式),把 editor 实例 destroy 掉,只渲染静态 HTML。编辑时再 init。这样虽然切换有点闪,但至少保证了编辑时的流畅度。代码大概这样:
// 编辑模式
const enterEditMode = () => {
if (!editorInstance) {
editorInstance = new EditorJS({ /* ... */ });
}
};
// 预览模式
const enterPreviewMode = async () => {
if (editorInstance) {
const savedData = await editorInstance.save();
editorInstance.destroy();
editorInstance = null;
renderStaticHTML(savedData); // 自己写的函数,把 JSON 转成 HTML
}
};
这个方案不完美,但简单有效,客户也没再抱怨卡顿。
图片上传的坑:跨域和进度条
Editor.js 的 image 工具默认只支持返回 { success: 1, file: { url: '...' } } 这种格式。但我们的后端接口是标准 RESTful,返回的是 { url: '...' },而且有 CORS 限制。
开始我以为改个 endpoint 就行,结果发现它根本不支持自定义 response parser。翻了源码,发现得自己写一个 image 工具的 wrapper。于是重写了 upload 方法:
class CustomImageTool {
static get isReadOnly() { return false; }
constructor({ data, config, api }) {
this.api = api;
this.data = data || {};
this.config = config;
}
async uploadSelectedFile(file) {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('https://jztheme.com/api/upload', {
method: 'POST',
body: formData,
credentials: 'include' // 处理 cookie 跨域
});
const json = await res.json();
// 后端返回 { url: 'xxx' },这里要转成 editor.js 要的格式
return { success: 1, file: { url: json.url } };
}
render() {
// 省略渲染逻辑
}
save(blockContent) {
return { url: this.data.url };
}
}
另外,原生 image 工具没有上传进度条,用户传大图时完全不知道卡在哪。我加了个临时 loading 提示,虽然简陋,但至少比干等强:
// 在 uploadSelectedFile 开头
this.api.tooltip.show('上传中...');
// 成功或失败后
this.api.tooltip.hide();
导出与兼容性:JSON 不是万能的
Editor.js 的核心优势是输出结构化 JSON,但客户后期又要求“导出 Word”和“复制到微信公众号”。这就尴尬了——微信公众号只认富文本,Word 也得是 HTML 或 docx。
我写了个转换器,把 JSON 转成 HTML,再用第三方库(比如 html-docx)生成 Word。但转换过程有很多细节问题:比如 header 的 level 映射、图片的 alt 属性、列表的缩进……搞了两天才勉强对齐样式。
最头疼的是,有些 block 插件(比如我们自己写的代码高亮块)在转换时容易丢样式。后来干脆在导出前加了个校验,如果检测到非标准 block,就弹窗提醒“部分格式可能无法导出”。
回顾与反思
整体来说,Editor.js 适合需要结构化内容的场景,比如 CMS、知识库、问卷系统。如果你只是要一个富文本编辑器,Quill 或 TinyMCE 可能更省事。
做得好的地方:
- JSON 数据结构清晰,前后端协作顺畅
- 插件机制灵活,自定义 block 不难
- 社区插件基本够用(除了表格有点弱)
还能优化的点:
- 性能问题没彻底解决,长内容还是卡
- 移动端体验一般,光标定位偶尔抽风
- 撤销/重做(undo/redo)功能要自己集成,官方只提供基础 API
现在项目上线了,日常使用没啥大问题。虽然有几个小 bug(比如快速连续删除 block 会报错),但频率低,暂时没动它。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的性能优化方案,或者处理过复杂的 block 转换,欢迎评论区交流!

暂无评论