JFlow 提供了一整套基础设施,用于支撑富交互的点线图,可以是流程图、ER图等。
JFlow 的核心思想来源于浏览器渲染 HTML 的布局方式,数据与图间存在一层不同于传统 HTML 布局的图布局,这层布局可以由数据自身计算生成,这层交由框架使用者具体问题具体实现,因此与JFlow对接的不是具体数据,而是由数据生成的布局节点和布局方法,提高了JFlow的可拓展性。
另外单个数据对应的展示形式又回到了传统 HTML 的渲染模式,JFlow内置了一部分类似的盒模型和事件系统,方便节点内部实现样式,传递事件。
下面我将通过一个例子来说明如何通过 JFlow 来搭建简单的ER图。
数据结构和布局定义
根据输入的数据模型,自己构建一个简单的基础 Layout,用于描述源数据与图节点、图节点和连线之间的关系。
假设数据模型如下
[
{
"name": "A" ,
"extends": "B",
"mixins": ["C", "D"]
},
{
"name": "B" ,
"implements": "E"
},
{
"name": "C" ,
},
{
"name": "D" ,
},
{
"name": "E" ,
},
]
有了这个数据结构,我们需要根据接口 [Layout](Layout) 实现一套对应的布局节点和一个布局类。 布局类描述了内部节点的排布和连线的信息。
布局节点需要:
- type 指定了一种类型的节点,用于区分节点的类型渲染
- source 节点对应的数据
布局类需要:
- 定义成员变量
- flowStack:节点数组
- type: 布局节点类型,
- source: 源数据,
- layoutNode: 布局节点对象,
- flowLinkStack:连线数组
- from: 布局节点
- to: 布局节点
- ...连线传参
- flowStack:节点数组
- 实现一个方法 reOrder(JFlow) 用于重新排列节点,生成 flowStack 和 flowLinkStack
- 实现一个方法 reflow(JFlow) 用于重新计算节点展示的位置
- static 用于指定layout是否需要布局变化检测, 默认 false,值为 true 时需要增加staticCheck方法来判断是否变化布局
这边先做一个简单的实现。
class VirtualNode {
constructor(source) {
this.type = 'VirtualNode';
this.source = source;
}
makeLink(callback) {
const {
extends: ext, mixins, implements: impl
} = this.source;
if(ext) {
callback({
from: ext,
to: this.source.name,
part: 'extends',
})
}
if(mixins) {
mixins.forEach(t => {
callback({
from: t,
to: this.source.name,
part: 'mixins',
fontSize: '24px',
lineDash: [5, 2]
})
})
}
if(impl) {
callback({
from: impl,
to: this.source.name,
part: 'implements',
})
}
}
}
class DemoLayout {
constructor(source) {
this.static = false;
// 管理节点和边界 必须
this.flowStack = [];
this.flowLinkStack = [];
const nodeMap = {};
const nodes = source.map(s => {
const node = new VirtualNode(s);
nodeMap[s.name] = node
return node;
});
nodes.forEach(node => {
// flowStack 接受 type 作为节点显示类型的区分
this.flowStack.push({
type: node.type,
source: node.source,
layoutNode: node,
})
node.makeLink((configs) => {
const fromNode = nodeMap[configs.from];
const toNode = nodeMap[configs.to];
if(!fromNode) return;
if(!toNode) return;
// flowlinkStack 中 from、to 属性接收 layoutNode,
this.flowLinkStack.push({
...configs,
from: fromNode,
to: toNode
})
})
});
this.erNodes = nodes;
}
reflow(jflow) {
const nodes = this.erNodes;
nodes.forEach((node, idx) => {
// 计算 节点位置
const renderNode = jflow.getRenderNodeBySource(node.source)
renderNode.anchor = [-idx * 220, (idx % 2) * 80];
});
}
}
定义好布局之后,可以选择原生和vue两种实现方式。
原生版
首先,在 html 增加一个标签
<div id="container" style="width: 600px; height: 300px"></div>
然后在工程中引入 jflow,以及相关绘图类, [LinearLayout](LinearLayout) 让元素在内部按主轴排列,属性和效果相当于 display: flow 布局
import JFlow, {
Group,
Text,
BezierLink,
LinearLayout,
commonEventAdapter
} from '@joskii/jflow';
import DemoLayout from '../demo-layout';
import source from '../data.json'
function renderNode(erNode) {
const className = new Text({
content: erNode.source.name,
textColor: '#EB6864'
});
const wrapper = new Group({
layout: new LinearLayout({
direction: 'vertical',
gap: 0,
}),
borderRadius: 8,
borderColor: '#EB6864',
borderWidth: 2,
padding: 20,
});
wrapper.addToStack(className);
// 增加内部节点后,需要重算布局
wrapper.recalculate();
return wrapper;
}
function renderLink(linkmeta, jflowStage) {
const meta = linkmeta.meta;
const link = new BezierLink({
...linkmeta,
content: linkmeta.part,
from: jflowStage.getRenderNodeBySource(linkmeta.from.source),
to: jflowStage.getRenderNodeBySource(linkmeta.to.source),
backgroundColor: '#EB6864',
fontSize: '16px'
});
return link;
}
function render(dom, data) {
const layout = new DemoLayout(data);
const jflowInstance = new JFlow({
allowDrop: false,
layout,
eventAdapter: commonEventAdapter
});
layout.flowStack.forEach(({ type, layoutNode, source }) => {
const Node = renderNode(layoutNode);
jflowInstance.setLayoutNodeBySource(source, layoutNode);
jflowInstance.setRenderNodeBySource(source, Node)
jflowInstance.addToStack(Node);
});
layout.flowLinkStack.forEach(linkMeta => {
const link = renderLink(linkMeta, jflowInstance);
jflowInstance.addToLinkStack(link);
});
jflowInstance.$mount(dom);
}
render(document.getElementById('container'), source)
这样就实现了一个简单的 ER 图。
效果展示
Vue版
下面是 基于 Vue Plugin 的实现代码
main.js
import { JFlowVuePlugin } from '@joskii/jflow';
Vue.use(JFlowVuePlugin);
<template>
<j-jflow
style="width: 600px; height: 300px; border: 1px solid #000"
:configs="configs">
<!-- VirtualNode 为 Layout~NodeMeta 的 type -->
<template #VirtualNode="{ meta }">
<virtual-node :node="meta"></virtual-node>
</template>
<!-- 当没有为 Layout~LinkMeta 指定type的时候,将默认使用 plainlink 插槽 -->
<template #plainlink="{ configs }">
<jBezierLink
:configs="{
...configs,
content: configs.part,
backgroundColor: '#EB6864',
fontSize: '16px'
}"
:from="configs.from"
:to="configs.to">
</jBezierLink>
</template>
</j-jflow>
</template>
<script>
import DemoLayout from '../demo-layout';
import VirtualNode from './virtual-node.vue';
import { commonEventAdapter } from '@joskii/jflow';
import source from '../data.json'
const layout = new DemoLayout(source);
export default {
components: {
VirtualNode,
},
data() {
return {
configs: {
allowDrop: false,
layout,
eventAdapter: commonEventAdapter
}
}
}
};
</script>
virtual-node.vue
<template>
<!-- jflowId 就是 layoutNode 中 ID -->
<j-group
:jflowId="node.id"
:configs="configs">
<j-text :configs="{
textColor: '#EB6864',
content: node.id,
}">
</j-text>
</j-group>
</template>
<script>
import { LinearLayout } from '@joskii/jflow';
const layout = new LinearLayout({
direction: 'vertical',
gap: 0,
});
export default {
props: {
node: Object,
},
data() {
return {
configs: {
layout,
borderRadius: 8,
borderColor: '#EB6864',
borderWidth: 2,
padding: 20,
}
}
},
}
</script>
Vue版通过更友好的方式来实现节点添加,另外在事件绑定处理上也更为优雅,另外还能借助 Vue 的一些特性(比如 provide inject)来实现更好的封装。