码农之家

editor.js 富文本源码解析:paragraph 段落渲染流程

前言

Editor.js 是一个 Block-Styled 块编辑器,以 JSON 格式输出数据的富文本和媒体编辑器。它,由“块”组成,完全模块化,例如段落、标题、图片都是块,可以进一步地编写自己的插件来扩展编辑器功能。

下面介绍段落在富文本编辑器的渲染流程,了解 editor.js 是如何工作的,知道自己编写的插件是怎么运行的。

开始使用

阅读 editor.js 官网文档,写的挺清晰,使用段落插件作为简单的示例,调试 editor.js 的工作过程

使用挺简单,实例化 EditorJS, 数据内容使用 data 存储,遍历 blocks 数组生成 DOM 内容,挂载在 holder id 上

import EditorJS from '@editorjs/editorjs';

const editor = new EditorJS({
  holder: 'editorjs',
  data: {
    blocks: [{
      type: "paragraph",
      data: {
        text: "Hello world"
      }
    }]
  }
});

Editorjs 核心类

实例化 EditorJS 内部调用 Core 实例化富文本核心类

在 Core 构造器 constructor 执行 Promise.resolve,从这里可以了解到 editor.js 采用的是异步渲染的方式

好处是:提升页面渲染性能,数据量大达到上千行万行,不会阻塞页面的渲染

需要注意点,在同步执行过程中,拿不到渲染的DOM,需要在异步队列执行后访问,也就是 onReady 异步函数后

在异步函数,执行流程分为以下4步:

  1. validate:校验挂载的 DOM 是否合法
  2. init:注册内置模块,管理模块
  3. start:模块 prepare 方法,做一些准备工作
  4. render:渲染编辑器内容
export default class Core {
  constructor(config) {
  
    Promise.resolve()
      .then(async () => {
        this.configuration = config;

        this.validate();
        this.init();
        await this.start();
        await this.render();

        onReady();
      })
      .catch((error) => {
        _.log(`Editor.js is not ready because of ${error}`, 'error');

        /**
         * Reject this.isReady promise
         */
        onFail(error);
      });
  }
}

渲染流程

注册内置模块

执行 init 方法,主要做两件事件;

  • 加载内置模块实例化
  • 配置模块,实现模块间通信
public init(): void {
  // 加载模块实例化
  this.constructModules();
  // 配置模块
  this.configureModules();
}

1、constructModules 方法加载内置的模块,遍历模块 new 实例化,添加到 this.moduleInstances 对象,moduleInstances 管理所有模块方法 API

2、configureModules 配置模块,实现不同模块间的数据通信

比如说,要在 BlockManager 模块调用 EventsAPI 模块的实例化方法,做法是在当前模块添加其他所有的模块到 Editor 对象上


private configureModules(): void {
  // 遍历 moduleInstances ,通过 getModulesDiff diff 对比,赋值其他模块的对象给 state 
  for (const name in this.moduleInstances) {
    if (Object.prototype.hasOwnProperty.call(this.moduleInstances, name)) {
      this.moduleInstances[name].state = this.getModulesDiff(name);
    }
  }
}
// 对比获取非当前模块对象
private getModulesDiff(name: string): EditorModules {
  const diff = {};
  for (const moduleName in this.moduleInstances) {
    if (moduleName === name) {
      continue;
    }
    diff[moduleName] = this.moduleInstances[moduleName];
  }

  return diff;
}

所有的模块都继承于 Module 模块,它内部有个方法,设置 state 会添加模块对象在 Editor 属性上,这样 Editor 就存储了其他模块的数据

public set state(Editor: EditorModules) {
  this.Editor = Editor;
}

start 启动模块

有些模块需要做一些准备工作,比如 tools 行内工具 UI DOM 创建 ,创建编辑器的容器 DOM,需要提前执行,所以将这些模块列出来按照顺序执行。

使用 reduceasync/await 控制顺序执行 prepare 方法

render 渲染内容

渲染内容使用实例化模块 Rendererrender 方法渲染

渲染流程

1、 遍历 blocksData 数组,调用 BlockManager 模块 composeBlock 方法,它会为段落实例化一个 Block

段落 Block 实例化内部调用 tool.create 方法,此时就开始执行 段落插件constructor 实例化逻辑,实例化对象添加到 toolInstance 属性

2、段落 DOM 渲染

段落插件必须提供一个 render 函数,在实例化 Block 会调用 compose 函数创建 DOM 赋值 holder

同时也会创建 tunes 转换模块,如上下移动、删除块

3、段落绑定事件

composeBlock 创建完 dom 后,会给 dom 绑定事件,这里使用了 requestIdleCallback API 在 2 秒后浏览器空闲时间执行

挂载 DOM

初始化所有的 blocks 后,使用 BlockManager 来管理,执行 BlockManager.insertMany(blocks) ,遍历 blocks 取出段落的 DOM(在 holder) 添加到文档片段,挂载到页面上

保存校验 json

editor.js 提供 save 方法获取编辑器的 JSON 数据

1、调用 Saver 模块的 save 方法

save 方法会遍历 blocks 所有的块,调用 getSavedData 获取块的数据,添加到 chainData 数组,getSavedData 方法处理保存和校验的逻辑

2、保存逻辑

save 执行段落 block save 方法,调用段落插件的 save 方法,获取段落的数据

3、validate 校验

判断段落插件是否有定义 validate 方法,有则将 data 数据进行校验,同时 sanitizeBlocks 方法根据 sanitizeConfig 配置对数据做一层过滤

输出数据

最后调用 makeOutput ,根据校验条件 isValid 如果通过则添加到 blocks 数组返回

makeOutput 输出数据对象

总结

editor.js 是一个 DOM 操作的富文本编辑器,每一个块是 contenteditable 编辑区域,用一个 Block 块定义表示,使用 BlockManager 模块管理所有的 Block 块。工作过程,会先加载内置模块,对一些模块执行 prepare 准备工作,调用 render 遍历 Block 渲染。

输出 JSON 数据,遍历所有的 Block,调用插件提供的 save 方法输出 data 对象

原文链接:https://juejin.cn/post/7327926127396438035 作者:网页建筑师