DonaldxDonald

DonaldxDonald

twitter
github

Web 富文本編輯器演進

文章是 2022 年寫的,在這裡上傳一份。

什麼是富文本?#

富文本 Rich Text ,相對於純文本 Plain Text 來說,就是有通用的格式選項(比如加粗和斜體)來格式化的文本。

rich-text

Web + 富文本編輯器#

vs-code

眾所周知,Web 是目前來說最通用的平台,不管是電腦、手機、遊戲機、汽車還是 Kindle ,只要有瀏覽器就能打開網站。加上 WebAssembly 和 Electron 等技術和工具的支持,在 Web 上執行複雜操作的超級應用(Figma, VS Code)也在陸續登場。面對 “一次編寫,隨處運行” 的誘惑下,許多傳統桌面應用也都在逐步地將陣地拓展到 Web 端,富文本編輯器就是其中之一。

富文本編輯器 = 坑?#

在 Web 前端業界內,富文本編輯器是公認的天坑。Web 開發好處是跨平台,但問題也是因為跨平台帶來的兼容問題。跨平台只是能在各平台上跑起來,但是跑起來怎麼樣就是天坑所在了。

在 Web 開發編輯器首先要處理好 焦點光標選區撤回棧、從外部粘貼內容解析等等的坑,然後考慮兼容不同瀏覽器(Chrome、Firefox、Safari 等)...... 處理好了最基本的英文輸入後,以中文使用者為代表的用戶來要求支持 IME 組合輸入了...... 解決了 IME 輸入後,RTL 語言(希伯來語、阿拉伯語)用戶來了...... 手機用戶來了...... 協同編輯用戶來了......

借用知乎上的一段話,總結起來就是:

落後的生產力與人們日益增長的需求之間的矛盾。

落後生產力:

  1. Web 相關標準推進緩慢
  2. 瀏覽器廠商對於相同操作或者場景實現方式的不同,導致兼容性問題
  3. 使用 HTML DOM 描述富文本內容有太多不可控制的情況

日益增長的需求:

  1. 不確定的交互意圖,比如按 Delete 鍵,不同的焦點位置有不同的情況需要考慮
  2. 內容輸入的多樣性,比如有:打字鍵入、粘貼、拖拽等,每個處理起來都相當複雜
  3. 大量需要攔截阻止和代理的瀏覽器默認行為,保證數據的完整性和正確性
  4. 用戶對於編輯器的使用要求越來越高,比如:合併單元格、列表多級嵌套、協同編輯、版本對比、段落標註,大家都認為這是基本需求,其實這裡面的技術難度是超出大家的想像的。

編輯器技術階段一覽#

階段描述典型產品
L0
  1. 強依賴瀏覽器 DOM API (contenteditable, document.execCommand)
  2. 視圖即數據
  1. UEditor
  2. TinyMCE
  3. CKEditor 1 ~ 4
L1
  1. 仍然基於 contenteditable
  2. 拋棄 document.execCommand 操作內容,改為自己實現
  3. 有抽象的數據模型來描述富文本編輯器的內容與狀態
  1. Quill
  2. Slate
  3. CKEditor 5
  4. Draft.js
  5. ProseMirror
  6. wangEditor v5
L2
  1. 拋棄 contenteditable ,改為自己實現
  2. 拋棄 document.execCommand 操作內容,改為自己實現
  3. 自己實現排版引擎
  1. Google Docs

L0#

L0 階段的編輯器主要是依賴了瀏覽器原生的 contenteditable API 來實現編輯,以 document.execCommand API 來實現多種操作,比如加粗、綁定鏈接、複製粘貼等等。

優勢:

  1. 技術門檻低。只要使用了以上兩個 API ,就可以讓網頁具備編輯能力。
  2. 基於瀏覽器原生編輯能力,輸入非常流暢。
  3. 沒有令人頭疼的組合輸入問題。

劣勢:

  1. 相同操作在不同瀏覽器上會有不同實現。
  2. 輸出富文本內容是 HTML ,不利於管理數據。
  3. 擴展複雜的富文本很困難。
  4. 沒有辦法實現協同編輯。

L1#

目前大多數編輯器框架都處於 L1 階段,比較有代表性的就是 Quill、Slate、ProseMirror 和 Draft.js。它們主要有兩個明顯的特點:

  1. 仍然依賴於 contenteditable API 用於內容編輯,但不再依賴 document.execCommand API 來操作內容,改為自己實現。
  2. 有抽象的數據模型來描述富文本編輯器的內容與狀態。

2012 - Quill#

quill

Quill 是 API 驅動的富文本編輯器框架,提供開箱即用的編輯器體驗。

Quill 的作者 Jason Chen 是一名華裔,Quill 其實算是 Jason 的一個 Side Project,當初是創辦了一家公司,專門做類似 Google Docs 的協作編輯器,因此自己寫了 Quill 出來使用。

Quill 對 DOM Tree 以及數據的修改操作進行了抽象,從而實際使用時不需要我們對 DOM 操作,而是通過 Quill 的 API 進行操作,對應的關係如下:

Editor Document ====> Parchment

DOM Node ====> Blot

有了這層抽象後,原本的對 DOM 的直接操作就變成了對 Blot 的操作,這些操作用 Delta 來表示。Quill 在 Delta 中拋棄了 DOM 的節點樹的層次,因此完全看不出包裹文字的標籤和節點關係,只有一個扁平化後的數組 ops

數據模型:

img

{
  "ops": [
    {
      "attributes": {
        "bold": true
      },
      "insert": "Check"
    },
    {
      "insert": " "
    },
    {
      "attributes": {
        "link": "https://donaldxdonald.xyz/"
      },
      "insert": "this"
    },
    {
      "insert": " out ~"
    }
  ]
}

Delta 的扁平化結構其實是協同編輯中的 OT 模型的一種實現,因此 Quill 也是生來就是為了協同編輯而設計的。扁平化帶來的好處是對性能提升有幫助,弊端則是在表示一些複雜的嵌套內容時會比較吃力。

Quill 的特點是:

  1. 依賴瀏覽器原生編輯能力 contentEditable (L1)
  2. 引入了一層抽象的數據結構用以描述內容以及行為
  3. 對協同編輯支持良好
  4. 輸出結構可以是字符串也可以是 Delta (JSON),但 Delta 作為數據模型可讀性不高

2015 - ProseMirror#

prosemirror

Marijn 是 CodeMirror 編輯器和 acorn 解析器的作者,前者已經在 Chrome 和 Firefox 自帶的調試工具裡使用了,後者則是 babel 的依賴。為了有更多的收入,Marijn 開始了新的項目,ProseMirror 。

Marijn 覺得當時市面上的開源編輯器都沒有一個採用他認為是理想的方法,且很多還是使用著舊的範式來設計,使用著 contentEditable來實現。這樣子開發者對文檔內容能控制的範圍就很小,而這又是很容易被用戶和瀏覽器修改的。雖然 ProseMirror 還是基於 contentEditable 實現編輯功能了,畢竟自己重新實現一套選區邏輯太麻煩了。

ProseMirror 是有 schema (範式)的,所以定義好了 schema 以後 ProseMirror 可以替你實現自動化 parser 。框架層面定義好了新引入一個 Node 需要什麼屬性和方法,比如 nodeFromJSON 方法做結構到 json 的轉換,toDOM 方法定義如何將結構數據轉換為 DOM(有點類似 JSX )。ProseMirror 就在中間這一層做了 json 數據到 DOM 的變更管理。

數據模型:

img

{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "marks": [
        {
          "type": "strong"
        }
      ],
      "text": "Check"
    },
    {
      "type": "text",
      "text": " "
    },
    {
      "type": "text",
      "marks": [
        {
          "type": "link",
          "attrs": {
            "href": "https://donaldxdonald.xyz/",
            "title": ""
          }
        }
      ],
      "text": "this"
    },
    {
      "type": "text",
      "text": " out ~"
    }
  ]
}

ProseMirror 的特點是:

  1. 依賴瀏覽器原生編輯能力 contentEditable (L1)
  2. 更抽象的 json 文檔模型。ProseMirror 只定義了可配置的模型框架,具體的結構可以在實際開發的時候自定義。
  3. 嵌套的樹形結構。能支持複雜結構的內容。
  4. 對協同編輯的良好支持。從誕生之初,ProseMirror 就開始關注著協同編輯的支持。
  5. 1.0 後加入了不可變數據,使得編輯器的數據處理有了一個完整的數據流,穩定且可控。

2016.02 - Draft.js#

draftjs

彼時還叫 Facebook 的 Meta 開源了 Draft.js ,既然都是同一個公司的,Draft.js 就在視圖層方面使用了 React 渲染 UI 。這也是第一個 React + 編輯器結合的案例,React 的流行也讓使用者可以快速地直接基於 Draft.js 進行二次開發。

Draft.js 不僅外表有 React ,內裡也是有很深的 React 的影子,類似 Redux 等狀態管理的 EditorStateContentState ,在數據層使用 Immutable等特性。JS 對象的屬性是可以隨意賦值的,也就是 mutable 可變的。而相對地,不可變的數據類型不允許隨意賦值,每次通過 Immutable API 的修改,都会生成一個新的引用。

Draft.js 的特點是:

  1. 依賴瀏覽器原生編輯能力 contentEditable (L1)
  2. 用 React 來實現視圖層
  3. 內容的存儲和渲染邏輯分離
  4. 使用 Immutable 數據
  5. 雖然也抽象了基於 json 的數據模型,但是對於嵌套數據的支持有些弱

2016.06 - Slate#

slate

此時市面上已有許多編輯器輪子在卷了,但是 Ian Storm Taylor 在開發自己的 CMS 產品時,仍然覺得沒有一個好用的編輯器,他覺得這些編輯器如果只是用來做一些簡單的產品的話,是挺不錯的了,但是如果想要開發像 Medium 、Google Docs 和 Dropbox Paper 這些大型應用的話,就太難太難了,於是就有了 Slate。

Slate 同樣是一個編輯器框架,而不是開箱即用的編輯器工具。作為晚輩的 Slate 集合了前輩們的許多優點,從 Draft.js 那裡參考的 Immutable 數據、插件機制和 React 視圖層,又從 ProseMirror 借鑒了嵌套數據結構和 Schema 約束規則。整合了許多編輯器框架的核心特性,又加上框架理念先進和作者對架構的追求(時至今日 2022 ,仍然是 beta 的狀態,還沒到 1.0),Slate 在社區上還是比較受歡迎的。

數據模型:

example-slate

{
  "object": "block",
  "type": "paragraph",
  "nodes": [
    {
      "object": "text",
      "text": "This is editable "
    },
    {
      "object": "text",
      "text": "rich",
      "marks": [{ "type": "bold" }]
    },
    {
      "object": "text",
      "text": " text, "
    },
    {
      "object": "text",
      "text": "much",
      "marks": [{ "type": "italic" }]
    },
    {
      "object": "text",
      "text": " better than a "
    },
    {
      "object": "text",
      "text": "<textarea>",
      "marks": [{ "type": "code" }]
    },
    {
      "object": "text",
      "text": "!"
    }
  ]
}

此時 Slate 的特點是:

  1. 依賴瀏覽器原生編輯能力 contentEditable (L1)
  2. 用 React 來實現視圖層
  3. 支持嵌套的 json 數據結構
  4. Immutable 數據
  5. 插件機制為核心
  6. 有約束數據的 Schema

2019 - Slate 0.50+#

Slate 在架構上進行了一個大更新,作者稱 “整個框架都從頭開始重新考慮了”,主要更新的點為:

  1. 將底層邏輯抽離出來 Slate Core ,與視圖層分離
  2. 用 TypeScript 重寫
  3. 簡化插件機制,插件不再與渲染邏輯耦合
  4. 用簡單的 json 對象替換 Immutable.js
  5. 自有概念和一些 Commands 更精簡更抽象,改名為 Transforms

數據模型:

example-slate

{
  "type": "paragraph",
  "children": [
    { "text": "This is editable " },
    { "text": "rich", "bold": true },
    { "text": " text, " },
    { "text": "much", "italic": true },
    { "text": " better than a " },
    { "text": "<textarea>", "code": true },
    { "text": "!" }
  ]
}

這時候 Slate 的特點是:

  1. 依賴瀏覽器原生編輯能力 contentEditable (L1)
  2. 非常簡潔的支持嵌套的數據模型
  3. 整體架構採用純函數 + 接口的方式,思路和代碼都非常簡潔
  4. 插件機制支持開發強大的功能
  5. 整體的設計理念與 DOM 很像

2022 - Lexical#

lexical

由於 Draft.js 在以往兼容瀏覽器方面做了很多的脏活,且這些是現在不太必要的,同時為了改善開發者體驗(很多開發者吐槽 Draft.js 不好用),Meta 又開源了一款新的編輯器框架 Lexical ,旨在替換 Draft.js。

目前看來暫時沒有什麼特別創新的概念,主要也還是吸收了其他編輯器框架的優點:

  1. 依賴瀏覽器原生編輯能力 contentEditable (L1)
  2. 保留了 Draft.js 中的一些概念(EditorState)
  3. 不與 React 綁定了,可以用各種框架實現視圖層
  4. 整個框架挺輕便的,幾乎沒有什麼其他的依賴

L2#

前面都有說到,憑藉瀏覽器的contenteditable API,可以快速地開發出一款編輯器出來,但是各種兼容問題太多了,別看現在大多數編輯器框架還在 L1 ,其實早在 2010 年,財力雄厚的 Google 就已經開始拋弃瀏覽器的 contenteditable 了,新版 Google Docs 基於 Canvas 自己研發光標系統、文本佈局系統和字體解析,因此除了自己的抽象數據結構外,連編輯操作都是自己實現了,編輯的呈現效果一致了,那協同編輯就自然水到渠成了。

google-docs

當然,Google Docs 的核心技術沒有開源,這種拼錢的工作還是得牢牢掌握在自己手中,國內的騰訊文檔和 WPS 等編輯器也算是 L2 的,不一定是用 Canvas ,但思路都是自己實現編輯佈局功能,棄用瀏覽器的 contenteditable

總結#

contenteditable is terrible, 但是編輯器已經最小化了對它的使用,比之更為嚴峻的是,操作系統、瀏覽器、輸入法相互組合形成的紊亂生態 —— 一個編輯器無法控制的,但產品又期望在上面開出繁花的生態。所以才說,Web 富文本編輯器是前端的天坑之一。

參考鏈接#

  1. 為什麼都說富文本編輯器是天坑?- 知乎
  2. 有多大比例的前端工程師,能在合理的時間內獨立開發出一個足以供商業網站使用的文本編輯器?- 知乎
  3. 開源富文本編輯器技術的演進(2020 1024)
  4. ContentEditable 困境與破局
  5. Slate.js - 革命性的富文本編輯框架 - 掘金
  6. Jason Chen: Building Editors in the Browser | JSConf.ar 2014
  7. Marijn Haverbeke: Salvaging contentEditable: Building a Robust WYSIWYG Editor | JSConf EU 2015
  8. ProseMirror
  9. Facebook open sources Lexical, an extensible text editor library - Hacker News
  10. 為什麼 ContentEditable 很恐怖 - OSCHINA - 中文開源技術交流社區
  11. 如何看待 Google Docs 將從 HTML 遷移到基於 Canvas 渲染?- 知乎
  12. 從流行的編輯器架構聊聊 Web 富文本編輯器的困境
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。