kityminder-editor + MongoDB 思維導圖數據自動實時保存方案


最近開始做自己的第一個開源項目:一個基於思維導圖的測試用例管理系統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的操作符,比如addreplace操作可以映射為$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); 

到此為止,所有的技術論證都已完成,下一步就是現實整個系統了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM