通過實時協作建立富文字編輯器的經驗教訓
CKEditor 5推出分散式修改同一份文件的功能,好像以後大家可以一起愉快地修改程式碼了,再也不用手工解決Git的衝突,在選擇你的原始碼還是我的原始碼之間衝突,大師Kent beck還為此提出對人行為的約束規則:test && commit || revert。閒話少說,看看它的分散式一致性方案是怎麼解決的,據說Google Docs也有類似協同編輯功能。
本文描述瞭如何在CKEditor 5中實現實時協作編輯。術語“協作”和“實時協作編輯”在整個文件中可互換使用,以指代由CKEditor 5實現的“真實實時協作”。替代方案 從一開始我們的目標就是提供一種解決方案,在協作編輯方面不會帶來任何妥協。可能會嘗試使用許多快捷方式來在應用程式中實現協作,而這些快捷方式並非專為此設計,但最終它們都會導致糟糕的使用者體驗:
- 完全或部分內容鎖定。 只有一個使用者可以同時編輯文件或文件的給定部分(塊元素:段落,表格,列表專案等)。
- 在“只讀”模式下 啟用協作功能。使用者可以對文字進行註釋,但前提是編輯器處於“只讀”模式。
- 手動解決衝突 。必須由其中一個使用者手動解決同一位置的編輯。
- 僅 在協作編輯中啟用基本功能 。您可以加粗文字或建立標題,但忘記對錶或巢狀列表的支援。
- 缺乏意圖儲存 。在解決衝突之後,使用者最終得到的內容與他們打算建立的內容不同(換句話說:衝突解決能力差)。
我們想避免所有這些陷阱。它需要建立一個真正實時的協作編輯解決方案,使所有使用者能夠同時建立和編輯內容,而不受任何限制或功能剝離。我們總是有一個想法:無論協作編輯是開啟還是關閉,編輯器的外觀,感覺和行為都應該相同。一切都與衝突有關 在協作編輯期間,使用者不斷修改其本地編輯器內容並在它們之間同步更改。當兩個或多個使用者編輯內容的相同部分時,可能會和將要出現衝突。解決衝突是制定或破壞協作編輯體驗的原因。
例如,當兩個使用者刪除同一段落的一部分時,他們的編輯狀態需要同步。但是,這是有問題的:當使用者A 從使用者B 接收資訊時,此資訊基於使用者B的 內容 - 這與使用者A 當前正在處理的 內容不同。
這是最簡單的方案之一,但即便如此,如果沒有適當的機制,也會導致缺乏最終的一致性 - 這是任何協作編輯解決方案的基本要求。一些編輯引入完全或部分內
有幾種方法可以在實時協作編輯中實現衝突解決。兩個主要候選者是Operational_transformation" rel="nofollow,noindex" target="_blank">操作轉換 (OT)和無衝突複製資料型別 (CRDT)。我們選擇了OT,也許有一天我們會寫下我們對正在進行的OT與CRDT戰鬥的看法。長話短說,CKEditor 5使用OT來確保它能夠解決衝突。OT基於一組操作 (描述更改的物件)和相應地轉換這些操作的演算法,因此無論接收這些操作的順序如何,所有使用者都以相同的編輯器內容結束。作為一個概念,它在IT文獻中得到了很好的描述([ 1 ],[ 2 ]),現有的實現證明了這一點(儘管沒有一個可以作為我們需求的穩定和強大的基礎)。
因此,在2015年,我們開始著手實施OT實施。我們很快意識到,基本的操作轉換(通常描述和實現)不足以為富文字編輯提供高質量的使用者體驗。OT的基本形式定義了三個操作:insert ,delete 和set屬性 。這些操作旨在線上性資料模型 上執行。他們負責插入文字字元,刪除文字字元和更改其屬性(例如設定粗體)。但是,強大的富文字編輯器需要更多。 支援複雜的資料結構 線性資料模型 是一個簡單的資料模型足以代表純文字。相反,HTML是一種基於樹的語言,其中元素可以包含多個其他元素。HTML文件在瀏覽器中表示為文件物件模型 (或DOM ),它是樹形結構 。可以線上性模型中表示簡單,平坦的結構化資料,但是當涉及複雜的資料結構(如表格,字幕影象或包含塊元素的列表)時,此模型會失敗。元素根本不能包含其他元素。例如,塊引用不能包含列表項或標題。
因此,我們需要更進一步,並提供適用於樹資料結構的 操作轉換演算法。早在2015年,我們可以找到一篇關於樹木的OT([1] )的文章,並且沒有任何證據證明有人在OT上為樹木工作。我們基於這項研究,但事實證明這比我們預期的更具挑戰性。第一次實施花了我們一年多的時間,在接下來的兩年中進行了幾次重大的修復工作。然而,結果非常出色。我們不僅設法構建了用於實時協作的引擎,而且還實現了一個完整的終端使用者解決方案,該解決方案可以驗證哪些是理論工作。高階衝突解決 切換到樹資料模型 還不足以實現防彈實時協作。我們很快意識到基本的操作集(插入 ,刪除 ,設定屬性 )不足以以優雅的方式處理現實場景。雖然這三個操作可能提供了足夠的語義來實現線性資料模型中的衝突解決,但它們並不滿足富文字編輯的語義。以下是使用者同時對內容的同一部分執行操作的一些示例:
(1)當使用者B 拆分該列表項時,使用者A 更改列表項型別(從HTML專案符號到編號)(2)使用者A 和使用者B對同一段確認按Enter
(3)當使用者B對一個 段落按下Enter確認時,使用者A在 段落中加入塊引用
(4)使用者A 新增一個句子的連結,而使用者B修改 該句子文字部分
(5)使用者A 新增一些文字的連結,而使用者B 刪除該文字的一部分,然後撤消刪除
為了正確處理這些和許多其他情況,我們需要大力增強我們的運算轉換演算法。我們做的最重要的增強是向基本三(insert ,remove ,set屬性 )新增一組新操作。目標是更好地表達任何使用者更改的語義。反過來,這使我們能夠實施更好的衝突解決演算法。對於我們新增的基本三項操作:
- 在重新命名 操作,處理元素的重新命名(使用,例如,改變一個段落為標題或列表項)。
- 該分割,合併,包裝,拆開包裝 操作,以更好地描述使用者的意圖。
- 所述插入文字 操作,插入文字內容和元素之間進行區分。
- 與解決衝突無關,我們還引入了標記 操作。
為什麼我們需要這些新業務?重新命名,分割,合併,包裹 和解開 的“行動”可以通過一個組合來執行插入,移動 和刪除 操作。例如,拆分段落可以表示為一對“ 插入 新段落”+“ 將 舊段落的一部分移動 到新段落”。但是,拆分 操作是以語義為中心的 - 它傳達了使用者的意圖。這意味著不僅僅是插入+移動 恰好一個接一個地執行。
由於新的操作,我們可以編寫更多的上下文轉換演算法。這樣我們就可以解決更復雜的用例,例如上面描述的場景(1-4)。
旁註:我們認為必要操作集與您所表示的樹資料的語義緊密相關。富文字編輯器與族譜樹具有不同的性質,因此需要不同的操作集。 進一步擴充套件 新增新操作仍然無法解決所有問題。我們需要進一步擴充套件我們的運營轉型實施,以處理我們多年來發現的情景。以下是我們最重要的補充:
- graveyard root -一種特殊的資料樹的根,其中移除的節點被移動,這使得能夠在更好的方案解決衝突時使用者A 改變資料的部件,它是在由去除的同時使用者B (方案(5)和類似的)。
- 推廣操作以處理範圍而不是奇異節點,以獲得更好的處理和記憶體效率。
- 操作中斷 - 有時,在轉換時,操作需要分成兩個操作,例如當一部分內容被刪除時(方案(5))。
- 選擇性撤消機制 - 撤消功能需要了解協作編輯,因此,例如,使用者只能撤消自己的更改。