Quill富文本编辑器的实践 - DevUI

Quill富文本编辑器的实践 - DevUI

2 年前 · 来自专栏 DevUI团队

DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导 沉浸 灵活 至简 的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠、取悦眼球的设计。如果你正在开发 ToB 工具类产品 ,DevUI 将是一个很不错的选择!



引言

富文本编辑器大概是最复杂、使用场景却极广的组件了。

可以说富文本编辑器让Web数据录入充满了无限的想象空间,如果只有文本框、下拉框这些纯文本的数据录入组件,那么Web的数据录入能力将极大地受限。我们将无法在网页上插入图片、视频这些富文本内容,更无法插入自定义的内容。

富文本编辑器让Web内容编辑变得更轻松、更高效,我们几乎可以在富文本编辑器中插入任何你想插入的内容,图片、视频、超链接、公式、代码块,都不在话下,甚至还可以插入表格、PPT、思维导图,甚至3D模型这种超复杂的自定义内容。

富文本编辑器的场景在Web上也是随处可见,写文章、写评论、意见反馈、录需求单,都需要使用到富文本。

本文结合DevUI团队在富文本组件中的实践,从使用场景、技术选型,再到对Quill的扩展,以及Quill的基本原理,跟大家分享Quill富文本编辑器的那些事儿。

本文主要由以下部分组成: 1. 富文本编辑器的使用场景 2. 技术选型 3. 我们为什么选择Quill 4. 如何扩展Quill 5. Quill基本原理

以下内容来自 Kagol 华为 HWEB 大前端技术分享会 上的演讲。

富文本编辑器的使用场景

  • 博客文章
  • Wiki词条
  • 工作项描述
  • 测试用例步骤
  • 反馈意见
  • 评论







技术选型

我们的需求: - 开源协议友好 - Angular框架或框架无关 - 灵活可扩展 - 支持插入/编辑表格和图片 - 插件丰富,生态好





选型分析

  • 首先排除官方不维护的 UEditor
  • 然后排除React框架专属的 Draft.js Slate
  • 接着排除开源协议不友好的 CKEditor
  • 由于我们的业务场景丰富,需要富文本插入/编辑表格的功能,所以还需要排除不支持表格的 Trix ,弱支持表格的 Etherpad Prosemirror ,以及表格功能收费的 TinyMCE
  • 最后只剩下 Quill wangEditor 两款编辑器可选, wangEditor 的扩展性和生态不如 Quill ,所以最终选择 Quill 作为富文本组件的基座

为什么选择Quill?

  • BSD协议,商业友好
  • 文档详细,上手快
  • API驱动,扩展性好
  • 插件丰富,生态好

文档详细

Document: quilljs.com/

介绍Quill的API:



介绍如何扩展Quill:



上手快

  • 安装Quill: npm i quill
  • 引入样式: @import 'quill/dist/quill.snow.css';
  • 引入Quill: import Quill from 'quill';
  • 初始化Quill: new Quill('#editor', { theme: 'snow' });

效果图:



API驱动,扩展性好





插件丰富,生态好



扩展Quill

插入标签

比如我想在编辑器里插入标签



上传附件

比如我想在编辑器里插入附件



插入表情

比如我想在编辑器中插入表情

类似语雀的评论: yuque.com/yuque/blog/sg



个性分割线

比如我想插入B站这种个性化的分割线



超链接卡片

比如我想插入知乎这样的超链接卡片



如何插入表情?

我们从如何插入表情入手,一起看看怎么在Quill中插入自定义的内容。

要在Quill中插入表情,只需要以下四步: - 第一步:自定义工具栏按钮 - 第二步:自定义Blot内容EmojiBlot - 第三步:在Quill注册EmojiBlot - 第四步:调用Quill的API插入表情

第一步:自定义工具栏按钮

const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    // 配置工具栏模块
    toolbar: {
      container: [ …, [ 'emoji' ] ], // 增加一个按钮
      handlers: {
        // 添加按钮的处理逻辑
        emoji() {
          console.log('插入表情');
});

给工具栏按钮增加图标

// 扩展Quill内置的icons配置
const icons = Quill.import('ui/icons');
icons.emoji = ‘<svg>…</svg>’; // 图标的svg可以从iconfont网站复制

效果如下:



工具栏上已经多了一个表情的按钮,并且能够响应鼠标点击事件,下一步就是要 编写插入表情的具体逻辑,这涉及到Quill的自定义内容相关的知识。

第二步:自定义Blot内容EmojiBlot

Quill中的Blot就是一个普通的ES6 Class,由于表情和图片的差别就在于:

Quill内置的图片格式不支持自定义宽高,而我们要插入的表情是需要特定的宽高的。

因此我们可以基于Quill内置的image格式来扩展。

emoji.ts

import Quill from 'quill';
const ImageBlot = Quill.import('formats/image');
// 扩展Quill内置的image格式
class EmojiBlot extends ImageBlot {
  static blotName = 'emoji'; // 定义自定义Blot的名字(必须全局唯一)
  static tagName = 'img'; // 自定义内容的标签名
  // 创建自定义内容的DOM节点
  static create(value): any {
    const node = super.create(value);
    node.setAttribute('src', ImageBlot.sanitize(value.url));
    if (value.width !== undefined) {
      node.setAttribute('width', value.width);
    if (value.height !== undefined) {
      node.setAttribute('height', value.height);
    return node;
  // 返回options数据
  static value(node): any {
    return {
      url: node.getAttribute('src'),
      width: node.getAttribute('width'),
      height: node.getAttribute('height')
export default EmojiBlot;

第三步:在Quill注册EmojiBlot

有了EmojiBlot,要将其插入Quill编辑器中,还需要将这个ES6类注册到Quill中。

import EmojiBlot from './formats/emoji';
Quill.register('formats/emoji', EmojiBlot);

第四步:调用Quill的API插入表情

EmojiBlot注册到Quill中之后,Quill就能认识它了,也就可以调用Quill的API将其插入到编辑器中。

emoji(): void {
  console.log(‘插入表情');
  // 获取当前光标位置
  const index = this.quill.getSelection().index;
  // 在当前光标处插入emoji(blotName)
  this.quill.insertEmbed(index, 'emoji', {
    url: 'assets/emoji/good.png',
    width: '64px',
},

效果图



Demo源码

源码链接: gitee.com/kagol/quill-d

也欢迎关注我们DevUI组件库的官网,了解更多有趣又实用的开源组件!

DevUI官网: https://devui.design

Quill基本原理

最后讲一讲Quill的基本原理。

基本原理

  • 使用Delta数据模型描述富文本内容及其变化,以保证行为的可预测
  • 通过Parchment对DOM进行抽象,以保证平台一致性
  • 通过Mutation Observe监听DOM节点的变化,将DOM的更改同步到Delta数据模型中



Quill如何表达编辑器内容?

Delta数据模型

通过Delta数据模型来描述富文本内容及其变化



Delta 是JSON的一个子集,只包含一个 ops 属性,它的值是一个对象数组,每个数组项代表对编辑器的一个操作(以编辑器初始状态为空为基准)。

{
  "ops": [
    { "insert": "Hello " },
    { "insert": "World", "attributes": { "bold": true } },
    { "insert": "\n" }
}

修改编辑器内容

比如我们把加粗的"World"改成红色的文字"World",这个动作用 Delta 描述如下:

{
  "ops": [
    { "retain": 6 },
    { "retain": 5, "attributes": { "color": "#ff0000" } }