Editor.js富文本编辑器实战踩坑与深度优化经验分享

司空俊郝 组件 阅读 2,635
赞 23 收藏
二维码
手机扫码查看
反馈

为什么选 Editor.js?

上个月接了个内容管理系统的需求,客户要一个能自由排版、支持图文混排、还能导出结构化数据的编辑器。一开始我试了 Quill,结果发现它输出的是 HTML,后续处理起来特别麻烦;又看了 Draft.js,学习曲线太陡,项目时间紧根本没空折腾。最后在 GitHub 上翻到 Editor.js,一眼看中它的 JSON 输出格式——每个 block 都是独立对象,类型、数据、配置一清二楚,后端解析起来贼方便。而且插件生态也还行,基础功能都能覆盖,就它了。

Editor.js富文本编辑器实战踩坑与深度优化经验分享

快速集成,但别高兴太早

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 转换,欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论