What?
E-R方法是“实体-联系方法”(Entity-Relationship Approach)的简称。它是描述现实世界概念结构模型的有效方法,而ER图就是这个方法在界面上的可视化呈现。
无论是普遍的数据库驱动的开发方式,还是高大上的DDD,或者是模型驱动的Lowcode平台, 亦或主数据驱动的APaas平台,ER图提供了一种可视化业务的视角。
Why?
业务设计 ≈ 模型设计 ≈ 数据库设计
我们知道,一个好的APaas/Lowcode平台能充分隔离了技术复杂度和业务复杂度,而模型元数据作为系统输入参数(每个项目的模型字段不相同),ER图可以从这个角度上来度量一个系统的业务复杂度。
在业务复杂大型项目里面,一般会采用DDD的方法论来进行业务设计,而领域模型的设计是其中很重要的一部分,一般设计模型的人是领域专家和实现功能的开发者并不是一波人,可视化的ER图可能作为中间态产物成为了重要的交流工具。
而在中小型项目中,由于业务和界面交付简单,视图模型(VO),业务模型(BO),数据模型(DO) 倾向于三元合一,ER 图则充当数据库设计工具。
业务如此重要,想象一下,一般在项目KO或者评审期间,所有人聚在一起,基本上都会把ER图展开来共同讨论。对于一些中小型项目,ER图确定了,基本上业务逻辑,工作量都确定了,通俗的说,老程序员和架构师会把这个过程叫“项目开始编码之前严格把控数据库设计”。
定制化的ER图更有价值
任何可落地的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 的应用都不快) | ||
最适合图像密集型的游戏,其中的许多对象会被频繁重绘 | 不适合游戏应用 |
相比SVG,Canvas 更像是更底层的实现,同时 Canvas 是 WebGL 的入口, 性能优化的空间更大。<br /> 对于对标powerdesigner的web版的ER图来说, “需要展示成百上千个模型”这个是最核心的功能 , Canvas 成了必然的选择。但是,由于Canvas提供的是更底层绘图api, 缺乏上层封装 ,会导致开发体验和速度上过于原始 ,而 G6 作为一款图可视化引擎,可以弥补这里面的差距 。
踩坑和实践分享
连接线
ER图的连线, “字段” —- “模型”
连接点,在G6里面是通过”锚点” 这个概念来实现的
连接点在左边 | 连接点在右边 |
“连字段上的锚点”,固定上有两个,分别放在一左一右.如何根据两个关联模型的相对方位自动选择左边还是右边?
我的方案是在node:dragend事件里面根据相对方位做判断,修改dege的sourceAnchor的值:
graph.on('node:dragend', (ev) => {
const shape = ev.target
const node = ev.item
const edges = node.getEdges()
const x = ev.x
edges.forEach((edge) => {
const sourceNode = edge.getSource()
const targetNode = edge.getTarget()
if (node === sourceNode) {
const edgeModel = edge.getModel()
const isTo = x < targetNode.getModel().x
const i = edgeModel.fieldIndex
const l = edgeModel.fieldsLength
if (sourceNode !== targetNode) {
graph.updateItem(edge, {
sourceAnchor: !isTo ? i + 2 : 2 + i + l,
// targetAnchor: isTo ? 0 : 1,
})
}
} else {
const edgeModel = edge.getModel()
const isTo = sourceNode.getModel().x < x
const i = edgeModel.fieldIndex
const l = edgeModel.fieldsLength
if (sourceNode !== targetNode) {
graph.updateItem(edge, {
sourceAnchor: !isTo ? i + 2 : 2 + i + l,
})
}
}
}) // ----获取所有的边
“连到模型上的锚点”应该没有固定的位置,而是应该在整个模型节点表面自动连接最近的锚点,否则连线会很不好看
旧版本“连到模型上的锚点”是固定在模型的两边 | 最新版本“连到模型上的锚点”会自动找到整个周边最接近的点 |
实现思路是,当我在edge没有设置锚点的时候,g6会自动选择最接近的锚点,因为我在整个模型图上面都设置了无数的锚点可供选择:
getAnchorPoints(cfg) {
const {
config,
data,
} = cfg
const {
fields,
} = data
const h = config.headerHeight + getLength(fields.length) * config.fieldHeight
return [[0, config.headerHeight / 2 / h], // 左上方
[1, config.headerHeight / 2 / h], // 右上方
...fields.map((field, index) => {
const x = 10 / config.width
const l = config.headerHeight + config.fieldHeight * (index + 1) - config.fieldHeight / 2
const y = l / h
return [x, y]
}), ...fields.map((field, index) => {
const x = (config.width - 10) / config.width
const l = config.headerHeight + config.fieldHeight * (index + 1) - config.fieldHeight / 2
const y = l / h
return [x, y]
}),
...getTopAnch(50),
...getBottomAnch(50),
...getLeftAnch(100),
...getRightAnch(100),
]
上下左右的边界总共设置了300个锚点,并且均匀分布
布局算法选择
对于ER图来说,布局效果的好坏很影响整体的观感。g6 内置了各种各样的布局,到底哪一种最适合ER图呢?
层次布局 | grid布局 | concentric布局 | 力导布局 |
试过各种各样的布局
最开始用的是层次布局,但是当没有关联的模型多的话,会在同一水平上排很长的模型, 看起来层次布局适合于流程图的情况
最后一个是力导布局(force)
力导布局最接近结果了,但是这个默认布局有个问题,没有关联关系的模型会拉得很开,造成空间上的浪费。
我最后解决的思路是,虚拟一个不可见的节点,把所有的模型拉在一起。
const createSysNode = () => {
return {
id: 'model-SYS-CENTER-POINT',
type: 'circle',
isSys: true,
isKeySharp: true,
size: 10,
}
}
最终结果:
由于力导向布局不是一次性布局好的,中间会产生多次布局,变化会反应到界面上,因此会有动画的效果。
注意:
g6的graph的布局是支持webworker的,但是对于subgraphLayout 方式并不支持webworker, 需要自己实现。
如果使用es方式引用g6的化,webworker并不会支持,原因是es 代码需要经过webpack预处理,如果要解决在这个问题,webpack需要配置worker-loader,用于封装webwoker执行逻辑的代码。
{
test: /\.worker\.ts$/,
exclude: /(node_modules)/,
use: [
{
loader: 'worker-loader',
options: {
inline: true,
fallback: false,
name: 'g6Layout.worker.js',
},
},
],
},
性能优化
通过引入fps测试组件来衡量性能优化的程度
export const useFpsHook = () => {
const fpsRef = useRef(null)
useEffect(() => {
if (fpsRef.current && window.SYS_backEndConfig && window.SYS_backEndConfig.ERD_FPS) {
const stats = new Stats() // alert(stats.dom)
stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
fpsRef.current.appendChild(stats.dom)
stats.dom.style.position = 'relative'
function animate() {
stats.begin() // monitored code goes here
stats.end()
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
}
}, [])
return {
fpsRef,
}
}
从最开始的FPS 个位数,800 个模型情况,到现在的 20 左右 ,以下记录一些优化心得。
以上的图我们可以推出这个结论:
性能 = 1 /(画布大小 * 节点对象数量)
因此性能优化的大体思路就是让 画布越小, 可视区域的节点对象数量越少。
缩小画布
代码:
<Popover footer={false} content={<RadioGroup value={zoomNum * 2} onChange={zoomChange} >
<Radio value={200}>100%</Radio>
<Radio value={100}>50%</Radio>
<Radio value={20}>10%</Radio>
</RadioGroup>} placement='bottom' >
{graph && `${zoomNum * 2 }%` }
</Popover>
真实缩放比例其实是1.13%,我其实是把画布缩小了一半,显示比例*2,性能提升还是挺明显的,这个其实是可以继续缩小,还有很大的优化空间。
减少可视区域的节点数量
**
我们发现:
缩放比例越小 | 缩放比例越大 |
---|---|
模型数量越多, 但是模型的细节就看不清楚 |
模型细节就越多, 但是模型数量越小 |
不清楚的地方,我们干脆就不显示,“所见即所渲染”
核心代码:
graph.on('beforepaint', _.throttle(() => {
// alert()
const gWidth = graph.get('width')
const gHeight = graph.get('height')
// 获取视窗左上角对应画布的坐标点
const topLeft = graph.getPointByCanvas(0, 0) // 获取视窗右下角对应画布坐标点
const bottomRight = graph.getPointByCanvas(gWidth, gHeight)
graph.getNodes().filter((a) => !a.isSys).forEach((node) => {
const model = node.getModel()
if (model.isSys) {
node.getContainer().hide()
return
}
const {
config,
data: _data,
} = model
const h = (config.headerHeight + _data.fields.length * config.fieldHeight + 4) / 2
const w = config.width / 2 // 如果节点不在视窗中,隐藏该节点,则不绘制
// note:由于此应用中有minimap,直接隐藏节点会影响缩略图视图,直接隐藏节点具体内容
if (!model.selected && (model.x + w < topLeft.x - 200 || model.x - w > bottomRight.x || model.y + h < topLeft.y || model.y - h > bottomRight.y)) {
node.getContainer().hide()
} else {
// 节点在视窗中,则展示
node.getContainer().show()
}
})
const edges = graph.getEdges()
edges.forEach((edge) => {
let sourceNode = edge.get('sourceNode')
let targetNode = edge.get('targetNode')
if (targetNode.getModel().isSys) {
edge.hide()
return
}
if (!sourceNode.getContainer().get('visible') && !targetNode.getContainer().get('visible')) {
edge.hide()
} else {
edge.show()
}
})
}, 10))
- 在graph “beforepaint”里面做判断显示和隐藏逻辑,
- G6 对show 和 hide 的实现跟HTML 不一样,可以真正的不render对象
- 另外加入了throttle 防止频繁渲染。
总结
在以模型为中心驱动开发的平台中,ER图是业务创造的重要场景之一(另外一个场景我认为是流程图)<br /> 由于平台本身的个性化,ER图如果能提供定制化的扩展能力,满足平台的个性化需求,将会发挥更大价值<br /> 一个中大型业务系统的模型数量是很庞大的,可视化全景展示对性能要求很高,所以选择canvas<br /> 基于canvas的可视化引擎G6为ER图实现提供了强大的框架支撑<br /> 文章后面分享了一些G6使用上的心得<br /> 基于G6的ER图已经开源,代码和功能还很糙,希望能一起完善<br /> 欢迎start [https://github.com/lusess123/web-pdm](https://github.com/lusess123/web-pdm)