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的操作符,比如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);
到此为止,所有的技术论证都已完成,下一步就是现实整个系统了。
kityminder-editor + MongoDB 思维导图数据自动实时保存方案的更多相关文章
- Web思维导图实现的技术点分析(附完整源码)
简介 思维导图是一种常见的表达发散性思维的有效工具,市面上有非常多的工具可以用来画思维导图,有免费的也有收费的,此外也有一些可以用来帮助快速实现的JavaScript类库,如:jsMind.KityM ...
- jsMind思维导图模式展示数据
效果图: jsmind组件下载地址:https://files.cnblogs.com/files/fengyeqingxiang/jsmind.zip 后端代码,此处以C#编写的后台,Java或其他 ...
- Java,面试题,简历,Linux,大数据,常用开发工具类,API文档,电子书,各种思维导图资源,百度网盘资源,BBS论坛系统 ERP管理系统 OA办公自动化管理系统 车辆管理系统 各种后台管理系统
Java,面试题,简历,Linux,大数据,常用开发工具类,API文档,电子书,各种思维导图资源,百度网盘资源BBS论坛系统 ERP管理系统 OA办公自动化管理系统 车辆管理系统 家庭理财系统 各种后 ...
- 轻松认识JVM运行时数据区域(使用思维导图)
下面是个人阅读周志明编写的深入浅出Java虚拟机做成思维导图的笔记,线条.颜色和图片的视觉印象比起单纯文字笔记好得太多了,文字笔记的枯燥以及硬性记忆我就不再多说,特别对于JVM这块略微有点枯燥的知识, ...
- effective c++ 思维导图
历时两个多月的时间,终于把effective c++又复习了一遍,比较慢,看的是英文版,之前看的时候做过一些笔记,但不够详细,这次笔者是从头到尾的翻译了一遍,加了一些标题,先记录到word里面,然后发 ...
- JavaSE教程-02Java基本语法-思维导图
思维导图看不清楚时: 1)可以将图片另存为图片,保存在本地来查看 2)右击在新标签中打开放大查看 1.注释 定义:用于解释说明程序作用的文字 注释类别 单行注释 格式: //注释文字 多行注释 格式: ...
- Java基础(含思维导图)
很早之前整理的Java基础的一些知识点,思维导图: 1.'别名现象' 对一个对象赋值另一个对象,会指向新的对象引用,赋值前的对象引用会由于不再被引用而被gc回收: 而基本类型则不同.基本类型存储了实际 ...
- 浅谈Java的主要学习要点_上海尚学堂java培训课程思维导图
Java是一种可以撰写跨平台应用程序的面向对象的程序设计语言.Java 技术具有卓越的通用性.高效性.平台移植性和安全性,广泛应用于PC.数据中心.游戏控制台.科学超级计算机.移动电话和互联网,同时拥 ...
- C++第三章复习与总结(思维导图分享)
在完成了第三章的学习后,为了便于日后的复习整理,我制作了一张思维导图,有需要的可以自取. 函数的定义与使用 带默认值的函数 在C++中我们可以为函数添加默认的参数值,在调用时可不传入或部分传入参数,为 ...
随机推荐
- VO、DTO、DO、PO
领域模型中的实体类可细分为4种类型:VO.DTO.DO.PO. PO(Persistent Object):持久化对象,表示持久层的数据结构(如数据库表): DO(Domain Object):领域对 ...
- spring IOC与AOP
Spring IOC容器 spring IOC 容器有两种,分别是 BeanFactory 容器和 ApplicationContext 容器. BeanFactory如下: /*第一步,利用Clas ...
- rs(0)与rs("字段名")的区别
rs(0)与rs("字段名")的区别 注意:rs是RecordSet对象 前者是按sqlyu语句读出来的记录急的先后顺序命名的, rs(0)就是你select ...
- 财务平台亿级数据量毫秒级查询优化之elasticsearch原理解析
财务平台进行分录分表以后,随着数据量的日渐递增,业务人员对账务数据的实时分析响应时间越来越长,体验性慢慢下降,之前我们基于mysql的性能优化做了一遍,可以说基于mysql该做的优化已经基本上都做了, ...
- C++数组的初始化
来源:https://zhidao.baidu.com/question/380723280.html int a[]={1,2,3}; 这种方式初始化,大括号里写了几个元素那么数组里就有几个元素,相 ...
- Redux中间件组合方法
中间件 https://redux.js.org/glossary#middleware 中间件在发送端 action端点 和 处理方reducer的端点之间. 主要负责将async action转换 ...
- go语言圣经练习
练习 3.10: 编写一个非递归版本的comma函数,使用bytes.Buffer代替字符串链接操作. package main import ( "fmt" "os&q ...
- java 注意事项---避免踩坑
1.......对象参数接收不能大写
- python操作redis命令
Python操作redis from redis import StrictRedis, ConnectionPoolredis_url="redis://:xxxx@112.27.10.1 ...
- delphi
其实现在哪里还有delphi,我之所以建立这个分类,只是为了纪念它,纪念我使用了一年delphi的经历. 那个年代c#刚出道,delphi还是c/s编程届数一数二的人物.但是没想到,这么快被c#打败了 ...