是一款線上腦圖産品,由 腦圖編輯 和 多人協作 兩部份主要功能所組成。自 2018 年 09 月立項至今經曆大大小小 24 個版本打磨終于迎來 1.0 正式版本。
架構大圖
從架構大圖中我們可以看到 UMind 依賴于 GGEditor,是一個前端全棧産品。其中 Node 部分主要用于多人協同操作時的資料同步,後面章節中會詳細講解。最上一層的産品端則可通過 UMind 提供的 PASS 服務能力獨立部署腦圖産品。如果你想要馬上嘗鮮多人協同功能,現在可以登入 Cloud Mind 平台進行體驗,年後 Team File 也将接入 UMind,屆時 UMind 将會服務于集團所有的小夥伴:)
可視化圖編輯器
說起 UMind 就必須提到
GGEditor,曾經有很多小夥伴質疑過 GGEditor 的項目名,其實 GGEditor 的全稱是 Great Graphic Editor,是不是突然感覺上高大上了不少 :)
流程圖 | 腦圖 | 拓撲圖 |
GGEditor 基于 Antv
G6與 React,提供流程圖、腦圖、拓撲圖的可視化編輯能力。集團内部我們專注于技術賦能,起初用于 Schema 編輯器、AIBoost 流程化搭建項目,目前已經超過 25+ 項目接入,集團外部我們在 GitHub 上的 Star 數已超過 650+,并有幸入選開源中國
2018 年度國産新秀榜。
多人協作版腦圖
可能大家之前會感覺多人協作的實作比較複雜,但是真正實作起來無非需要解決以下兩個問題:
- 操作資料的實時同步
- 操作資料的沖突處理
操作轉換
首先我們需要将使用者的操作行為轉換為對應的操作模型以友善後繼的操作資料同步與沖突處理,格式如下:
{
"command": {
"name": "INSERT / DELETE / UPDATE / MOVE",
"data": {
"id": "",
"model": {...}
}
}
}
name
字段表示操作行為,分别對應插入、删除、更新、移動四種使用者操作行為。
data
字段包含操作資料,其中
id
用來辨別節點,
model
則是節點資料模型。
資料同步
這裡講的資料同步分為兩層:
- 第一層是浏覽器與 Node 伺服器之間的雙向通信
- 第二層是存在多台 Node 伺服器之間的資料同步
第一層可以通過 WebSocket 協定實作雙向通信,我們使用的是
Socket.IO類庫。第二層則是能過 MetaQ(對應
MQ産品)來保障分布式多伺服器之間的消息廣播。
沖突處理
相較于富文本編輯器的多人協作,得利于腦圖每個節點的唯一辨別,使得沖突的處理能夠更加的簡單。
實作思路
上圖所示便是最常見的沖突例子:A 使用者與 B 使用者對同一節點同時執行更新與删除操作,C 使用者則需要同步操作。
為了達到所有使用者最終結果統一,每當同步操作資料後需要根據被操作節點的狀态辨別來決定是否同步或舍棄操作:
- A 使用者需要同步删除操作
- B 使用者需要舍棄更新操作
- C 使用者如果先同步删除操作(廣播消息無法保證順利)則也需舍棄後續的更新操作。
那麼應該如何辨別各節點狀态呢?還記得在這之前我們已經定義過了使用者的基本操作行為,再配合使用者執行操作時的時間戳便能夠辨別各節點的狀态。這裡以 B 使用者為例,執行删除操作之後的節點狀态如下:
{
"NODE01": {
"INSERT": 100000000001,
"DELETE": 100000000003
}
}
随後 B 使用者接收到更新操作廣播:
{
"command": {
"name": "UPDATE",
"data": {
"id": "NODE01",
}
},
"t": 100000000002
}
經過對比
DELETE
與
UPDATE
的操作時間,最終決定舍棄本次更新操作,至此沖突解決。
實作細節
上個章節我們大體介紹了沖突處理的實作思路,但是實際開發過程中遇到的問題往往更加複雜,這個章節我們就來聊聊沖突進行中的實作細節。
分解移動操作
節點的移動操作可以看作是删除操作與插入操作的組合,最終隻需記錄下插入操作的時間戳以備沖突處理時使用。
删除父級節點
當删除一個父級節點時不僅需要辨別該節點的删除狀态,也需同時辨別其後繼子節點為删除以備沖突處理時使用。
更新多級屬性
更新節點較于插入與删除更為複雜,因為會涉及到節點預設的主題樣式,是以需考慮多層屬性合并沖突處理。
如上圖所示:
- A 使用者選擇了預設樣式(文字顔色:紅色,文字大小:14,節點背景:白色)
- B 使用者更新了節點樣式(文字顔色:藍色)
然後經過沖突處理,希望得到的結果是:
- A 使用者在原有的預設樣式基礎上合并 B 使用者的更新操作。
- B 使用者則需要同步到除文字顔色之外 A 使用者的更新操作。
為了能夠實作以上這種沖突處理的效果,我們需要分别記錄各個屬性的操作時間,收到更新廣播之後再依次對比進行取舍。這裡我們以 B 使用者為例來講解具體實作:
B 使用者更新字型顔色之後的結點狀态:
const NODE_STATUS = {
NODE01: {
UPDATE: {
'textStyle.fill': 100000000002
}
}
};
緊接着收到了 A 使用者發出的更新廣播:
const message = {
command: {
name: "UPDATE",
data: {
id: "NODE01",
model: {
textStyle: {
fill: "red",
fontSize: 14
},
rectStyle: {
fill: "white"
}
},
paths: [
"textStyle.fill",
"textStyle.fontSize",
"rectStyle.fill"
]
}
},
t: 100000000001
};
開始依次對比擷取真正需更新的屬性:
使用符号分割屬性層級的好處:能夠在
.
方法中直接當作參數使用
pick
const { command, t } = message;
const { id, model, paths } = command.data;
const updatePaths = [];
paths.forEach((path) => {
if (NODE_STATUS[id]['UPDATE'][path] > t) {
return;
}
updatePaths.push(path);
});
console.log(_.pick(model, updatePaths));
寫在最後
UMind 1.0 版本釋出可以視為從 0 到 1 的過程,這裡特别感謝
Antv團隊 @蕭慶 @有田 的支援。接下來無論在腦圖功能的打磨還是多人協同的探索方面我們都還有很多的路要走,如果正巧看到這篇文章的你有任何意見或是興趣都歡迎通過以下方式來聯系我們。
履歷投遞:[email protected]
業務接入: