(由于业务模型本身是商业敏感数据,很多截图我就不放上来了)

What?

E-R方法是“实体-联系方法”(Entity-Relationship Approach)的简称。它是描述现实世界概念结构模型的有效方法,而ER图就是这个方法在界面上的可视化呈现。

无论是普遍的数据库驱动的开发方式,还是高大上的DDD,或者是模型驱动的Lowcode平台, 亦或主数据驱动的APaas平台,ER图提供了一种可视化业务的视角。

Why?

业务设计 ≈ 模型设计 ≈ 数据库设计

  1. 我们知道,一个好的APaas/Lowcode平台能充分隔离了技术复杂度和业务复杂度,而模型元数据作为系统输入参数(每个项目的模型字段不相同),ER图可以从这个角度上来度量一个系统的业务复杂度。

在业务复杂大型项目里面,一般会采用DDD的方法论来进行业务设计,而领域模型的设计是其中很重要的一部分,一般设计模型的人是领域专家和实现功能的开发者并不是一波人,可视化的ER图可能作为中间态产物成为了重要的交流工具。


而在中小型项目中,由于业务和界面交付简单,视图模型(VO),业务模型(BO),数据模型(DO) 倾向于三元合一,ER 图则充当数据库设计工具。

  1. 业务如此重要,想象一下,一般在项目KO或者评审期间,所有人聚在一起,基本上都会把ER图展开来共同讨论。对于一些中小型项目,ER图确定了,基本上业务逻辑,工作量都确定了,通俗的说,老程序员和架构师会把这个过程叫“项目开始编码之前严格把控数据库设计”。

定制化的ER图更有价值

  1. 任何可落地的APaas/Lowcode一定不是通用化的。平台建设,并非从0开始的,他是对本公司平台化之前的技术栈,项目交付过程的抽象,必然会明显偏向某些存量的业务领域。而正是由于结合了技术栈绑定和业务属性,平台才极具价值和竞争力(无可替代)。<br /> 因此会产生很多在本公司技术和业务生态里面约定成俗的概念元素(资源),以减少沟通成本。<br /> 由于平台是以模型为中心的,因此ER图是这些概念元素(资源)可视化表现的最佳舞台,需要对通用的ERD进行定制化扩展业务含义的元素,领域专家可以在上面进行业务创造。<br />

在线版本的powerdesigner

ER图设计工具有一个神器,就是sysbase的powerdesigner, 由于是如此普遍,有不少公司因为用了破解版被盯上要求购买license。
powerdesigner只所以流行,除了基本功能过硬外,最大的优点在于能够很好的支持元信息中文,这个是同类的ER图软件缺少的(原因是ER图软件都是海外公司开发的)。

powerdesigner的一个缺点是,只支持windows
可能是我孤陋寡闻,在mac上面没有找到很好的替代品….
所以, 做在线ER图,除了产品本身的需要外,恐怕这也是一个重要的原因。

How?

技术选型

SVG vs Canvas

以下摘录w3cschool的原文:

Canvas SVG
依赖分辨率 不依赖分辨率
不支持事件处理器 支持事件处理器
弱的文本渲染能力 最适合带有大型渲染区域的应用程序(比如谷歌地图)
能够以 .png 或 .jpg 格式保存结果图像 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)
最适合图像密集型的游戏,其中的许多对象会被频繁重绘 不适合游戏应用
  1. 相比SVGCanvas 更像是更底层的实现,同时 Canvas WebGL 的入口, 性能优化的空间更大。<br /> 对于对标powerdesignerweb版的ER图来说, “需要展示成百上千个模型”这个是最核心的功能 Canvas 成了必然的选择。但是,由于Canvas提供的是更底层绘图api, 缺乏上层封装 ,会导致开发体验和速度上过于原始 ,而 G6 作为一款图可视化引擎,可以弥补这里面的差距


踩坑和实践分享

连接线

ER图的连线, “字段” —- “模型”
连接点,在G6里面是通过”锚点” 这个概念来实现的

5AA91131-06CD-48A6-8BBD-C2FE93AF8848.png F8593258-BA3E-4F5F-B642-7774155DA7B4.png
连接点在左边 连接点在右边

“连字段上的锚点”,固定上有两个,分别放在一左一右.如何根据两个关联模型的相对方位自动选择左边还是右边

我的方案是在node:dragend事件里面根据相对方位做判断,修改dege的sourceAnchor的值:

  1. graph.on('node:dragend', (ev) => {
  2. const shape = ev.target
  3. const node = ev.item
  4. const edges = node.getEdges()
  5. const x = ev.x
  6. edges.forEach((edge) => {
  7. const sourceNode = edge.getSource()
  8. const targetNode = edge.getTarget()
  9. if (node === sourceNode) {
  10. const edgeModel = edge.getModel()
  11. const isTo = x < targetNode.getModel().x
  12. const i = edgeModel.fieldIndex
  13. const l = edgeModel.fieldsLength
  14. if (sourceNode !== targetNode) {
  15. graph.updateItem(edge, {
  16. sourceAnchor: !isTo ? i + 2 : 2 + i + l,
  17. // targetAnchor: isTo ? 0 : 1,
  18. })
  19. }
  20. } else {
  21. const edgeModel = edge.getModel()
  22. const isTo = sourceNode.getModel().x < x
  23. const i = edgeModel.fieldIndex
  24. const l = edgeModel.fieldsLength
  25. if (sourceNode !== targetNode) {
  26. graph.updateItem(edge, {
  27. sourceAnchor: !isTo ? i + 2 : 2 + i + l,
  28. })
  29. }
  30. }
  31. }) // ----获取所有的边

“连到模型上的锚点”应该没有固定的位置,而是应该在整个模型节点表面自动连接最近的锚点,否则连线会很不好看

BBB8B7C5-068A-4E1C-85F2-A3EE116C80D2.png 608DE2AA-3015-44CA-8A4D-2828EA38313D.png
旧版本“连到模型上的锚点”是固定在模型的两边 最新版本“连到模型上的锚点”会自动找到整个周边最接近的点

实现思路是,当我在edge没有设置锚点的时候,g6会自动选择最接近的锚点,因为我在整个模型图上面都设置了无数的锚点可供选择:

  1. getAnchorPoints(cfg) {
  2. const {
  3. config,
  4. data,
  5. } = cfg
  6. const {
  7. fields,
  8. } = data
  9. const h = config.headerHeight + getLength(fields.length) * config.fieldHeight
  10. return [[0, config.headerHeight / 2 / h], // 左上方
  11. [1, config.headerHeight / 2 / h], // 右上方
  12. ...fields.map((field, index) => {
  13. const x = 10 / config.width
  14. const l = config.headerHeight + config.fieldHeight * (index + 1) - config.fieldHeight / 2
  15. const y = l / h
  16. return [x, y]
  17. }), ...fields.map((field, index) => {
  18. const x = (config.width - 10) / config.width
  19. const l = config.headerHeight + config.fieldHeight * (index + 1) - config.fieldHeight / 2
  20. const y = l / h
  21. return [x, y]
  22. }),
  23. ...getTopAnch(50),
  24. ...getBottomAnch(50),
  25. ...getLeftAnch(100),
  26. ...getRightAnch(100),
  27. ]

上下左右的边界总共设置了300个锚点,并且均匀分布

布局算法选择

对于ER图来说,布局效果的好坏很影响整体的观感。g6 内置了各种各样的布局,到底哪一种最适合ER图呢?

B21C9A4F-566E-43AA-B077-F0897DA98834.png

7B2EE233-E3D9-44B5-B4D9-1EAFFF05EB92.png FD8ACE85-1901-4157-8CAA-09EC1E093DCC.png 8E6536A6-1007-435A-8B3F-2BE275636860.png
层次布局 grid布局 concentric布局 力导布局

试过各种各样的布局
最开始用的是层次布局,但是当没有关联的模型多的话,会在同一水平上排很长的模型, 看起来层次布局适合于流程图的情况
最后一个是力导布局(force)
力导布局最接近结果了,但是这个默认布局有个问题,没有关联关系的模型会拉得很开,造成空间上的浪费。
我最后解决的思路是,虚拟一个不可见的节点,把所有的模型拉在一起。

  1. const createSysNode = () => {
  2. return {
  3. id: 'model-SYS-CENTER-POINT',
  4. type: 'circle',
  5. isSys: true,
  6. isKeySharp: true,
  7. size: 10,
  8. }
  9. }

最终结果:

638978A4-5A10-4576-986B-2BD3A509080C.png

由于力导向布局不是一次性布局好的,中间会产生多次布局,变化会反应到界面上,因此会有动画的效果。

注意:

g6的graph的布局是支持webworker的,但是对于subgraphLayout 方式并不支持webworker, 需要自己实现。
如果使用es方式引用g6的化,webworker并不会支持,原因是es 代码需要经过webpack预处理,如果要解决在这个问题,webpack需要配置worker-loader,用于封装webwoker执行逻辑的代码。

  1. {
  2. test: /\.worker\.ts$/,
  3. exclude: /(node_modules)/,
  4. use: [
  5. {
  6. loader: 'worker-loader',
  7. options: {
  8. inline: true,
  9. fallback: false,
  10. name: 'g6Layout.worker.js',
  11. },
  12. },
  13. ],
  14. },

性能优化

通过引入fps测试组件来衡量性能优化的程度

  1. export const useFpsHook = () => {
  2. const fpsRef = useRef(null)
  3. useEffect(() => {
  4. if (fpsRef.current && window.SYS_backEndConfig && window.SYS_backEndConfig.ERD_FPS) {
  5. const stats = new Stats() // alert(stats.dom)
  6. stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
  7. fpsRef.current.appendChild(stats.dom)
  8. stats.dom.style.position = 'relative'
  9. function animate() {
  10. stats.begin() // monitored code goes here
  11. stats.end()
  12. requestAnimationFrame(animate)
  13. }
  14. requestAnimationFrame(animate)
  15. }
  16. }, [])
  17. return {
  18. fpsRef,
  19. }
  20. }

21D2E555-F70B-4BD7-A799-174B8B102A2E.png
从最开始的FPS 个位数,800 个模型情况,到现在的 20 左右 ,以下记录一些优化心得。

1487709-20190809150507384-1624695011.png

以上的图我们可以推出这个结论:

  1. 性能 = 1 /(画布大小 * 节点对象数量)

因此性能优化的大体思路就是让 画布越小, 可视区域的节点对象数量越少。

缩小画布

DB424001-A1F4-40F1-8F25-7CA8561759B8.png

代码:

  1. <Popover footer={false} content={<RadioGroup value={zoomNum * 2} onChange={zoomChange} >
  2. <Radio value={200}>100%</Radio>
  3. <Radio value={100}>50%</Radio>
  4. <Radio value={20}>10%</Radio>
  5. </RadioGroup>} placement='bottom' >
  6. {graph && `${zoomNum * 2 }%` }
  7. </Popover>

真实缩放比例其实是1.13%,我其实是把画布缩小了一半,显示比例*2,性能提升还是挺明显的,这个其实是可以继续缩小,还有很大的优化空间。

减少可视区域的节点数量
**
我们发现:

缩放比例越小 缩放比例越大
0D84075E-E987-4125-A257-5B357678BF8C.png D94A188C-F8B1-4085-8554-77B8D5A834BF.png
模型数量越多,
但是模型的细节就看不清楚
模型细节就越多,
但是模型数量越小

不清楚的地方,我们干脆就不显示,“所见即所渲染”
核心代码:

  1. graph.on('beforepaint', _.throttle(() => {
  2. // alert()
  3. const gWidth = graph.get('width')
  4. const gHeight = graph.get('height')
  5. // 获取视窗左上角对应画布的坐标点
  6. const topLeft = graph.getPointByCanvas(0, 0) // 获取视窗右下角对应画布坐标点
  7. const bottomRight = graph.getPointByCanvas(gWidth, gHeight)
  8. graph.getNodes().filter((a) => !a.isSys).forEach((node) => {
  9. const model = node.getModel()
  10. if (model.isSys) {
  11. node.getContainer().hide()
  12. return
  13. }
  14. const {
  15. config,
  16. data: _data,
  17. } = model
  18. const h = (config.headerHeight + _data.fields.length * config.fieldHeight + 4) / 2
  19. const w = config.width / 2 // 如果节点不在视窗中,隐藏该节点,则不绘制
  20. // note:由于此应用中有minimap,直接隐藏节点会影响缩略图视图,直接隐藏节点具体内容
  21. if (!model.selected && (model.x + w < topLeft.x - 200 || model.x - w > bottomRight.x || model.y + h < topLeft.y || model.y - h > bottomRight.y)) {
  22. node.getContainer().hide()
  23. } else {
  24. // 节点在视窗中,则展示
  25. node.getContainer().show()
  26. }
  27. })
  28. const edges = graph.getEdges()
  29. edges.forEach((edge) => {
  30. let sourceNode = edge.get('sourceNode')
  31. let targetNode = edge.get('targetNode')
  32. if (targetNode.getModel().isSys) {
  33. edge.hide()
  34. return
  35. }
  36. if (!sourceNode.getContainer().get('visible') && !targetNode.getContainer().get('visible')) {
  37. edge.hide()
  38. } else {
  39. edge.show()
  40. }
  41. })
  42. }, 10))
  1. 在graph “beforepaint”里面做判断显示和隐藏逻辑,
  2. G6 对show 和 hide 的实现跟HTML 不一样,可以真正的不render对象
  3. 另外加入了throttle 防止频繁渲染。

总结

  1. 在以模型为中心驱动开发的平台中,ER图是业务创造的重要场景之一(另外一个场景我认为是流程图)<br /> 由于平台本身的个性化,ER图如果能提供定制化的扩展能力,满足平台的个性化需求,将会发挥更大价值<br /> 一个中大型业务系统的模型数量是很庞大的,可视化全景展示对性能要求很高,所以选择canvas<br /> 基于canvas的可视化引擎G6ER图实现提供了强大的框架支撑<br /> 文章后面分享了一些G6使用上的心得<br /> 基于G6ER图已经开源,代码和功能还很糙,希望能一起完善<br /> 欢迎start [https://github.com/lusess123/web-pdm](https://github.com/lusess123/web-pdm)