一、前言
想必大家看到這個標題,心中不禁會浮現幾個問題:
-
什麼是富文字編輯器?
-
富文字編輯器和遊戲角色有什麼關係?
-
為什麼是升級ing?
什麼是富文字編輯器——富文字編輯器整合了格式設定、媒體嵌入、社互動動等一系列編輯功能,所見即所得的給使用者提供多元的展示效果。譬如論壇、社群、評論等等都用到了富文字編輯器。
和遊戲角色的關係——富文字編輯器和遊戲角色有很多共通之處,為了讓富文字編輯器的介紹更加有代入感,本文將採用遊戲角色類比的方式進行講解。至於共通之處體現在哪裡,後面將一一介紹。
為什麼是升級ing——“升級ing”代表持續的進行時,本文的目的是聚焦富文字編輯器的共性問題,拋磚引玉,希望能給大家提供一些解決思路。富文字編輯器一直在持續發展中,而對於共性問題的探索也從未停歇過。
本篇文章主要分為五個部分:
-
前言
-
瞭解富文字編輯器
-
富文字編輯器選型指南
-
富文字編輯器如何擴充套件
-
總結
本文通過遊戲角色類比的方式,希望能夠讓富文字編輯器接觸較少的開發者,都可以深入的瞭解富文字編輯器。今天,我們就一起來探討下在富文字編輯器選型、擴充套件過程中遇到的共性問題。
二、瞭解富文字編輯器
通常,我們在選擇一款新的遊戲之前,都會選擇先去官網、論壇瞭解遊戲資料,從中篩選出有效資訊,輔助我們選擇合適的角色。
開發人員在接到富文字編輯器需求時,也不會隨便選擇其中一個,而是基於龐大的資料進行技術選型。這一節內容,就是為後續的選型所做的準備工作。
2.1 角色風格 - 富文字編輯器形態
遊戲角色在開服上線前,都會預設配備不同的風格,則風格往往決定了我們對於角色的初始印象。
富文字編輯器同樣具有幾種常用的初始形態,經典模式、文件模式、內聯模式,如下圖所示:
那麼從上圖的對比中,可以看出來:富文字編輯器必不可少的組成部分是內容編輯區域。狀態列是用來記錄編輯時的相關資料,可以隱藏。而工具欄則可以任意調整顯示的位置、時機甚至切換至幕後操控(通過快捷鍵等方式觸發)。
反之,我們可以獲得這樣一條訊息:通過工具欄、內容區域、狀態列、選單欄的不同組合可以賦予富文字編輯器不同的展示形態。
2.2 成長階段 - 富文字編輯器發展歷程
遊戲中的角色都是可成長角色,在成長過程總會出現一些瓶頸期,而跨過所謂的瓶頸期之後,角色的能力將出現顯而易見的改變。
在整個發展過程中,富文字編輯器遇到過一些困境。也正是因為這些困境,可以將發展歷程分為L0、L1、L2和L3階段。
L0->L1
L0,即初代的富文字編輯器,依賴於瀏覽器自身的execCommand,僅提供了有限的命令,實現最簡單的功能。隨著對樣式越來越豐富的要求,此時的富文字編輯器無法滿足需求,L1階段的編輯器應運而生。L1的富文字編輯器採用 自定義execCommand的方案,可以實現更加豐富的富文字功能。
L1->L2
L0、L1的富文字編輯器,仍然都是通過execCommand修改HTML。而不同瀏覽器中,對於同一表象的富文字,其HTML結構可能大不相同。
比如說 加粗 ,其HTML可能是加粗,可能是加粗,也可能是加粗等等。為了解決資料與檢視無法一一對應的問題,提出了自定義資料模型的概念。
自定義資料模型, 是富文字編輯器在富文字HTML-DOM樹的基礎上抽離出來的資料結構,相同的資料結構可以保證渲染的HTML也是相同的。自定義的命令直接控制資料模型,最終保證渲染的HTML文件的一致性。
對於相同的HTML,不同的富文字編輯器最終呈現的資料模型並不相同。以 Hello EditorName 為例,這裡對比了Quill、ProseMirror、Draft、Slate的資料模型如下:
L2階段的富文字編輯器,通過抽離資料模型,解決了富文字中髒資料、複雜功能難以實現的問題。通過資料驅動,可以更好的滿足定製功能、跨端解析、線上協作等需求。
L2->L3
到L2階段的編輯器,可以滿足絕大部分的使用場景。那為什麼後面又發展出L3呢?
這是因為,L0-L2的富文字編輯器都是基於瀏覽器的contentEditable,在修改資料模型時,往往需要對使用者操作進行攔截。對使用者行為進行攔截是很難控制的,再加上不同瀏覽器的相容問題,很容易出現bug。
為了解決contentEditable編輯不可控的問題,以 Google Docs 為代表的編輯器通過“自研排版引擎”步入了L3階段。
自研排版引擎,徹底拋棄了contentEditable,通過自行控制游標位置、選區繪製、排版、監聽輸入等行為,實現和瀏覽器相似的編輯效果。“自研”,無疑具備了更高的擴充套件性。但與此相對應的,其開發難度高、成本高、隱性問題多,在整體體驗和效能上與原生瀏覽器渲染仍存在一定差距,該階段的編輯器還有一段路要走。
ps: There are a thousand Hamlets in a thousand people's eyes。
下述關於成長階段的劃分僅基於作者本人的看法。
回顧富文字編輯器的發展歷程,不難發現:富文字編輯器的結構脫離不了模型、檢視、控制器這三大模組。如下圖所示:
正如遊戲角色所突破的瓶頸期,富文字編輯器在L1躍遷至L2發生的改變是:自定義資料模型的抽離;L2躍遷至L3的改變則是:排版引擎的全新定義。
三、富文字編輯器選型指南
當我們已經通過各種渠道瞭解到遊戲背景、人物資料之後,下一步就要登入遊戲建立遊戲角色。此時,新手常常遇到的困擾無疑就是:如何選擇最合適自己的遊戲角色。
類似的,對初次接觸富文字編輯器的小夥伴來說,常提到的問題是:我該選擇哪款富文字編輯器?
首先,可以根據你的業務需求,選擇對應階段的富文字編輯器:
-
業務本身就是以富文字編輯器為核心,或者有協同編輯需求。—— 選擇L2、L3的編輯器擴充套件,或者自研編輯器。這裡可以參考有道雲筆記和語雀的方案,參考連結見文末。
-
業務需求頻繁迭代,互動設計要求較高的。—— 建議選擇L2的編輯器。
-
業務較為穩定,要求不高的。——L1、L2任選。
-
如果業務場景比較複雜,難以評判之後的業務場景。——建議選擇L2的編輯器。
其次,在選定好階段的基礎上,根據專案架構(Vue、React、Augular等),以及富文字編輯器自身的特點,選擇適合的編輯器就可以。可以從下述幾個方面考慮:
-
開源程度
-
社群生態
-
互動細節
-
擴充套件支援度
-
定製化成本
以上,就是我梳理的選型套路。像是CKEditor、TinyMCE、Quill等都是有口皆碑的,大家在選型的時候不妨可以考慮下這些編輯器:
四、富文字編輯器如何擴充套件
選擇合適的角色,僅僅只是遊戲的開始。在遊戲過程中,需要不斷地調整遊戲角色的技能樹,將其潛力發展到極限。
隨著業務的不斷髮展,對富文字編輯器也會提出更高的要求。對此,經常困擾開發人員的往往就是以下幾個問題:
1、如何快速的擴充套件富文字功能?
2、如何快速的讓編輯器改頭換面?
對於以上兩個問題,下文將從能力擴充套件、主題改造兩個方面進行分析。
4.1 能力擴充套件
本節內容不會聚焦某個富文字編輯器具體如何擴充套件,而是針對上述不同擴充套件方式分享一些通用的處理思路。
4.1.1 工具欄擴充套件
就像是遊戲角色中,通過道具的不同裝配方案,調整最終的戰力資料。工具欄擴充套件就是通過對工具欄中不同功能的組合及改造,滿足最終的業務需求。
常見的工具欄是由若干個功能按鈕、狀態按鈕組、下拉選單、模態框等組成,如下圖所示:
一般的,富文字編輯器中都具備管理工具欄的配置項,可根據需要查閱官方文件。這裡我們探討一種場景,如何對已有的功能按鈕進行擴充套件?
以“Quill編輯器字型高亮的功能”為例——該功能按鈕的顏色與游標位置的字型顏色相呼應,從而達到繫結變化的效果,如下圖所示:
那麼,如果專案中引入的富文字編輯器不提供這樣的能力,該如何處理呢?這裡提供了兩種方案:
1)控制功能按鈕原生的UI樣式。
以下圖Tiny的字型高亮功能為例,按鈕是svg的結構,可通過控制strokeColor/fillColor達到效果。此時只需要在編輯器中增加游標位置變化的監聽OnSelectionChange,獲取游標位置的字型高亮顏色,重置按鈕UI。
2)SVG圖示替換當前的按鈕。
當功能按鈕是通過圖片的方式呈現,很難控制UI變化時,就可以採用此方案。以SVG圖示替換圖片圖示,通過變更svg-path的strokeColor/fillColor,達到相同的效果。
小結:如果專案是初次引入富文字編輯器,這裡不妨參考4.2主題改造中方案二。
4.1.2 選單欄擴充套件
“選單欄擴充套件”類似是給遊戲角色的裝備增加一些輔助的技能,這些新增的能力依託於裝備,每個裝備所配備的能力也互有差異。
本節所說的選單欄,特指編輯器內部的內聯選單欄。比如圖片工具欄、表格工具欄、右鍵選單欄等。如下圖所示:
對選單欄來說,最常出現的需求就是:給現有的外掛新增選單欄,如何實現呢?
1)富文字編輯器提供關聯配置能力,直接按照API文件配置即可。這裡摘取了Tiny編輯器中部分選單欄的配置方案,如下圖所示:
2)不具備關聯配置能力,此時需要監聽游標位置的變化。當游標在對應富文字資料區域內變化時,觸發事件/命令控制此選單欄展示。
不管是以上哪種方案,擴充套件的選單欄可以選擇內建到編輯器中實現,也可以通過事件丟擲到編輯器外部,以自定義元件的形式關聯。我比較推薦使用自定義選單欄元件的方案:
// 虛擬碼僅作為示例 輔助理解
// 富文字編輯器
<Editor :config="editorConfig"/>
// 自定義選單欄元件
<ContextToolBarComponent @command="handleCommand"/>
editor.on("selectionChange",(selection)=>{
// 判斷選區位置
CheckSelectionDataModel(selection)
// 控制選單欄展示隱藏,繫結資料實體
ControlContextToolBarComponentShowHide(selectionPosition)
})
// 選單功能觸發
handleCommand(command, _instance){
editor.execCommand(command, _instance)
}
4.1.3 編輯器內部擴充套件
對於遊戲角色的戰力提升而言,道具和裝備都屬於外力上的加強,角色本身也是需要重點關注的。
在富文字編輯器發展歷程一節中,總結出富文字編輯器的結構脫離不了模型、檢視、控制器這三大模組,那麼從這三個模組出發,擴充套件的方案也有所區分。
資料模型擴充套件
之前的介紹中提到過,L1-L2階段變遷的關鍵行為是抽離自定義資料模型。富文字編輯器的資料模型決定了最終富文字渲染的結構。當某個預置的富文字結構不能滿足預期時,就需要對這個富文字的資料模型進行擴充套件。根據富文字編輯器是處於L2階段前或階段後,擴充套件方式也有較大區別。
以圖片資料擴充套件關聯圖片備註為例,將
<figure>
<img src=“xxx”/>
</figure>
擴充套件為
<figure>
<img src=“xxx”/>
<caption>圖片備註</caption>
</figure>
1)L2階段及之後 富文字編輯器已具備資料模型抽象能力,此時只需要在資料結構中新增/編輯定義好的資料物件,並繫結渲染的HTML結構即可:
// 原資料結構 {type:'image',src:'xxx'}
// 擴充套件為 {type:'image',src:'xxx',caption:'圖片備註'}
// 新增資料物件 caption:'圖片備註',繫結HTML結構 `<caption>圖片備註</caption>`
2)L2階段之前, 富文字資料未進行資料模型的抽象。針對這種場景,可以利用html-parse-stringify外掛,自行抽離資料模型,再進行資料擴充套件。以
hello HTML-parse-stringfy !
為例,可以轉化為下圖所示的資料結構:html-parse-stringify外掛,可以將HTML_AST化,從轉化為所需要的資料結構,當前html-parse-stringify 也有一些問題,本文中不做擴散,感興趣的同學可以留言討論。
檢視擴充套件
檢視應該比較好理解,屬性資料相同的角色,配備不同的皮膚或者技能特效,在戰鬥過程中的呈現效果不同。同一個富文字資料來源,通過不同的檢視擴充套件,就可以展示不同的視覺效果。
1)不改變富文字的資料結構,僅在樣式設定上有所區分
通過切換DOM結構上繫結的class屬性,切換不同的樣式:
<blockquote class="pgc-blockquote-abstract">引用內容</blockquote>
<blockquote>引用內容</blockquote>
<blockquote class="pgc-blockquote-quote">引用內容</blockquote>
2)直接改變富文字的資料結構
普通連結切換至卡片,資料結構由Inline-Block切換至Block(link-card),DOM渲染隨之切換。DOM結構對比如下:
<p><br></p><p><a href="http://www.vivo.com.cn">vivo智慧手機官方網站-X60系列丨專業影像旗艦</a></p><p><br></p>
<p><br></p><a href="http://www.vivo.com.cn" data-draft-node="block" data-draft-type="link-card">vivo智慧手機官方網站-X60系列丨專業影像旗艦</a><p><br></p>
控制器擴充套件
控制器是比較抽象的概念,對遊戲角色來說主要是用來控制技能觸發條件、釋放的時機、觸發條件及屬性影響之類的。
類似的,富文字編輯器的控制器也是對資料層及檢視層控制方式的統稱。控制器的擴充套件,可以通過 事件、命令、配置項 等多維度實現。今天,我們簡單聊一下事件和命令如何擴充套件。
1)事件的擴充套件
事件有點像是主動技能,由角色主動釋放。富文字編輯器會主動丟擲一些事件,實現在編輯器內部或外部的控制,如OnselectionChange、OnInit等等。當新增的功能需要由編輯器內部控制外部元件,且原生的事件無法滿足時,往往需要通過新增事件監聽的形式實現。
事件的擴充套件在跨端操作中非常有用,後續會在跨端實踐一文中重點介紹。
// 簡單舉個例子,圖片上傳失敗後往往需要觸發重新上傳 :
// 若圖片通過編輯器上傳,失敗後點選重新上傳是編輯器自帶的行為邏輯。
// 但放在客戶端控制資源上傳的場景下,便需要編輯器通知客戶端“某某資源重新請求上傳”。
// 這個時候的跨端通訊,就需要富文字編輯器丟擲事件通知客戶端執行操作。
editor.on('appRetryingUploadImage', ({ data }) => {
call('reUploadPic', { picUrl: data.path, fileId: data.id })
})
2)命令的擴充套件
命令控制與事件控制邏輯相反,命令類似被動技能,當外部環境達到某個條件時,觸發角色的某種操作。富文字編輯器的命令管理就提供了在編輯器外部控制編輯器內部操作的能力。當操作不在Commond命令庫時,就需要對Command命令進行擴充套件。不同編輯器對Command的擴充套件寫法不同,但是萬變不離其宗 —— Command的核心是exec、refresh。
以CKEditor與Tiny為例:
CK5
class XXXCommand extends Command{
refresh(){}
execute(){}
}
CK4
editor.addCommand('XXXCommand', {
exec: ()=> {},
refresh: ()=>{}
})
Tiny
editor.addCommand('XXXCommand', () => {});
exec為執行命令的回撥函式,用來控制編輯器的相關操作的執行;refresh為命令指行結束後的回撥函式,常用來控制命令執行後編輯器相關狀態的重新整理;
除事件、命令外,部分編輯器還可以通過擴充套件配置項等方式,達到定製化操作的目的。
4.1.4 新增富文字功能外掛
要想將新技能的價值發揮到最大,不僅需要將角色的屬性資料提升到合適的水平,還要靈活調配技能組,配置合適的道具裝備等等。
富文字編輯器新增一外掛,往往需要多個模組共同擴充套件:
展開介紹下上圖中的各個模組:
定義資料模型
通過4.1.3 資料模型擴充套件一節,我們可以發現:資料模型是新增富文字功能的核心。只有先確定好資料層,才能決定檢視渲染如何控制,以及最終如何呈現在前端。
定義資料模型,主要分三步走:
1、確定資料模型的DOM是以Inline型別、Block型別還是可切換;
2、明確資料模型的准入限制及其可編輯限制,例如說標題中不能巢狀超連結等類似的規則;
3、確定資料模型及其資料輸入、資料輸出;
-
資料輸入 即需要配置的內容,以圖片為例,需要圖片URL、圖片的備註文案
-
資料輸出 為編輯器HTML渲染後的DOM結構
-
資料模型 包括:儲存的HTML字串、抽象的自定義資料型別(JSON)
輸入-模型-輸出的轉化示例圖,如下圖所示:
自定義工具欄按鈕
工具欄按鈕是資料控制的視窗,可以外顯在工具欄中,也可以隱藏通過快捷鍵控制。如果外顯在工具欄中,需要根據具體需求,定製對應狀態的功能按鈕,繫結選單或者控制操作,可參考4.1.1工具欄擴充套件一節。
新增事件或命令
確定好資料核心和控制視窗之後,下一步就是制訂控制策略。首先確定需求中的控制策略,是正向的——由富文字編輯器操作觸發外部反饋,還是反向的——由外部觸發編輯器內部操作,還是兩者皆存在。然後根據控制策略,對應的選擇擴充套件事件、命令還是兩者都擴充套件。具體擴充套件方案可參考4.1.3控制器擴充套件一節
關聯游標選區
通過游標的位置,確定當前選區對應的資料結構,從而控制特殊狀態的切換。怎麼確定是否需要關聯游標選區呢?
1、新增功能的按鈕狀態是否與游標位置有關。在自定義工具欄按鈕這一步驟中就可以完成關聯;
2、新增功能是否需要關聯選單欄顯示。處理方案參考4.1.2選單欄擴充套件一節;
3、新增功能是否與其他富文字功能相關聯。如互斥邏輯 —— 標題內不允許插入超連結;
若確定需要關聯游標選區,那麼富文字編輯器中就需要增加OnSelectionChange的監聽,完成相關的處理。
editor.on("selectionChange",(selection)=>{
// 判斷選區位置
CheckSelectionDataModel(selection)
// 修改自身及其他按鈕狀態
ChangeButtonStatus(button)
// 控制選單顯隱
ControlMenuShow(menuBar)
})
關聯操作記錄管理(撤銷、重做)
在富文字編輯器中進行互動操作時,不可避免的會出現一些誤操作。富文字編輯器的互動場景越複雜,出現誤操作的概率越高。因此一般富文字編輯器都會對操作記錄進行管理,用以降低誤操作所帶來的影響。
不同的富文字編輯器中undo/redo的處理邏輯不同,相似的是富文字編輯器會定義操作過程中的關鍵行為(如常見的插入、刪除等),將其儲存在操作記錄中。
當我們在新增的外掛功能中關聯操作記錄管理時,只需要複用其他外掛關鍵行為的入庫出庫邏輯就可以啦。
// 虛擬碼,僅輔助理解
UndoManage.push(keyOperation)
UndoManage.undo()
UndoManage.redo()
增加複製貼上控制
“複製貼上”算是富文字編輯器操作中最為頭疼的問題之一,有相關開發經驗的小夥伴們應該遇到過,從其他來源複製的內容貼上到編輯器內,檢視展示異常的情況。針對這種情況,往往需要對剪下板中的資料進行過濾,轉化為富文字編輯器可識別的資料。
editor.on('paste',(evt)=>{
// 根據游標處對應的資料結構,確定過濾規則
let filterRules = checkSelection()
// filterRulers JSON資料結構對資料物件進行過濾修改
// filterRulers HTML字串可以使用正規表示式或者編輯器內建的過濾方法
evt.data = filterRulers.exec(evt.data)
})
4.2 主題改造
“主題改造”應該很好理解,就是遊戲中的更換皮膚,快速的切換遊戲角色的風格。
在富文字編輯器中主題改造,其實也就是工具欄、選單欄以及特殊富文字的樣式上的更換。通常的處理方案有兩種:
引入新主題樣式檔案。替換新主題樣式檔案,或者在舊主題樣式上進行樣式覆蓋。
構建脫離於編輯器本身的工具欄元件。將主題修改涉及到的工具欄、選單欄脫離編輯器,在專案中建立全新的工具欄元件、選單欄元件。
如果是對已有專案進行改造,那麼需要考慮到新舊主題切換的投入產出比,擇優選取;如果是新專案且對主題樣式細節要求較高的話,可以採用方案二。
// 虛擬碼 僅輔助理解
<!-- 自定義工具欄 -->
<CustomToolbarComponent>
<ButtonBold @click="execCommondBold"/>
<ButtonUnderline @click="execCommondUnderline"/>
<ButtonHead @click="execCommondHead"/>
</CustomToolbarComponent>
<!-- 富文字編輯器編輯區域 -->
<EditorContainer></EditorContainer>
<script>
// 執行富文字編輯器的Commond
execCommondBold:()=>{
editor.execCommond('bold')
}
execCommondUnderline:()=>{
editor.execCommond('underline')
}
execCommondHead:()=>{
editor.execCommond('head')
}
</script>
<style>
// 自定義主題樣式
button-bold{}
button-underline{}
button-head{}
</style>
方案二相較於方案一來說:
-
優點:將工具欄的控制權由第三方編輯器,遷移至專案中,在可控性和擴充套件性都能得到最大限度的提升;對跨端業務的適配度更高,各端只需一套控制方案,各功能元件分渠道定製即可;
-
缺點:需要將工具欄中按鈕繫結的命令/事件、狀態繫結等控制方案轉移至新的元件中,會佔用一定的開發成本。
小結:功能擴充套件和主題改造的方案不止以上這些,也存在其他折衷的方案,只需要根據業務場景選擇合適的方案即可。有句話說的好:「最好的不一定適合,適合的才是最好的」。
至此,本篇文章的內容也就接近尾聲了。希望大家看到這裡,對於以下幾個問題,能得到一些解答:
1、基於現在的業務需求,該選擇哪款富文字編輯器?
2、隨著業務的擴充套件,該如何擴充套件富文字功能?
3、設計改版,如何快速的改頭換面?
五、總結
就像是在遊戲世界中,你不得不打怪升級。同時在過程中,你也會積累更多的技巧,為之後的越級打怪打下堅實的基礎。在富文字編輯器開發過程中,確實會遇到很多難解的問題、複雜的需求,花費了我們大量的時間精力。在一次又一次的錘鍊中,我們都將會有所收穫,有所成長。
本篇文章分享了我在富文字編輯器開發過程中關於共性問題的一些思考,希望能對即將參與富文字編輯器開發的小夥伴,或者正在進行富文字編輯器開發的小夥伴帶來一些幫助。
後續還會跟大家分享一些富文字編輯器在跨端方案解決上的一些經驗,如果感興趣的話可以持續關注。
參考資料
作者:vivo網際網路前端團隊-Tian Yuhan