富文本編輯器一直是前端領域的一個天坑,但若不是深入接觸編輯器開發的工程師,卻不一定清楚富文本編輯器到底坑在哪里,作為有幸和編輯器打了一年交道的前端,今天來聊聊Web富文本編輯器的那些事。
通常當我們拿到一個帶有富文本編輯器的需求時,我們首先要理清這個需求的使用場景,然后我們可以為這些具體的業務場景選擇一款合適的開源富文本編輯器,進行定制開發
看看目前市面上我們可以選擇的開源編輯器的實現方式,大致分為兩種:
第一種是基于HTML DOM的屬性來實現,代表如、、Quill
這是使用最久的傳統富文本編輯器實現方式,這種實現方式的優勢很明顯,是瀏覽器Dom的一個原生屬性,值為true時表示該元素變為可編輯狀態。因此原生就直接支持很多內容編輯操作,包括光標位移、內容選擇的行為、鍵盤事件(如方向鍵控制光標)等等,甚至是富文本編輯所需要用到的絕大部分實現(.)
這些原生支持使得性能和輸入體驗都非常棒,在此基礎之上進行二次開發看起來相當容易,輔以技術,可以將編輯器放在一個獨立的對象下,與頁面的對象分離
缺點也非常要命多文檔文本編輯器課程設計,以why--is-為代表的文章,幾乎說明了一切,總結下來無非是:瀏覽器兼容性差、用戶行為難以控制、難以抽象編輯器內的視圖邏輯關系并將它們映射到代碼模型中(試想一下你要抽象一個變化規則不可掌控的可變Dom結構的邏輯關系)、光標(選區)的視覺位置與邏輯位置可能不吻合
截取自draft.js的演講PPT
第二種是基于自定義Model的實現,代表如:draft.js、trix
這種實現方式,簡單的來說就是定義一套編輯器內部使用的數據結構(model),與用戶在編輯器內所見的Dom視圖相映射;通過捕獲用戶的操作行為多文檔文本編輯器課程設計,由原先的直接操作Dom,改為更新數據結構狀態,再將更新后的狀態映射至視圖的方式,來實現編輯器的所見即所得,顯然操作行為對數據結構的更新是非常可控的
截取自draft.js的演講PPT
這是一種十分先進的編輯器設計理念,它幾乎拋棄了的特性,這也意味著所帶來的副作用都消失了
這種實現方式的另一個好處在于,它可以適用于多人在線協作的業務場景。由于用戶操作實際影響的是內部的數據結構,且每次操作產生的結果都被控制在一定范圍內(只影響部分節點),可以較為容易的通過鎖和diff算法來合并短時間內的多次修改。
看起來這顯然是一個比編輯器更好的選擇
遺憾的是目前這種實現方式的開源編輯器可供選擇的并不多,實際情況中可能并不能滿足所有的開發場景,比如draft.js只能基于react而且并非開箱即用,而如trix這樣相對小眾的項目在國內則有些水土不服(別問我怎么知道的),如果你目前使用的不是react或者就想要一個開箱即用的編輯器去做定制,又沒有條件自己造個輪子,在不需要考慮多人協作場景的情況下,我們依然可以從編輯器上尋求突破
回過頭來看看編輯器,現實情況其實也沒有那么糟糕,畢竟這是使用最為廣泛的一種實現方式,擁有大量的實踐,這些成熟的開源項目早已為我們提供了解決方案
來看看它們是怎么做的吧:
以國內熟知的為例(也是微信公眾號所用的編輯器),它的核心提供了這么幾樣東西
dtd規則:用來規定編輯器內的dom嵌套規則,和過濾方法搭配使用,避免出現
xxx
uNode對象:根據HTML DOM抽象而成的文檔模型對象,抽象了dom的屬性和層級關系,保留了一些dom操作的方法(與第二種實現方式的自定義model類似),將編輯器內容的HTML映射過來之后可以很方便的執行規則過濾,如剔除冗余屬性和非白名單標簽等
Range對象:光標和選區的信息對象,記錄了 當前光標(選區)的開始、結束邊界的容器節點和偏移量以及當前光標(選區)的閉合狀態,還提供了一系列對光標(選區)操作的API
:提供注冊、銷毀和觸發自定義事件監聽器的方法,用來生成一些鉤子
指令集:.增強版,執行指令的通用接口,富文本格式操作的核心,提供了一系列指定命令的執行和狀態查詢方法(如對選區內容執行字體加粗命令、查詢當前選區內容是否處于加粗狀態)
:撤銷重做的堆棧,記錄內容變化過程
:Dom操作方法集
可以利用上面這些核心方法組合出一些實用的工具,比如在中非常重要的過濾規則體系,就是利用了與uNode的組合實現的(通過對封裝了注冊規則的方法和執行過濾的方法,參數就是根據編輯器內容的dom轉化而來的uNode對象,基于該對象執行具體的過濾)
定義和執行過濾
整個正是圍繞著這些核心對象構建的,并且在此基礎上提供了大量的API以便開發者進行定制化的開發,顯然作為一個編輯器它已經足夠成熟了
但在實際的生產環境中,面對不同的產品需求我們依然需要處理一些棘手的情況
固定結構內容
一個常見的場景是,固定結構內容,比如圖片與圖片注釋
編輯器內的表現
這就是一個典型的固定結構內容,編輯器中出現了一個不可更改的固定搭配,即圖片后面必須跟著注釋輸入框
來看看要實現這個需求需要考慮哪些要問題
圖片和注釋元素必須一對一圖片和注釋元素的位置順序不能改變光標不允許插入到固定結構中間光標可以定位在注釋元素里注釋元素里只能放純文本
編輯器的設計原則之一是編輯器內的一切內容皆可自由編輯,而固定結構元素某種程度上違背了這一原則,這會帶來很多問題,用戶有太多方法可以破壞你預設的結構
一種常見的解決方案是將固定結構的元素包裹在一個不可編輯元素內,并為其中的可交互元素獨立設置交互事件(比如點擊輸入、粘貼內容過濾)
不可編輯的固定結構內容
但這還不夠,有幾個問題:
編輯器中存在不可編輯元素,會有瀏覽器兼容性的問題,如火狐瀏覽器下光標無法正確移動甚至無法刪除這個元素兩個不可編輯器的塊級元素在相鄰位置時,光標無法插入中間,退格鍵也會同時刪除多個復制粘貼這個內容,結構可能會錯亂其他操作也可能會破壞結構
為了解決上述問題,就需要劫持用戶的光標操作(鼠標點擊、方向鍵、退格鍵),同時設立一套結構規則來檢查當前結構是否有錯亂
預覽一下效果
簡而言之,就是通過劫持,判斷光標是否處于不可編輯元素的最近位置,符合條件時,用自定義行為代理瀏覽器默認的選擇、刪除、復制剪切等行為
再通過對光標移動事件()的監聽,檢查內容中的固定結構是否符合規則(如兩個不可編輯元素之間必須至少存在一個用于插入光標的空行標簽等)
面對固定結構內容,根據不同的使用場景,可以有兩種解決方案,
對于結構簡單但需要進行交互的場景,就像圖片注釋那樣,可以使用前面提到的=false+行為劫持+過濾規則的方式實現
對于結構較為復雜但不需要進行交互或交互場景較為簡單的情況,則可以使用來實現
生成的固定結構內容
使用的好處是不用擔心結構問題,這完全就是一張圖片,如果在文章發布后需要其他交互也可以在詳情頁將之轉化為正常的DOM結構,缺點是生成的圖片需要上傳至圖片服務器這會占用額外的存儲資源
另一個需要考慮的問題是在瀏覽器下如果畫布上有其他域過來的圖片,就算設置了允許跨域也會被的安全策略block[ (DOM 18): The is .],這就可能需要使用本地占位圖來解決
可以根據實際情況來選擇解決方案
光標
除此之外,UE也存在一些作為編輯器的通病,一個最常見的問題就是光標的視覺位置與邏輯位置的問題
試想有這么一段標紅的粗體文本
當我們將光標放在這段文字的開頭,我們會發現,光標的實際位置有4種可能
盡管視覺上的表現沒有什么區別,但光標在不同位置時用戶進行某些操作就會產生不同的結果
原本我們只是想用退格鍵將標題上移一行,但由于光標位置在|...的位置上,結果將標題的格式也給清空了
解決方法也很簡單,還是 劫持=>判斷=>代理,這也是編輯器對光標進行嚴格控制的通用解決方案
撤銷重做堆棧
撤銷重做堆棧也是一個問題,正常情況下會按照一個最小時間段自動記錄每一次的內容變化,以便用戶撤銷回上一步的狀態,但這也會帶來一些問題,試想一個這樣的場景
我們從本地插入一張圖片,這張圖片最終需要上傳到服務器上,所以我們先在編輯器內插入了一個占位圖,然后開始上傳本地圖片,等服務器返回了正確的圖片地址后,再將正確的圖片元素替換到占位圖所在的位置上,順便為圖片添加圖片注釋的組件
那么 (插入占位圖 => 上傳圖片 => 替換占位圖 => 添加附加組件)就是一個完整的事件流,如果單獨記錄了這個事件流中每一個步驟,當用戶執行撤銷操作的時候就會出現問題
因此我們需要為自動記錄設置一個暫停開關,這樣就可以控制的記錄時機
生命周期鉤子
為了使編輯器更加穩定,我們還可以通過來設計某些事件的生命周期鉤子
比如可以分發撤銷、重做操作完成前后的回調來做一系列額外的處理,也可以對圖片上傳的過程分發鉤子函數
富文本編輯器的話題其實遠不止上面這些,比如如何優雅的與編輯器內元素進行交互,如何由State驅動Dom,如何做移動端的適配,表格操作等等,每一點都可以深入探討,篇幅有限,這里就不再展開
總結一下,基于編輯器穩定可靠的定制開發要注意的幾個點
嚴格控制內容(格式規則檢查、內容輸入和輸出過濾)嚴格控制光標(劫持、檢查、代理)控制撤銷重做堆棧為一些關鍵操作添加生命周期鉤子