在开发流程编排工具时,我们需要一个既能处理复杂图形关系,又能保持代码简洁的技术组合。AntV X6 作为专业的图编辑引擎,提供了丰富的节点操作和布局算法,而 Vue 3 的响应式特性和组合式 API 让前端开发变得异常高效。这个组合特别适合构建审批流、任务流等需要可视化配置的场景。
我去年在开发一个电商订单处理系统时就深有体会。当时需要让运营人员能够拖拽配置订单审核流程,试过几种方案后,最终选择 X6 + Vue 3 的组合。实测下来,从原型到上线只用了两周时间,而且后期维护成本很低。X6 的节点自定义能力让我们可以轻松实现各种业务图标,而 Vue 3 的 setup 语法让事件绑定变得非常直观。
首先创建 Vue 3 项目(这里假设你已经配置好 Vue 环境),然后安装 X6 和必要的布局引擎:
bash复制yarn add @antv/x6@1.34.6 dagre insert-css
注意版本选择很重要。X6 目前有 1.x 和 2.x 两个大版本,虽然 2.x 功能更强大,但 1.x 的文档和社区资源更丰富,对新手更友好。我在三个项目中分别用过这两个版本,1.x 的稳定性确实更胜一筹。
在 Vue 组件中初始化画布时,有几个关键参数需要注意:
javascript复制import { Graph } from '@antv/x6'
const graph = new Graph({
container: document.getElementById('container'),
width: 800,
height: 600,
grid: true, // 显示网格对齐
snapline: true, // 开启对齐线
interacting: { // 精细控制交互
nodeMovable: true,
edgeMovable: false
}
})
这里有个小技巧:设置 scroller: true 可以让大流程图支持拖拽查看。曾经有个项目的流程特别复杂,开启这个选项后用户体验提升明显。
审批流中的每个节点通常需要显示负责人、审批类型等信息。通过继承 X6 的节点基类,我们可以创建符合业务需求的节点:
javascript复制Graph.registerNode('approval-node', {
inherit: 'rect',
width: 180,
height: 60,
attrs: {
body: {
fill: '#F5F7FA',
stroke: '#D4D8DD',
rx: 4,
ry: 4
},
label: {
text: '审批节点',
fontSize: 14,
fill: '#333'
},
'approver-icon': {
xlinkHref: 'https://example.com/approver.svg',
width: 16,
height: 16,
x: 10,
y: 10
}
}
})
在实际项目中,我建议把节点样式抽离成独立的配置文件。这样当产品经理要求调整节点外观时,你只需要修改配置文件,而不必深入业务逻辑。
流程图中最复杂的部分往往是连线交互。X6 提供了丰富的事件系统:
javascript复制// 连接线创建事件
graph.on('edge:connected', ({ edge }) => {
const source = edge.getSourceNode()
const target = edge.getTargetNode()
console.log(`从 ${source.id} 连接到 ${target.id}`)
// 业务校验示例
if (source.data.type === 'END') {
alert('结束节点不能作为起点!')
graph.removeEdge(edge.id)
}
})
// 自定义连接桩
graph.addNode({
id: 'node1',
shape: 'approval-node',
ports: {
groups: {
in: { position: 'top' },
out: { position: 'bottom' }
},
items: [
{ id: 'in1', group: 'in' },
{ id: 'out1', group: 'out' }
]
}
})
踩过的一个坑:记得在组件销毁时移除事件监听,否则可能导致内存泄漏。可以在 Vue 的 onUnmounted 钩子中调用 graph.off() 清除所有监听。
流程图的美观度直接影响用户体验。Dagre 可以自动计算节点位置:
javascript复制import dagre from 'dagre'
function autoLayout() {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setGraph({ rankdir: 'TB', nodesep: 50, ranksep: 70 })
graph.getNodes().forEach(node => {
dagreGraph.setNode(node.id, {
width: node.getSize().width,
height: node.getSize().height
})
})
graph.getEdges().forEach(edge => {
dagreGraph.setEdge(edge.source.cell, edge.target.cell)
})
dagre.layout(dagreGraph)
// 应用布局结果
dagreGraph.nodes().forEach(id => {
const node = graph.getCell(id)
if (node) {
node.position(dagreGraph.node(id).x, dagreGraph.node(id).y)
}
})
}
在电商后台项目中,我们给不同类型的节点设置了不同的间距:审批节点间距 50px,通知节点间距 30px,这样布局结果更符合业务逻辑。
当流程节点超过 100 个时,可能会遇到性能问题。通过以下方法可以显著提升性能:
graph.freeze() 和 graph.unfreeze()useForeignObject: falsevirtual: true 开启虚拟渲染javascript复制// 批量添加节点时的优化写法
graph.freeze()
try {
nodes.forEach(node => graph.addNode(node))
edges.forEach(edge => graph.addEdge(edge))
autoLayout()
} finally {
graph.unfreeze()
}
曾经处理过一个包含 300+ 节点的超大型流程图,通过上述优化手段,渲染时间从 8 秒降到了 1 秒以内。
前后端交互的数据结构设计很关键。推荐采用如下格式:
json复制{
"nodes": [
{
"id": "node1",
"shape": "approval-node",
"data": {
"type": "APPROVAL",
"approver": "张经理",
"required": true
}
}
],
"edges": [
{
"source": "node1",
"target": "node2",
"label": "通过"
}
]
}
在实际项目中,我们还会保存每个节点的位置信息,这样再次打开时能保持用户之前的布局。
保存流程时,只需要调用 graph.toJSON() 即可获取当前画布数据。加载时:
javascript复制function loadFlow(data) {
graph.fromJSON(data)
// 恢复自定义事件
data.nodes.forEach(node => {
if (node.data?.specialEvent) {
setupCustomEvent(node.id)
}
})
}
有个实用的技巧:在保存前可以调用 graph.cleanSelection() 清除选中状态,避免把临时状态也保存到后端。
X6 内置了 History 插件,只需简单配置:
javascript复制import { History } from '@antv/x6-plugin-history'
graph.use(
new History({
enabled: true,
maxSize: 100 // 记录100步操作
})
)
// 绑定快捷键
graph.bindKey('ctrl+z', () => graph.undo())
graph.bindKey('ctrl+shift+z', () => graph.redo())
在财务审批系统中,这个功能让用户误操作后可以轻松回退,获得了客户高度评价。
通过监听鼠标右键事件,可以实现丰富的上下文菜单:
javascript复制graph.on('node:contextmenu', ({ node, x, y }) => {
showContextMenu({
position: { x, y },
items: [
{
label: '查看审批记录',
onClick: () => showApprovalHistory(node.id)
},
{
label: '更改审批人',
onClick: () => changeApprover(node.id)
}
]
})
})
实现时要注意在点击菜单外部时及时关闭菜单,我通常会在 document 上添加一个全局点击监听来处理。
如果遇到自定义节点显示不正常,可以按以下步骤排查:
Graph.registerNode 的第三个参数 true(表示覆盖已有定义)事件系统不工作的常见原因:
pointer-events 属性javascript复制// 确保自定义元素可交互
Graph.registerNode('custom-node', {
// ...
attrs: {
'.interactive-area': {
pointerEvents: 'visiblePainted'
}
}
})
在最近的一个项目中,就因为漏写了这个属性,导致整个下午都在调试为什么点击事件不触发。