mxgraph整合vue入门之画布持久化

前言

很多时候,我们拖拽进画布中的组件,代表着我们的领域模型或者其他实体,我们希望通过mxgraph描述这些实体之间的联系,那么这个状态就需要保存
环境: mxgraph 4.2.2和vue 2.6

mxgraph画布格式

mxGraph

mxGraph代表着整个画布对象实体,画布是否能移动 isPanning()/画布是否可用 enabled() 等等…
也就是说画布的状态/行为等都放在mxGraph这个类中,它比较泛化,它是对当前画布抽象的一个概念

mxGraphModel

mxGraphModel,顾名思义,是mxGraph对应的model,即模型,也是begin-endUpdate操作的对象

let model = yourgraph.getModel()

model.beginUpdate()
try{
    ...画布操作
}finally{
    model.endUpdate()
}

你可以把它看做一个最外层的mxCell,类似前端最外层wrapper,它负责存储画布的数据结构. 它放在一个初始化画布的model属性中,可以通过getModel()获取

/**
 * Variable: model
 * 
 * Holds the <mxGraphModel> that contains the cells to be displayed.
 */
mxGraph.prototype.model = null;

/**
 * Function: getModel
 * 
 * Returns the <mxGraphModel> that contains the cells.
 */
mxGraph.prototype.getModel = function()
{
	return this.model;
};
mxCell

Cell顾名思义,细胞/组件,是构成一个画布的基本元素,在我对mxgraph的实际使用过程中与mxCell打交道最多,它是我们领域模型的抽象,比如我第一期写的汽车,那么画布中的一个mxCell就代表一辆汽车,上面讲到,mxGraphModel是mxCell的存储位置

/**
 * Variable: cells
 * 
 * Maps from Ids to cells.
 */
mxGraphModel.prototype.cells = null;

源码中的确如此,在mxGraphModel中有一个属性叫做cells,里面存放着现有的mxCells集合.

mxCell根据其类型又分为节点型连线型,可以在插入mxCell时指定你使用的是vertex还是edge.

mxGraph.prototype.insertVertex = function(){...}
mxGraph.prototype.insertEdge = function(){...}

上面两个插入语句是有返回值的,分别返回两种mxCell,标识两种不同组件的字段如下:

节点型vertex

vertex

连线型edge

edge

而连线型的mxCell往往指定了连接谁和谁,即起点source和终点target.

/**
 * Variable: source
 *
 * Reference to the source terminal.
 */
mxCell.prototype.source = null;

/**
 * Variable: target
 *
 * Reference to the target terminal.
 */
mxCell.prototype.target = null;

下图中,分别用到了两种节点,论坛名是vertex,红色连线是edge:

dragexample

mxgraph的用户数据域

官方文档中提到,我们可以在插入insertVertex的时候将自定义的数据放入mxCell中:

try{
    let cell = yourgraph.insertVertex(parent,id,{你的数据对象},,,,,样式,是否相对布局)
    ....
}finally{...}

//在使用时
cell.value.你放进去的属性...

也就是说你可以在insertVertex的value入参的位置指定,但是要注意,这个时候你就要重写使用到了value属性的地方,比如说画布显示的标签方法(就是上面图中的CSDN和博客园字样),默认是value:

mxGraph.prototype.convertValueToString = function(cell)
{
   var value = this.model.getValue(cell);
   
   if (value != null)
   {
       //value如果是xml节点,显示的是节点名称
      if (mxUtils.isNode(value))
      {
         return value.nodeName;
      }
       //value如果是字符或者方法,那就返回toString
      else if (typeof(value.toString) == 'function')
      {
         return value.toString();
      }
   }
   
   return '';
};

但是没有if分支判断如果value是object怎么办,那你的标签可能就变成了[object object]这个样子,那你必须重写convertValueToString,改成你放入的对象.name.所以我在开发中使用自定义的数据域(属性),防止mxgraph内部使用value属性冲突:

try{
    let cell = yourgraph.insertVertex(parent,id,value,,,,,样式,是否相对布局)
    cell.userData = 你的数据
    ....
}finally{...}

这里利用insertVertex的返回值是我们插入画布的mxCell对象,而且当前执行过程还在一次begin-endUpdate之中,所以我们可以直接修改cell,给我们的自定义组件增加一个自定义的属性userData,避开mxgraph的保留字,防止使用出错.

mxgraph编码译码工具

mxgraph画布提供的持久化api
函数声明参数表
mxUtils.getXml()node-xml节点 linefeed-换行符是否要转换,默认转成&#\x\a;
mxUtils.getPrettyXml()node-xml节点, tab-制表符替换符, indent-缩进替换, newline-换行替换, ns-生成的xml的名称空间

这两个函数都是输出xml节点,接下来介绍如何得到xml节点,引入mxgraph的Codec机制

mxgraph的Codec机制

codec这个单词的意思是编码译码器,对应的操作有两种,编码encode和解码decode

编码就是把画布变成代码,解码就是把读到的文本再重新复现成画布,这两个过程对应着我们存画布和读取画布,无论目标地址是redis/mysql或者其他位置.而这里的代码就是xml

编码解码

官方demo使用方法,我这里用elementUi+vue中的一个按钮实现:

// 初始化一个编码器
let tool = new mxCodec()
// 让编码器对model域进行编码
let result = tool.encode(yourgraph.getModel())
// 调用getPrettyXml()对结果进行美化
console.log(mxUtils.getPrettyXml(result))

在这里插入图片描述

导出的格式如下

<mxGraphModel>
  <root>
    <mxCell id="0" />
    <mxCell id="1" parent="0" />
    <mxCell id="2" value="CSDN" style="fontSize=16;省略样式串" vertex="1" parent="1">
      <mxGeometry x="378.84375" y="70" width="200" height="100" as="geometry" />
    </mxCell>
    <mxCell id="3" value="博客园" style="fontSize=16;省略样式串" vertex="1" parent="1">
      <mxGeometry x="120.00375000000003" y="260" width="200" height="200" as="geometry" />
    </mxCell>
    <mxCell id="4" style="strokeColor=red" edge="1" parent="1" source="2" target="3">
      <mxGeometry relative="1" as="geometry" />
    </mxCell>
  </root>
</mxGraphModel>

首先根节点是整个mxGraphModel,然后包装节点root,然后是初始化画布插入的两个根节点,id为0和1,源码:

function mxGraphModel(root)
{
	this.currentEdit = this.createUndoableEdit();
	
	if (root != null)
	{
		this.setRoot(root);
	}
	else
	{
		this.clear();
	}
};

mxGraphModel.prototype.clear = function()
{
	this.setRoot(this.createRoot());
};

mxGraphModel.prototype.createRoot = function()
{
    // 第一个cell id=0
	var cell = new mxCell();
    // 给第一个cell直接insert第二个cell id=1
	cell.insert(new mxCell());
    // 然后返回上面的clear方法,setRoot把根节点设置为cell
	return cell;
};

接下来是用户数据部分,以CSDN节点为例:
节点名mxCell,id不传参默认自增,value和style是常量,在insertVertex时指定;
vertex=1表示它是一个节点类组件,parent=1对应上面的id=1,默认的父元素都是id为1的这个,除非你自己指定了要把组件插入到别的组件内;
然后子节点,mxGeometry,单词表示几何图形,后面就是它的参数,

<mxCell id="2" value="CSDN" style="fontSize=16;省略样式串" vertex="1" parent="1">
      <mxGeometry x="378.84375" y="70" width="200" height="100" as="geometry" />
</mxCell>

注意as属性,它表示当前节点是父节点mxCell的geometry属性,源码:

/**
 * Variable: geometry
 *
 * Holds the <mxGeometry>. Default is null.
 */
mxCell.prototype.geometry = null;

连线型的节点类似,edge=1表示它是连线型,它的位置必须在左右端点之后,先有CSDN博客园两个节点,它才能知道自己的sourcetarget分别指向谁.

<mxCell id="4" style="strokeColor=red" edge="1" parent="1" source="2" target="3">
      <mxGeometry relative="1" as="geometry" />
</mxCell>
自定义用户数据部分的编码方式

上面的例子没有用户数据,只有一个mxGeometry对象,那假如像上面的例子,我往userData中放了数据,它会是什么样子呢?

{
    "image": "static/mxgraph/images/cnblogs.svg",
    "name": "博客园",
    "width": 200,
    "height": 200
}

插入组件时,value是我数据中的name,而把整个对象放入userData中:

let cn = graph.insertVertex(graph.getDefaultParent(), null, item.name, offsetX+150, offsetY+150, width, height, "省略样式", false);
// item赋值到用户指定的属性userData中
cn.userData = item

再次调用编码过程,结果如下:

<mxGraphModel>
  <root>
    <mxCell id="0" />
    <mxCell id="1" parent="0" />
    <mxCell id="2" value="CSDN" style="样式串" vertex="1" parent="1">
      <mxGeometry x="490.00374999999997" y="30" width="200" height="100" as="geometry" />
      <Object image="static/mxgraph/images/csdn.svg" name="CSDN" width="200" height="100" as="userData" />
    </mxCell>
    <mxCell id="3" value="博客园" style="样式串" vertex="1" parent="1">
      <mxGeometry x="210.00374999999997" y="230" width="200" height="200" as="geometry" />
      <Object image="static/mxgraph/images/cnblogs.svg" name="博客园" width="200" height="200" as="userData" />
    </mxCell>
    <mxCell id="4" style="strokeColor=red" edge="1" parent="1" source="2" target="3">
      <mxGeometry relative="1" as="geometry" />
    </mxCell>
  </root>
</mxGraphModel>

可以看到,因为mxCell.userData中放了我的数据,所以Object标签中的as就叫做userData,与geometry属性平级.那假如我的项目中规定了某种数据格式呢,例如:

function Custom(item){
    this.name = item.name
    this.width = item.width
    this.height = item.height
}

然后我对应的赋值语句变成

cn.userData = new Custom(...)

会怎么样?结果是什么都没有
nothing

你会发现刚刚通过普通花括号放进去的Object是可以被编码的,而使用了某个构造方法之后就不行了,这是因为未指定如何对自定义类进行编码,mxGraph的codec遇到你的Custom类时不知道怎么把它放进xml里,这时你就需要注册新的编码规则:

/*分别指定你的Custom类如何编码如何解码*/
let customCodec = new mxObjectCodec(new Custom())
customCodec.encode = function (codec, obj) {
    //编码时创建Custom标签,里面放JSON串
    let component = codec.document.createElement('Custom')
    mxUtils.setTextContent(component, JSON.stringify(obj))
    return component
}

customCodec.decode = function (decoder, node, into) {
    //解码时把节点内容转化为对象,调用Custom构造方法
    let content = JSON.parse(mxUtils.getTextContent(node))
    content = new Custom(content)
    return content
}
//然后通过注册工具注册到编码库里
mxCodecRegistry.register(customCodec)

这时我们再去执行:

<mxGraphModel>
  <root>
    <mxCell id="0" />
    <mxCell id="1" parent="0" />
    <mxCell id="2" value="CSDN" style="样式串" vertex="1" parent="1">
      <mxGeometry x="411.84375" y="20" width="200" height="100" as="geometry" />
      <Custom as="userData">
        {"name":"CSDN","width":200,"height":100}
      </Custom>
    </mxCell>
    <mxCell id="3" value="博客园" style="样式串" vertex="1" parent="1">
      <mxGeometry x="609.84375" y="226" width="200" height="200" as="geometry" />
      <Custom as="userData">
        {"name":"博客园","width":200,"height":200}
      </Custom>
    </mxCell>
    <mxCell id="4" style="strokeColor=red" edge="1" parent="1" source="2" target="3">
      <mxGeometry relative="1" as="geometry" />
    </mxCell>
  </root>
</mxGraphModel>

按照我们想要的格式输出了,到此就成功了(注意给你的类预留空构造器,因为上面有new Custom()).
那后续的操作就是大家随意组合了,把生成的xml传到后端去进行xml的解析,或者直接一整个保存下来,我的处理方法:

①传到后端
②xml解析,提取mxCell标签,然后按照id顺序把mxCell放到mysql和redis中

下面是我把画布放入mysql中的样子,data_key是我自己指定的排序规则,data_value就是xml标签的原样内容:
在这里插入图片描述

解码

解码不再赘述,与编码过程相反,调用:

mxCodec.prototype.decode = function(node, into){....}

即可,前面的参数是编码的xml文本,后面的参数是哪个画布的model或者其他地方,这里顺带提一下,你可以只持久化某个mxCell组件,encode的时候不用传入一整个model,只传某个组件也是可以的,得到的xml串就只有你要的mxCell标签.

那这期就到这里了,任何疑问可以私信或留言,欢迎交流!

画布总是难看而且还不听话,下期更新:画布行为控制和默认样式修改

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值