Figma 是如何做協(xié)同編輯的?
大家好,我是前端西瓜哥。
我一直對圖形編輯器如何做多人協(xié)同編輯很感興趣,最近讀了 Figma 前 CTO Evan Wallace 的文章《How Figma’s multiplayer technology works》,很有收獲,于是寫了這篇筆記。
我建議讀者直接閱讀原文,里面還有動圖。
https://madebyevan.com/figma/how-figmas-multiplayer-technology-works/
參考 CRDT
協(xié)同編輯,需要用到數(shù)據(jù)一致性算法,目前成熟的算法有 OT 和 CRDT。
Figma 沒用 OT,太復(fù)雜,尤其是當(dāng)離線數(shù)據(jù)本地緩存了很久才提交時,會進(jìn)行復(fù)雜的 OT 算法計算,產(chǎn)生組合爆炸問題。
CRDT,也有一定復(fù)雜度,而且是去中心的,F(xiàn)igma 還是需要一個中心服務(wù)實現(xiàn)鑒權(quán)功能。
OT 和 CRDT 更多是針對富文本編輯的,而 Figma 是設(shè)計工具,作者認(rèn)為沒有必要引入這些復(fù)雜的東西,這樣會讓項目難以維護(hù)。
Figma 最終選擇借鑒 CRDT 的思想,自己實現(xiàn)一套協(xié)同系統(tǒng)。
這里我比較贊同,我永遠(yuǎn)認(rèn)為 “不要過早擴(kuò)展”,能簡單就不要復(fù)雜。
因為一些后期不一定會用到的功能,強(qiáng)行做了更復(fù)雜的抽象和擴(kuò)展,導(dǎo)致功能開發(fā)的心智負(fù)擔(dān)過重,當(dāng)發(fā)現(xiàn)這些后期功能不需要,并且要擴(kuò)展另一個方向的一套功能時,原本抽象的設(shè)計變得毫無意義,且一切都積重難返,最后的結(jié)果只能是屎上雕花了。
沖突處理
Figma 的設(shè)計文件的數(shù)據(jù)是一棵圖形樹,圖形之間可能會有父子關(guān)系,比如一個 group 下有一個 rectangle,形成多層的樹結(jié)構(gòu)。協(xié)同編輯操作的對象就是這么一棵樹。
Figma 協(xié)同操作的最小原子是 對象的屬性。
修改同一個對象的不同屬性沒有沖突問題。
多個用戶同時修改同一個對象的相同屬性時,最晚提交到服務(wù)端的值會覆蓋其他用戶的值,包括文本內(nèi)容。
假設(shè)一個屬性的值是 B,一個用戶修改為 AB,另一個用戶修改為 BC,最終同步后,他們不會得到 ABC,只會是 AB ,或者 BC,看誰最晚提交。
這個其實在大多協(xié)同表格應(yīng)用也是類似的,單元格的內(nèi)容也是最后提交者優(yōu)勝,只有富文本文檔才要求得到 ABC。
處理閃爍現(xiàn)象
首先要明確 Figma 協(xié)同編輯的基本要求:
- 可以本地立即修改,而不是提交后再更新,這是為了有絲滑的用戶體驗,同時也能支持離線編輯能力;
- 使用中心服務(wù),而不是去中性化(說你呢 CRDT),F(xiàn)igma 的服務(wù)端會維護(hù)圖形樹,作為最終的權(quán)威,并負(fù)責(zé)修正用戶提交的數(shù)據(jù)。
當(dāng)多個用戶同時修改同一個對象屬性時,服務(wù)端返回的有沖突的屬性值如果立即給對象應(yīng)用上,可能會有 “閃爍” 現(xiàn)象。
是這么一個場景,在同一時間,用戶 A 將圖形改成紅色(本地改成紅色然后提交到服務(wù)器),用戶 B 改成黃色,用戶 B 比用戶 A 更早提交到服務(wù)器。
對于用戶 A,他會先看到顏色從紅色變成黃色,黃色再變成紅色,這種不期望的 “閃爍” 現(xiàn)象。
解決方式是,用戶 A 提交將顏色改成紅色的操作,要等待服務(wù)端確認(rèn)。在等待服務(wù)端確認(rèn)期間,如果收到其他用戶修改同一個屬性的操作(用戶 B 改成黃色),會把這個改動 丟棄。
之后用戶 A 收到服務(wù)端的確認(rèn)消息后,如果此時有個用戶 C 修改圖形為紫色的操作同步過來,就會走正常的流程,將圖形改成紫色。
創(chuàng)建與刪除
創(chuàng)建類似前面的做法,也是最后寫入者優(yōu)勝。(沒理解)
對于刪除操作,F(xiàn)igma 服務(wù)器不會保存被刪除的數(shù)據(jù),這么做是為了防止文檔大小持續(xù)增長。
被刪除的數(shù)據(jù)由進(jìn)行刪除操作的客戶端負(fù)責(zé),該客戶端可通過 undo(撤銷)恢復(fù)。
系統(tǒng)需要保證 id 的一致性。
做法是給每個客戶端分配一個唯一 id,將其作為新創(chuàng)建對象 id 的一部分。這樣兩個客戶端就不會生成相同的對象 id 了。(這有點像雪花算法)
更改對象的父元素
修改對象的位置是 Figma 系統(tǒng)中最復(fù)雜的部分。
其復(fù)雜度來自移動一個對象到另一個父節(jié)點操作。需要做到:
- 該移動操作不和該對象的其他無關(guān)屬性沖突;
- 并發(fā)的兩個操作不會導(dǎo)致一個對象同時在多個父元素下。
很多做法是 “刪除+重新創(chuàng)建” 表示對象的移動,但這會導(dǎo)致 id 的改變,對 Figma 并不合適。
Figma 最后選擇給對象加一個屬性,指向它的父節(jié)點。這樣 id 得以保持不變,多個用戶同時進(jìn)行操作只是在改這個屬性,也有效避免了副本的出現(xiàn)。
副本指的是,兩個用戶同時分別把一個圖形放到不同的父節(jié)點上,如果用的是修改 children 數(shù)組的方式,就會導(dǎo)致兩個父節(jié)點都掛著同一個圖形的引用。
然后還有一個 “環(huán)” 的問題,假設(shè) B 和 C 是兄弟節(jié)點,一個用戶將 B 放到 C 下,另一個用戶把 C 放到 B 下,就會產(chǎn)生一個環(huán)。
解決方法是,最先改變父子關(guān)系,會作為最終狀態(tài)。假設(shè)用戶 1 將 C 放到 B 下的操作先到服務(wù)器,服務(wù)器會應(yīng)用它。此時服務(wù)器收到用戶 2 把 B 放到 C 下的同步信息,服務(wù)器會將其駁回,帶上真正的父節(jié)點 id。
在駁回前,用戶 2 其實收到了用戶 1 的操作,客戶端此時會將 A 和 B 臨時形成環(huán),然后移出圖形樹,接著駁回的信息回來,客戶端就能確定父節(jié)點,然后恢復(fù)到圖形樹中。
該方法并不是非常好,因為圖形消失了一段時間,但方案比較簡單,且這種場景非常罕見,F(xiàn)igma 不打算用更復(fù)雜的方案。
順序一致性
如果多個用戶同時修改一個節(jié)點下的兄弟節(jié)點的位置,如何保證它們的最終順序是一致的?
Figma 使用了 “Fractional Indexing”(小數(shù)索引) 技術(shù)。
兄弟節(jié)點會分配一個大于等于 0,小于 1 的小數(shù)索引值。
插入新的節(jié)點,會取于它相鄰的兩個節(jié)點的索引值的中間位置,比如要在索引為 0.3 和 0.4 的中間插入新節(jié)點,這個節(jié)點的索引值會標(biāo)記為 0.35。
如果出現(xiàn)索引值相同的情況,服務(wù)端會進(jìn)行糾正,把更晚提交的新節(jié)點的索引往后移動一點。
實現(xiàn)撤銷(undo)
單機(jī)的 undo,是將狀態(tài)會恢復(fù)到上一個時間點,如果不加以改變,換成多人協(xié)同,就會導(dǎo)致當(dāng)前用戶的操作在其他用戶撤銷時被覆蓋。
Figma 團(tuán)隊總結(jié)了一個重要的準(zhǔn)則:撤銷后復(fù)制了一些東西,然后重做到當(dāng)前位置,文檔不應(yīng)該被改變。
Figma 的做法是 改歷史記錄。
Figma 會在用戶撤銷的時候修改重做歷史,以及在重做的時候修改撤銷歷史。
用戶 A 和用戶 B 都打開一張圖紙,其中一個圖形原來是紅色。用戶 A 將其更換為藍(lán)色,同步,此時雙方都看到圖形是藍(lán)色。
此時用戶 B 又將圖形改成黃色,同步,此時雙方都是黃色的。。
用戶 A 進(jìn)行撤銷操作,撤銷為紅色(因為撤銷棧記錄的是紅變藍(lán)),此時重做棧的命令對象跑到重做棧,本來應(yīng)該是藍(lán)變紅,但是 最新的文檔狀態(tài)是黃色,所以這里強(qiáng)行把替換為黃變紅。
這樣歷史記憶就被篡改了,可以保證重做后能回到最新狀態(tài)。
對于用戶 B,則不需要修改,因為他的歷史記錄是就是紅變黃(黃是最終狀態(tài))。
要點
最后是作者的一些心得:
- CRDT 的文獻(xiàn)很有參考價值,即使你不打算做非中心化協(xié)同;
- 可視化編輯器的協(xié)同編輯并沒有想象中難做;
- 在開做之前先調(diào)研并實現(xiàn)原型是非常有價值的。
結(jié)尾
文章看下來,大概有一些圖形編輯器如何做協(xié)同編輯的概念了,以后有機(jī)會實踐一下。
其中一點我是非常贊同的,就是方案能簡單就不要復(fù)雜,我不是很喜歡一些高度抽象的東西,代碼是寫給人看的,只是順便讓機(jī)器執(zhí)行而已。