开始

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: 布局节点
      • ...连线传参
  • 实现一个方法 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)来实现更好的封装。

效果展示

学习更多:

初级

  1. 对象、连线和布局
  2. 对象、连线布局和Vue动态插槽
  3. 事件和冒泡
  4. 组与组布局

高级

  1. 坐标系统
  2. 扩展JFlow
  3. 内部性能优化