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); 

到此为止,所有的技术论证都已完成,下一步就是现实整个系统了。

原文地址:https://www.cnblogs.com/star91/p/kitymindereditor--MongoDB-si-wei-dao-tu-shu-ju-zi-.html