最近開始做自己的第一個開源項目:一個基於思維導圖的測試用例管理系統MinderCase,在做了一周的技術調研后,決定采用kityminder-editor作為思維導圖編輯器,為了支持實時存儲,當思維導圖內容變化時使用JSON-Patch計算出內容變化產生的diffPatches,然后將diffPatches傳給后台映射為對應的MongoDB操作符,執行更新操作
JSON-Patch是用來描述JSON數據變化的一種形式。使用它可以避免在JSON數據只發生部分修改時發送整個文檔。與HTTP Patch方法是天造地設的一對,它允許以符合標准的方式對HTTP API進行部分更新。
kityminder-editor使用JSON結構來存儲數據,每個節點包括一個data字段表示節點本身的屬性,children數組包含該節點的子節點,依次遞歸嵌套。在編輯思維導圖的時候,最多的動作就是對節點的增、刪、改,對於這種樹形結構,借鑒傳統關系型數據庫的設計思路,可以使用節點引用的存儲方法,參考MongoDB樹形結構建模,但是在很多情況下,節點的增刪查改會會引起大量的數據庫操作,因此可以充分發揮MongoDB的優勢,把整個JSON數據存儲在一個document中。最終的存儲結構很簡單,除了數據庫自動生成的_id
和_v
字段,只有data
字段表示完整的思維導圖數據。
{
"_id": ObjectId("5b589668eac4d118dabf9546"),
"data": {
"root": {
"data": {
"id": "bn5xbxbefk00",
"created": 1532532217325,
"text": "思維導圖"
},
"children": [
{
"data": {
"id": "bna5hhbc6xc0",
"created": 1532961461384,
"text": "分支主題"
},
"children": [
]
}
]
},
"template": "default",
"theme": "fresh-blue",
"version": "1.4.43"
},
"__v": NumberInt("0")
}
當思維導圖內容發生變化時,會觸contentchange
事件,在這個事件中我們使用exportJson
方法導出當前思維導圖的JSON結構數據,與上一次保存的數據進行JSON Diff,生成diffPatches,如下所示,刪除一個節點引發的數據變化,可以使用diffPatches操作數組表示。
window.editor.minder.on("contentchange", () => {
const newMinderData = minder.exportJson();
const diffPatches = compare(oldMinderData, newMinderData);
this.minderData = newMinderData;
diffPatches.length > 0 &&
axios
.patch(`/minder/${id}`, diffPatches)
.then(function(response) {
oldMinderData = newMinderData;
console.log(response);
})
.catch(function(error) {
console.log(error);
});
});



后台在接收到前端傳來的diffPatches
后,將其映射為MongoDB的操作符,比如add
和replace
操作可以映射為$set
操作,remove
可以映射為$unset
操作,目前為止,我發現kityminder-editor的操作引發的diff僅限這三種操作。
const diffPatches = ....;
const opMap = {
add: "$set",
replace: "$set",
remove: "$unset"
};
const diffPatches = ctx.request.body; //
const update = {};
diffPatches.reduce((prev, cur) => {
const { op, path, value = 1 } = cur;
if (!prev[opMap[op]]) {
prev[opMap[op]] = {};
}
prev[opMap[op]][`data${path.replace(/\//g, ".")}`] = value;
return prev;
}, update);
實踐過程中發現remove
操作映射成$unset
操作會有一些問題,當進行刪除操作是,對應的patch操作是沒有value的,上面給了一個默認值為1,該操作映射為$unset
執行后,對應對象字段或者數組元素會變成null,如下圖所示。

實踐過程中發現對象字段變為null
,如上圖中的priority
字段變成null
,對思維導圖的渲染不會產生影響,但是子節點數組children
數組中包含null
元素會導致思維導圖加載失敗。實際上MongoDB對於數組的元素的刪除有其對應的操作符$pull,遺憾的是該操作符並不能根據數組索引來刪除數組,關於這方面的具體問題可以參考In mongoDb, how do you remove an array element by its index,總的來說,目前沒有好的方法解決這個問題,即使我嘗試修改產生diffPatches
的代碼,給remove
操作添加上value
字段,當一個操作引發對個patch時,在更新數據庫時可能會發生沖突導致更新失敗。因此只能“曲線救國”,在讀取思維導圖數據時,遞歸遍歷所有節點的子節點數組,過濾除null
值,這樣在渲染整個思維導圖數據時就不會發生錯誤。考慮到讀取數據的實時性要求不高,而修改數據實時保存實時性要求比較高,這個方案還是可以接受的。
function isValidArr(arr) {
return Array.isArray(arr) && arr.length;
}
function removeInvalidChild(node = {}){
const { children } = node || {};
if(isValidArr(children)){
node.children = children.filter((item) => {
removeInvalidChild(item);
return item;
})
}
}
removeInvalidChild(root);
到此為止,所有的技術論證都已完成,下一步就是現實整個系統了。