<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          談?wù)劄g覽器中富文本編輯器的技術(shù)演進(jìn)

          共 7301字,需瀏覽 15分鐘

           ·

          2021-11-17 01:18

          作者簡介:劉楊,抖音前端團(tuán)隊(duì)低代碼平臺(tái)核心開發(fā)者。

          發(fā)展歷程

          富文本編輯器按發(fā)展歷程而言,分為 L0、L1、L2 三個(gè)階段,每個(gè)階段都比上一個(gè)階段定制程度更高,由瀏覽器導(dǎo)致的問題也更少(因?yàn)閺?qiáng)依賴瀏覽器 API 的情況更少),同時(shí)開發(fā)難度也更大。本文將詳細(xì)講解各個(gè)階段,然后列舉一些相關(guān)的產(chǎn)品來加以說明。

          L0 階段

          這是富文本編輯器的早期階段,這個(gè)階段的編輯器強(qiáng)依賴于 DOM API,包括:

          • 可編輯內(nèi)容依賴 contenteditable API;
          • 編輯內(nèi)容使用 document.execCommand API。

          并且沒有抽象的數(shù)據(jù)模型來描述富文本編輯器的內(nèi)容與狀態(tài)。這個(gè)階段的編輯器有大名鼎鼎的 UEditor,也有 CKEditor 1 - 4,至今許多郵件編輯器也依然處于這個(gè)階段。

          Content Editable

          L0 階段的編輯器主要就是依賴于 Content Editable API 來實(shí)現(xiàn)功能的。首先,任何 HTML 元素加上 contenteditable="true" 之后里面的內(nèi)容都可以被編輯,然后,如果想點(diǎn)擊某個(gè)按鈕來操控這些內(nèi)容,則可以通過瀏覽器提供 document.execCommand API 來實(shí)現(xiàn)。document.execCommand 支持的操作類型多種多樣,包括加粗、改背景、綁定鏈接、復(fù)制、剪切等等。

          查看有哪些 command[1]。(鏈接見文末)

          優(yōu)勢

          L0 階段的編輯器主要優(yōu)勢有:

          • 技術(shù)門檻低;
          • 基于瀏覽器原生編輯能力,輸入非常流暢;
          • 沒有令人頭疼的組合輸入問題,這點(diǎn)會(huì)在后文中詳細(xì)說明。

          劣勢

          當(dāng)然,這個(gè)階段的劣勢也比較明顯。

          劣勢一

          首先,第一個(gè)問題就是不同瀏覽器對于同一個(gè)操作有不同的實(shí)現(xiàn),導(dǎo)致視覺與實(shí)際 DOM 存在一對多的關(guān)系。比如對于設(shè)置 粗斜體 這個(gè)操作,不同瀏覽器的實(shí)現(xiàn)的 DOM 就不同,可能存在以下情況:

          <strong><em>粗斜體em>strong>
          <em><strong>粗斜體strong>em>
          <strong><em>em>strong><strong><em>em>strong><strong><em>em>strong>
          <em><strong>strong>em><em><strong>strong>em><em><strong>strong>em>

          這種結(jié)構(gòu)最直接的影響就是樣式不好設(shè)置,上例還好,有些操作會(huì)用不同標(biāo)簽實(shí)現(xiàn),這樣一來就得兼容多個(gè)瀏覽器,給多個(gè)標(biāo)簽設(shè)置樣式。

          劣勢二

          第二個(gè)問題是不同瀏覽器的選區(qū)實(shí)現(xiàn)也不同,導(dǎo)致視覺選區(qū)與實(shí)際 DOM 選區(qū)之間存在多對多的關(guān)系。

          再以選中 斜體 的斜體為例,視覺上選中了斜體,實(shí)際上就不清楚了,假設(shè) DOM 結(jié)構(gòu)是 粗斜體,那么就存在下列可能:

          • 選中了 粗斜體 ,如果做刪除操作,會(huì)遺留標(biāo)簽;
          • 選中了 粗斜體,如果做刪除操作,也會(huì)遺留 strong 標(biāo)簽;
          • 選中了 粗斜體。

          再結(jié)合上面多種 DOM 結(jié)構(gòu),每種都可能有多種選區(qū),那么我們要做的兼容處理就更加復(fù)雜。

          劣勢三

          第三個(gè)問題是光標(biāo)放置的位置也是不確定的。

          假設(shè)在 粗斜體 前插入 i 字符,實(shí)際上可能是這些情況:

          • i粗斜體 => i粗斜體;
          • i粗斜體 => i粗斜體;
          • i 粗斜體 => i粗斜體。

          劣勢四

          第四個(gè)問題發(fā)生在復(fù)制粘貼操作上,永遠(yuǎn)都不可能確定從別的地方復(fù)制過來的內(nèi)容粘貼到 L0 階段的編輯器上會(huì)發(fā)生什么,因?yàn)椋?/p>

          • 復(fù)制的內(nèi)容的 HTML 標(biāo)簽充滿著可能性;
          • 永遠(yuǎn)不知道瀏覽器是怎么處理這些標(biāo)簽的。

          劣勢五

          沒有辦法實(shí)現(xiàn)協(xié)同。

          總結(jié)

          隨著瀏覽器的演變,上述問題可能有些已經(jīng)越來越趨向于統(tǒng)一,但仍不可避免的還會(huì)有許多,更關(guān)鍵的是不受控制,BUG 隨時(shí)可能會(huì)出現(xiàn),修修補(bǔ)補(bǔ)也將會(huì)越來越多,項(xiàng)目也會(huì)越來越難以維護(hù)。因此,L1 階段的編輯器就應(yīng)運(yùn)而生。

          L1 階段

          這個(gè)階段是大多數(shù)現(xiàn)在富文本編輯器所處在的階段,比如 QuillCKEditor 5,Slate,Draft.js 等等,它最明顯的兩個(gè)特點(diǎn)是:

          • 仍然依賴于 contenteditable API 來使得內(nèi)容能編輯,但是不再依賴 document.execCommand API 來操作內(nèi)容,而是改為自己實(shí)現(xiàn)。
          • 有抽象的數(shù)據(jù)模型來描述富文本編輯器的內(nèi)容與狀態(tài)。

          Modal - 編輯器內(nèi)容的抽象

          上面說到,L1 階段的編輯器會(huì)有抽象的數(shù)據(jù)模型來描述富文本編輯器的內(nèi)容與狀態(tài),這個(gè)數(shù)據(jù)模型就被稱為 Modal,當(dāng)然,不同編輯器給取的名字會(huì)不一樣,下面舉兩個(gè)編輯器來說明。

          L1 階段的鼻祖 Quill 的 Modal

          例如下面這段話:

          A
          B C D

          在 Quill 就會(huì)用這樣一個(gè)數(shù)據(jù)結(jié)構(gòu)去表示:

          {
          ????"ops":?[
          ????????{
          ????????????"insert":?"A\nB?"
          ????????},
          ????????{
          ????????????"insert":?"C",
          ????????????"attributes":?{
          ????????????????"bold":?true
          ????????????}
          ????????},
          ????????{
          ????????????"insert":?"D"
          ????????}
          ????]
          }

          Quill 的 Modal 基于 OT 模型,有 ?retain,insert,delete 三種操作類型,使用可選的 attributes 屬性來標(biāo)記內(nèi)容的一些特性。這種模型使得它天生就支持協(xié)同。Quill 管這個(gè) ModalDelta

          Quill 拋棄了 DOM 的節(jié)點(diǎn)樹的層次,因此完全看不出包裹文字的標(biāo)簽和節(jié)點(diǎn)關(guān)系,只有一個(gè)扁平化后的數(shù)組 ops。幾乎所有的 L1 階段的富文本編輯都會(huì)做或多或少的扁平化,Quill 是最徹底的那一類。當(dāng)然,扁平化帶來的好處是對性能提升有幫助,弊端則是在表示一些復(fù)雜的嵌套內(nèi)容時(shí)會(huì)比較吃力,比如在表格的單元格中插入另一個(gè)表格。

          Slate 編輯器的 Modal

          Slate 保留了 DOM 的樹形結(jié)構(gòu),因此節(jié)點(diǎn)的層次關(guān)系是比較直觀明了的。下面是上面那個(gè)例子的表示:

          [
          ????{
          ????????"type":?"paragraph",
          ????????"children":?[
          ????????????{
          ????????????????"text":?"A"
          ????????????}
          ????????]
          ????},
          ????{
          ????????"type":?"paragraph",
          ????????"children":?[
          ????????????{
          ????????????????"text":?"B?"
          ????????????},
          ????????????{
          ????????????????"text":?"C",
          ????????????????"bold":?true
          ????????????},
          ????????????{
          ????????????????"text":?"?D"
          ????????????}
          ????????]
          ????}
          ]

          View - 將 Modal 渲染出來

          View 層類似 React 中的 render,將 Modal 數(shù)據(jù)給渲染出來,渲染出來的內(nèi)容包括編輯器內(nèi)的內(nèi)容和選區(qū)等。這樣一來,就能夠自己決定什么樣的內(nèi)容輸出什么樣的 DOM 結(jié)構(gòu),不依靠瀏覽器的實(shí)現(xiàn),從而避免 L0 中 DOM 結(jié)構(gòu)多樣性的問題。不同編輯器對 View 層的稱呼不一樣,比如 Slate 就稱之為 Rendering。如今,很多編輯器都基于 VM 框架(如 React)來實(shí)現(xiàn) View 層。

          Selection 的進(jìn)一步封裝

          無論是 L0 還是 L1 階段,選區(qū)都需要在原生 Selection API 的基礎(chǔ)上進(jìn)行封裝來實(shí)現(xiàn)。原生的 Selection 對象是由多個(gè) Range 對象組成的,Range 對象內(nèi)包含以下四個(gè)屬性:

          • anchorNode:代表鼠標(biāo)開始按下處的文字或葉子節(jié)點(diǎn);
          • anchorOffset:代表鼠標(biāo)開始按下處的文字在文字節(jié)點(diǎn)中的第幾個(gè)或葉子節(jié)點(diǎn)之前的同級(jí)節(jié)點(diǎn)數(shù);
          • focusNode:代表鼠標(biāo)松開時(shí)的文字或葉子節(jié)點(diǎn);
          • focusOffset:代表鼠標(biāo)松開時(shí)的文字在文字節(jié)點(diǎn)中的第幾個(gè)或葉子節(jié)點(diǎn)之前的同級(jí)節(jié)點(diǎn)數(shù)。

          相比 L0 階段,L1 階段的編輯器會(huì)將 Modal 的一些數(shù)據(jù)封裝到 Selection 對象中去。下面舉幾個(gè)編輯器的例子來說明。

          Quill 的抽象

          Quill 的 Selection 與原生的不同,只有一個(gè) Range 對象。Range 也只由 { index, length } 這種極其簡單的數(shù)據(jù)組成:

          • index 表示選區(qū)開始的內(nèi)容距離開頭的絕對位置;
            • 絕對位置的計(jì)算就是把當(dāng)前內(nèi)容之前所有的內(nèi)容數(shù)量都加起來;
            • 單個(gè)文字、圖片、視頻等這樣的內(nèi)容都是按數(shù)量 1 來計(jì)算的。
          • length 表示選中區(qū)域的內(nèi)容的數(shù)量。

          Quill 的 Range 能這么設(shè)計(jì)與它的 Modal 設(shè)計(jì)是強(qiáng)相關(guān)的。

          Slate 的抽象

          Slate 的 Selection 對象也只有一個(gè) Range 對象。它 的 Selection 對象參考了原生的實(shí)現(xiàn),有 anchorfocus 兩個(gè)對象。

          anchor 對象由 pathoffset 屬性組成,path 相當(dāng)于 anchorNode 的角色,用來確定節(jié)點(diǎn)的位置 offset 相當(dāng)于 anchorOffset,用來確定文字等內(nèi)容在節(jié)點(diǎn)中的位置,focus 也類似。

          例如下例中的 Text 節(jié)點(diǎn)的 path[0, 1, 0],然后假設(shè) Text 節(jié)點(diǎn)的內(nèi)容是 123,那么其中 2offset 就是 1。

          上圖中,每一層最左邊的節(jié)點(diǎn)都標(biāo)記為 0,然后向右依次加一,這樣,任何一個(gè)節(jié)點(diǎn)的位置都能通過一個(gè)編號(hào)數(shù)組給唯一確定。

          總結(jié)

          L1 階段的編輯器對 Selection 的封裝方式使得選區(qū)也有一種數(shù)據(jù)結(jié)構(gòu),從而使得同一份數(shù)據(jù)結(jié)構(gòu)有唯一的渲染,避免 L0 中選區(qū)的問題。

          Commands - 能被稱為 L1 階段的核心要點(diǎn)

          L1 階段的編輯器摒棄了瀏覽器的 document.execCommand,從而完全自己來實(shí)現(xiàn)對編輯器內(nèi)容的操作,它能在很大程度上避免 L0 中瀏覽器操作的不確定性。

          事件監(jiān)聽

          L1 階段的編輯器大都會(huì)通過事件監(jiān)聽來猜測用戶想要對內(nèi)容的操作。這種方式的好處顯而易見:通過監(jiān)聽 DOM 事件來操作 Modal,從而能保證唯一的渲染。

          如果用戶點(diǎn)擊編輯器上的按鈕,那么這種操作就是非常確定的,因?yàn)榘粹o的作用是我們自己定的,比如加粗、改成斜體等。

          如果用戶在編輯區(qū)域進(jìn)行輸入的話,那么可以通過 beforeinput 之類的事件知道用戶準(zhǔn)備輸入什么。但這種方法依然會(huì)遇到一些問題,最大的問題就是上文中提到的組合輸入問題。

          組合指的就是 操作系統(tǒng)、輸入法、瀏覽器 加在一起的組合。一個(gè)用戶輸入的內(nèi)容經(jīng)過這三者才到達(dá)我們的事件監(jiān)聽函數(shù)中。那么其中任何一環(huán)出現(xiàn)差錯(cuò)就可能會(huì)帶來如下問題:

          • 不是所有的用戶操作都有事件支持,例如某些安卓機(jī)輸入法的聯(lián)想詞;
          • 有些操作觸發(fā)的事件是錯(cuò)誤的,例如用戶在輸入文字,但識(shí)別成刪除文字;
          • 瀏覽器對事件支持的兼容性問題,例如 beforeinput 就有些瀏覽器不支持,或支持不全面。

          DOM 變更監(jiān)聽

          除了事件監(jiān)聽,另一個(gè)方式就是 DOM 變更監(jiān)聽。我們依然使用 contenteditable="true" 來使得編輯器內(nèi)容是可以被編輯的。然后使用 Mutation Observer[2] 來監(jiān)聽編輯器內(nèi)容的變化,接著根據(jù)內(nèi)容的變化反推用戶的操作,從而修改 Modal,最后再次根據(jù) Modal 渲染一遍編輯器內(nèi)的內(nèi)容,確保內(nèi)容的確定性。

          這種方式彌補(bǔ)了一些事件監(jiān)聽的不足,但仍然有缺陷:

          • 反推的過程完全是根據(jù)經(jīng)驗(yàn),難免有經(jīng)驗(yàn)不足,某些情況沒考慮到的情況;
          • 可能發(fā)生錯(cuò)誤的推測,造成錯(cuò)誤的渲染。

          使用 VM 框架帶來的一些問題

          上文中說到現(xiàn)在很多編輯器會(huì)使用 VM 框架來做渲染,這就使得 Modal 和 View 之間還存在 Virtual DOM。但由于 contenteditable 能繞開 VM 框架直接操作 DOM,所以可能會(huì)遇到狀態(tài)不一致或者渲染錯(cuò)誤的問題,這就需要去花時(shí)間解決。

          Slate 的 Commands 實(shí)現(xiàn)

          Slate 完全重寫了 Commands API,它是結(jié)合事件監(jiān)聽和 Mutation Observer 一起來實(shí)現(xiàn)的,它自己定義了一套 Transform API 去更改 Modal,使用者可以自定義 Modal 到 DOM 的渲染邏輯。它的渲染層可以是 React,也可以是其他框架。

          Operations - 多人協(xié)同必備

          Operations 就是記錄了一系列原子化操作的數(shù)組。以 Quill 為例,它的 Operations 的數(shù)據(jù)結(jié)構(gòu)與 Modal 一致(上文提到過 Quill 的 Modal 就是一系列原子化的操作),不過與 Modal 描述當(dāng)前內(nèi)容不同,Operations 記錄了所有的原子化操作,也可以說 Modal 是 Operations 經(jīng)過一定的轉(zhuǎn)換(例如合并一些項(xiàng))得到的。

          Operations 對實(shí)現(xiàn) OT 算法很有幫助。OT 算法本質(zhì)上是將不同用戶的原子化操作合并的過程,目的是為了解決多人編輯之間的沖突。Quill 的 OT 算法在 quill-delta 庫中實(shí)現(xiàn)的。

          要了解 OT 算法可以 閱讀這篇文章[3]。(鏈接見文末)

          優(yōu)勢

          L1 階段的編輯器總結(jié)下來有這么一些優(yōu)點(diǎn):

          • 從原理上解決了大部分 L0 中各種不確定的問題,減少了 BUG 的產(chǎn)生;
          • 能支持多人協(xié)同;
          • 能滿足大部分使用場景。

          劣勢

          • 相比 L0 階段,引進(jìn)了一些組合輸入問題;
          • 相比 L0 階段,引進(jìn)了可能要處理一些 DOM 變更監(jiān)聽的未知性問題;
          • 依然擁有 contenteditable 的一些問題,比如光標(biāo)的兼容性問題等等。

          L1 階段富文本編輯器的比較

          下面舉了一些富文本編輯器的例子做了些簡單的比較:

          編輯器優(yōu)點(diǎn)缺點(diǎn)
          Quill首創(chuàng)的 Delta 抽象數(shù)據(jù)模型數(shù)據(jù)模型是線性的,無法較容易的支持復(fù)雜的嵌套場景,未來可能有大版本改動(dòng)
          Prosemirror強(qiáng)大的定制能力新概念和 API 比較多,有著高昂的學(xué)習(xí)上手成本
          Draft.jsReact ?友好的編輯器數(shù)據(jù)模型是線性的,無法較容易的支持復(fù)雜的嵌套場景
          CKEditor 5強(qiáng)大的功能和定制能力幾乎沒有缺點(diǎn)
          Slate強(qiáng)大的數(shù)據(jù)模型,靈活的設(shè)計(jì),理論上能實(shí)現(xiàn)任何強(qiáng)大的功能底層重構(gòu)多次,目前版本可能還不穩(wěn)定

          L2 階段

          Google Docs 的問世開創(chuàng)了 L2 級(jí)別編輯器的時(shí)代,它完全不依賴 Content Editable API,包括選區(qū)、光標(biāo)等,都是是自己繪制的,甚至自己實(shí)現(xiàn)了一個(gè)基于元素和絕對定位的排版引擎,基本上脫離了瀏覽器自身的大部分排版規(guī)則,可以說是非常復(fù)雜了。這樣做帶來的好處是顯而易見的:

          • 所有瀏覽器無論做什么操作進(jìn)行選區(qū)的選中,都能夠保持一致性,例如在不同瀏覽器中雙擊選中,可能有些瀏覽器選中的是一個(gè)詞語,有些瀏覽器選中的是一句話,這是由瀏覽器自身邏輯決定的,而自己繪制則完全能避免這些問題;
          • 不會(huì)有光標(biāo)的兼容性問題,比如光標(biāo)位置問題偏移、光標(biāo)顯示不正常等等,自行繪制光標(biāo)無論是在哪個(gè)瀏覽器下,都是一致的;
          • 不依賴于瀏覽器大部分的排版,雖然說 L1 階段我們可以控制渲染的 DOM 結(jié)構(gòu)和 CSS,但仍然依賴瀏覽器去排版繪制,像 Google Docs 這種直接基于絕對定位等少量的瀏覽器特性去自行計(jì)算各元素的位置,進(jìn)行排版,就能極大程度上避免瀏覽器在排版上的問題。

          同時(shí),像分頁、標(biāo)尺、腳注等一些高級(jí)功能在有了自己的排版引擎后實(shí)現(xiàn)起來就比較簡單了。當(dāng)然,這個(gè)階段的編輯器已經(jīng)極為復(fù)雜了,一般的團(tuán)隊(duì)不需要自研這么復(fù)雜的編輯器,也用不到。

          未來

          Google 在今年發(fā)表了一篇文章 Google Docs will now use canvas based rendering: this may impact some Chrome extensions[4],宣布將來將進(jìn)一步脫離 DOM,使用 Canvas 來進(jìn)行渲染。對于 Google Doc 這樣一種極為專業(yè)的編輯器來說,這是肯定要走的路,因?yàn)樯厦嬉舱f到了,Google Doc 并沒有完全脫離瀏覽器的排版與繪制,另外,像一些字體的渲染仍需要利用瀏覽器基本的實(shí)現(xiàn),這可能導(dǎo)致不同瀏覽器的效果存在差異。使用 Canvas 來自己繪制會(huì)進(jìn)一步減少這種問題。另一條需要關(guān)注的路線是瀏覽器的實(shí)現(xiàn)逐漸靠著標(biāo)準(zhǔn)進(jìn)行統(tǒng)一,說不定哪天很多功能又能重回原生了!

          參考資料

          [1]

          查看有哪些 command: https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand#%E5%91%BD%E4%BB%A4

          [2]

          Mutation Observer: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

          [3]

          閱讀這篇文章: https://nicodechal.github.io/2020/08/10/ot-js-transform-analysis/

          [4]

          Google Docs will now use canvas based rendering: this may impact some Chrome extensions: http://workspaceupdates.googleblog.com/2021/05/Google-Docs-Canvas-Based-Rendering-Update.html


          往期推薦


          2021 TWeb 騰訊前端技術(shù)大會(huì)精彩回顧(附PPT)
          面試題:說說事件循環(huán)機(jī)制(滿分答案來了)
          專心工作只想搞錢的前端女程序員的2020



          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 58
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产一级A片免费在线观看 | 一道本高清无码在线看 | 国产无遮挡A片又黄又爽小直播 | 国产 无码 高潮 在线 | 成人网站在线观看视频 |