开始接触 Bpmn-js 是因为一个绕不开的真实需求:后台管理系统要上一套审批流,甲方开口就要一个"像画图工具一样拖拽出流程"的页面。当时我快速对比了一圈方案,最后定下来 Vue3 + Bpmn-js 的组合,把 BPMN 2.0 标准的流程设计器完整落地到了生产环境。
这套方案最终做到了什么:画布拖拽建模、节点属性配置、XML 导入导出、流程校验、自定义业务字段、暗黑主题适配,从流程编辑器到执行引擎之间用标准 BPMN XML 打通。这篇文章会把整个选型、开发、踩坑的过程完整写出来,适合准备在 Vue3 项目里集成 Bpmn-js、或者正在纠结"流程图组件到底怎么选"的开发者参考。我尽量不写废话,直接给能用的结论和代码。
1. 为什么不是自研画布:Bpmn-js 选型的真实对比
1.1 流程设计器不是简单的拖拽画板
很多团队听到"流程设计器"第一反应是:不就是拖几个框、画几条线吗?自己用 SVG 或者 Canvas 写一个不得了。这种想法我特别理解,但实际拆解需求之后会发现,一个能上生产的设计器远比"看起来能拖"复杂得多。
流程设计器至少要做这些事:节点拖拽和连线、连线路径自动避障、节点增删改查、撤销重做、键盘快捷键、属性面板联动、模型序列化和反序列化、流程合法性校验、缩放平移视图控制。这还没算"审批引擎读得懂"这个最关键的要求。BPMN 2.0 本身是一套完整规范,有 event、gateway、task、sequence flow 等各种元素类型,每个元素有对应的 XML 语义。如果自研画布只实现了"画几个矩形",导出的数据引擎根本不认,那这个设计器就只能当摆设。
我当时把需求文档里"流程可被审批引擎执行"这句话划了重点:设计器产出的必须是合法、可解析的 BPMN 2.0 XML,而不是自定义 JSON。这个前提直接决定了技术选型方向——必须站在一个成熟的建模引擎上做,而不是从零画。
1.2 自研流程编辑器的隐性成本
这里我想多说一句自研方案的隐性成本,因为我确实见过团队在这上面耗了大半年。连线自动避障看起来简单,实际涉及路径计算、节点碰撞检测、连线与节点绑定关系;拖拽新增节点要处理 undo/redo 状态快照;序列化要考虑所有元素类型、坐标、连线方式、扩展属性。
更麻烦的是规范兼容。BPMN 2.0 的 XML schema 细节非常多,流程引擎对 XML 的解析又很严格。自己定义一套数据结构,后续每次对接新引擎都可能出现语义对不上的问题。用一个通俗点的类比:自研画布像是在白纸上画建筑草图,Bpmn-js 则是给你一套带结构计算、规范图纸、图层管理的制图软件。前者画得开心,后者才能拿去施工。
如果你只是做一个轻量拓扑图、思维导图,那自研或者轻量库完全没问题。但只要涉及审批流、工作流引擎,我建议直接站在 BPMN 规范实现者的肩膀上,省下来的时间足够把业务打磨得更细。
1.3 Bpmn-js 与 LogicFlow、AntV X6 的取舍
当时我对比的三个主要选择:LogicFlow、AntV X6、Bpmn-js。简单说下结论,方便你按场景对号入座。
LogicFlow 是滴滴开源的流程绘制框架,基于 TypeScript 编写,流程图所需的基础能力很全,自定义节点也方便。但它主推的是一套自己的数据格式,虽然提供 bpmn 适配插件,整体重心还是"通用流程图框架",不是"BPMN 规范引擎"。
AntV X6 是图编辑引擎,能力非常强,适合做脑图、ER 图、拓扑图、DAG 调度图这类场景。但如果要做 BPMN 设计器,需要自己实现大量 BPMN 规范相关逻辑,等于还是在造轮子。
Bpmn-js 是 bpmn.io 官方出品的 BPMN 2.0 建模工具包,内置了符合规范的 palette、contextPad、overlays、属性编辑能力,输出 XML 直接能被 Camunda、Flowable、Activiti 这类引擎解析。缺点也很明显:自定义节点样式不如通用流程图框架灵活,文档偏少,很多机制要读源码才能搞清楚。
最终我的选择是 Bpmn-js,核心判断依据就是"标准优先"。前端框架用 Vue3,是因为整个后台体系已经全面迁到 Vue3 + Vite,设计器作为一个独立模块嵌进去,需要跟现有技术栈统一,后期维护成本最低。
2. 环境准备与依赖安装:版本是最容易踩坑的起点
2.1 初始化 Vue3 + Vite 项目
如果是从零开始,直接用官方脚手架就可以。我这边用的是 Vue 3.4 和 Vite 5,完整依赖如下:
npm create vue@latest my-workflow-designer cd my-workflow-designer npm install bpmn-js装完之后先看一眼package.json,确认 bpmn-js 的实际版本。这里有个非常关键的习惯:一定要锁定版本。bpmn-js 的 API 在不同大版本之间有过调整,尤其是属性面板和事件机制。我项目里锁在11.x,配套的属性面板用的也是跟它兼容的版本。如果不锁定,同事npm install拉到一个新大版本,很可能出现运行时报错。
2.2 bpmn-js 相关依赖的选型与分工
除了核心的bpmn-js,实际项目中通常还会用到这几个包,建议先搞清楚它们各自干什么:
| 包名 | 作用 |
|---|---|
| bpmn-js | 核心建模引擎,基于 diagram-js,提供画布、元素注册表、模型层 |
| bpmn-moddle | BPMN 模型的读和写,负责 XML 与内存模型对象之间的转换 |
| diagram-js | 底层图形交互框架,bpmn-js 基于它构建,一般不需要直接操作 |
| bpmn-js-properties-panel | 属性面板组件,配合属性编辑使用 |
| @bpmn-io/properties-panel | 新版属性面板的底层实现,按需引入 |
| bpmn-js-bpmnlint | 流程建模规范检查工具,可选 |
很多人刚开始搞混 bpmn-js 和 bpmn-moddle,其实一句话就能分清:bpmn-js 负责"画"和"交互",bpmn-moddle 负责"把模型变成 XML、把 XML 变成模型"。在设计器里importXML、saveXML这些方法底层都是 bpmn-moddle 在干活。
2.3 CSS 资源和打包处理的注意事项
Bpmn-js 的样式是独立的 CSS 文件,不像组件库那样自动按需引入。最少要引入两张表:
import 'bpmn-js/dist/assets/diagram-js.css' import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'这里有个比较容易忽略的细节:diagram-js.css管的是画布、连线、拖拽框这些基础样式,bpmn-embedded.css管的是 BPMN 元素图标和字体。如果只引了前者,流程节点会全部变成"空心方块",图标全丢。
如果你要接属性面板,还要额外引入属性面板的样式:
import 'bpmn-js-properties-panel/dist/assets/properties-panel.css' import 'bpmn-js/lib/assets/bpmn-js.css'另一个重点:Vite 打包 bpmn-js 时,官方推荐的引入路径是模块内部的lib目录,而不是直接import BpmnModeler from 'bpmn-js'。因为包入口文件默认指向的是打包后的完整文件,里面有些依赖在 Vite 下可能解析出问题。正确写法是:
import BpmnModeler from 'bpmn-js/lib/Modeler'如果遇到 Vite 依赖预构建报错,可以在vite.config.js里显式配置:
export default defineConfig({ optimizeDeps: { include: ['bpmn-js', 'bpmn-moddle'] } })3. 核心集成:把画布稳定地嵌入 Vue3 组件
3.1 组件结构与容器渲染
Bpmn-js 是典型的"命令式"库:你给它一个真实 DOM 容器,它在里面生成画布。这和 Vue 的声明式渲染思路不一样,所以组件里必须留一个不参与 Vue 更新的空白容器。我的组件结构大致是这样:
<template> <div class="designer-wrapper"> <div class="designer-canvas" ref="canvasRef"></div> <div class="designer-panel" id="js-properties-panel"></div> </div> </template>canvasRef绑定的是画布容器,js-properties-panel是属性面板容器。两个容器都必须有明确高度,Bpmn-js 不会帮你处理容器尺寸,画布初始化时如果容器高度是 0,后面怎么调都白搭。我的样式大概是这样:
.designer-wrapper { display: flex; height: 100%; width: 100%; } .designer-canvas { flex: 1; height: 100%; background: #fafafa; } .designer-panel { width: 300px; height: 100%; overflow-y: auto; border-left: 1px solid #e0e0e0; }3.2 Composition API 中的实例化与响应式避坑
到了核心环节:初始化。在 Vue3 组合式 API 里,我推荐用shallowRef存 BpmnModeler 实例,并且配合markRaw使用。直接上代码:
import { shallowRef, onMounted, onBeforeUnmount, markRaw } from 'vue' import BpmnModeler from 'bpmn-js/lib/Modeler' const canvasRef = ref(null) const modeler = shallowRef(null) onMounted(() => { modeler.value = markRaw(new BpmnModeler({ container: canvasRef.value, propertiesPanel: { parent: '#js-properties-panel' } })) modeler.value.createDiagram() })这里有个 Vue3 特有的深坑:如果把modeler直接定义成ref()或者把它放进 reactive 对象里,Vue 会用 Proxy 包一层。而 bpmn-js 内部有大量基于instanceof检查和事件监听器的逻辑,代理对象会破坏这些判断,出现"事件触发不了""元素选不中""报错说不是合法实例"之类的诡异问题。
shallowRef可以避免深层响应式代理,markRaw则是进一步明确告诉 Vue"这个对象别给我做响应式处理"。这两个搭配使用,基本能杜绝这一类问题。
3.3 组件销毁时的清理,避免事件泄漏
集成命令式库,生命周期管理是重中之重。Bpmn-js 内部会往容器上挂 DOM 事件监听器,还会注册各种模块实例。组件卸载时如果只删掉 DOM 而不调用destroy(),这些监听器会留在内存里。
我的销毁逻辑是这么写的:
onBeforeUnmount(() => { if (modeler.value) { modeler.value.destroy() modeler.value = null } // 如果容器是组件自己创建的,顺手清理一下 canvasRef.value?.removeAttribute('style') })另外,如果给 modeler 注册过自定义事件监听,销毁逻辑里应该先off再destroy,养成好习惯。示例:
const selectionChanged = (event) => { // 处理选中元素变化的逻辑 updatePropertiesPanel(event) } modeler.value.on('selection.changed', selectionChanged) onBeforeUnmount(() => { modeler.value?.off('selection.changed', selectionChanged) modeler.value?.destroy() modeler.value = null })4. 功能闭环:导入导出、工具栏、属性面板
4.1 导入与导出 XML 的完整实现
设计器不能只活在浏览器里,得有"打开流程"和"保存流程"两个口子。导入的逻辑是后端把 BPMN XML 字符串传给前端,前端调用importXML:
async function openBpmn(xmlString) { if (!modeler.value) return try { const result = await modeler.value.importXML(xmlString) const { warnings } = result if (warnings && warnings.length) { console.warn('导入过程存在警告:', warnings) } // 导入成功后把视口调整到合适位置 const canvas = modeler.value.get('canvas') canvas.zoom('fit-viewport', 'auto') } catch (err) { console.error('导入失败:', err) window.$message?.error('流程文件解析失败') } }导出的核心是saveXML,这个方法返回 Promise,拿到xml字段就是完整的 BPMN 2.0 字符串:
async function saveBpmn() { if (!modeler.value) return null const { xml } = await modeler.value.saveXML({ format: true }) return xml }format: true是让输出的 XML 自动缩进换行,方便后端存库和排查问题。建议保存前先做一次流程校验(后面会讲到),校验通过再调saveXML。
有些场景还需要导出图片,可以用saveSVG:
const { svg } = await modeler.value.saveSVG()得到的 svg 字符串可以直接显示、下载,或者交给后端转 PNG。这个功能在生成流程图快照、流程文档时很实用。
4.2 撤销重做、调整画布视角与缩放
Bpmn-js 的撤销重做能力内置于 commandStack 模块,使用方式非常直接:
const commandStack = modeler.value.get('commandStack') commandStack.undo() commandStack.redo()为了让工具栏上的撤销/重做按钮状态实时变化,监听commandStack.changed事件:
modeler.value.on('commandStack.changed', () => { undoable.value = commandStack.canUndo() redoable.value = commandStack.canRedo() })缩放和视角控制通过 canvas 模块实现:
const canvas = modeler.value.get('canvas') // 放大/缩小 canvas.zoom(1.2) canvas.zoom(0.8) // 一键适应视口:推荐在打开流程后调用 canvas.zoom('fit-viewport', 'auto') // 返回默认缩放 canvas.zoom('return')工具栏一般就是放一排按钮:打开、保存、撤销、重做、放大、缩小、适应视口、校验。每个按钮对应上面这些 API。
4.3 属性面板的接入与数据同步
属性面板是"设计器能配置业务"的关键。老版本的属性面板已经集成在 bpmn-js 里,但新版(bpmn-js 11+)把它拆成了独立模块。初始化时需要指定属性面板容器,同时配置 additionalModules:
import BpmnModeler from 'bpmn-js/lib/Modeler' import propertiesPanelModule from 'bpmn-js-properties-panel' import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda' import camundaModdle from 'camunda-bpmn-moddle/resources/camunda' modeler.value = markRaw(new BpmnModeler({ container: canvasRef.value, propertiesPanel: { parent: '#js-properties-panel' }, additionalModules: [ propertiesPanelModule, propertiesProviderModule ], moddleExtensions: { camunda: camundaModdle } }))选中元素时,属性面板会自动跟随显示对应元素的属性。如果你想在面板外部做一些联动(比如右侧表单显示当前节点信息),监听selection.changed:
modeler.value.on('selection.changed', (event) => { const element = event.newSelection?.[0] if (element) { // 通过业务属性扩展读取自定义字段 currentElement.value = element } })5. 定制扩展:自定义节点样式、校验与业务字段
5.1 通过 moddle 扩展添加自定义业务属性
真实项目中,光有 BPMN 标准属性远远不够。比如审批节点需要配置"审批人角色""会签/或签""超时时间",这些都是自定义字段。Bpmn-js 提供 moddle 扩展机制来支持自定义属性。
第一步,建一个 JSON 描述自定义属性的命名空间和字段:
{ "name": "Custom", "prefix": "custom", "uri": "http://mycompany.com/schema/custom", "xml": { "tagAlias": "lowerCase" }, "types": [ { "name": "CustomTaskElement", "superClass": ["bpmn:Task"], "properties": [ { "name": "approvalType", "isAttr": true, "type": "String" }, { "name": "approverList", "isAttr": true, "type": "String" } ] } ] }第二步,在初始化 modeler 时注册这个扩展:
import customModdle from './custom-moddle.json' modeler.value = markRaw(new BpmnModeler({ container: canvasRef.value, // other config... moddleExtensions: { custom: customModdle } }))第三步,通过元素业务对象读写扩展属性:
function getApprovalType(element) { const bo = element.businessObject return bo.get('custom:approvalType') || 'single' } function setApprovalType(element, value) { const bo = element.businessObject const modeling = modeler.value.get('modeling') modeling.updateProperties(element, { 'custom:approvalType': value }) }注意,自定义属性一定用modeling.updateProperties去改,这样能进入 undo/redo 操作栈,用户按 Ctrl+Z 时可以回退。
5.2 流程校验与错误点位提示
保存前校验是必须的。我的校验逻辑分两层:第一层是 Bpmn-js 自带的导入解析校验(上面importXML会返回 warnings),第二层是业务级校验。
业务校验通常要检查这些点:
- 流程必须有开始事件和结束事件
- 所有节点必须至少有一条出线
- 网关节点的条件连线必须配置条件表达式
- 每个审批节点必须配置审批人
- 不能有孤立节点
实现思路很简单:遍历元素注册表,逐个检查。
function validateFlow() { const elementRegistry = modeler.value.get('elementRegistry') const errors = [] const allElements = elementRegistry.getAll() let hasStart = false let hasEnd = false allElements.forEach((element) => { const bo = element.businessObject if (bo.$type === 'bpmn:StartEvent') hasStart = true if (bo.$type === 'bpmn:EndEvent') hasEnd = true if (bo.$type === 'bpmn:Task') { if (!bo.get('custom:approverList')) { errors.push({ elementId: element.id, message: `节点 ${bo.name || element.id} 未配置审批人` }) } } }) if (!hasStart) errors.push({ elementId: '', message: '流程缺少开始节点' }) if (!hasEnd) errors.push({ elementId: '', message: '流程缺少结束节点' }) return errors }校验结果除了弹窗提示,最好还能在画布上标红。这里可以用 overlays 功能,在出错的节点上叠加一个错误角标:
const overlays = modeler.value.get('overlays') errors.forEach((error) => { if (error.elementId) { overlays.add(error.elementId, { position: { top: 0, right: 0 }, html: `<div class="error-badge">${error.message}</div>` }) } })5.3 自定义 Palette 和 ContextPad 的入口
默认 palette(左侧元素工具栏)包含所有 BPMN 元素类型,对业务人员来说太多了,容易误拖。我通常会精简 palette,只保留业务需要的:开始事件、任务、用户任务、排他网关、并行网关、结束事件、连线。
实现方式是通过 additionalModules 注入自定义 palette provider:
class CustomPaletteProvider { constructor(palette, create, elementFactory, handTool, lassoTool) { this.palette = palette this.create = create this.elementFactory = elementFactory this.handTool = handTool this.lassoTool = lassoTool palette.registerProvider(this) } getPaletteEntries() { const { create, elementFactory, handTool, lassoTool } = this function createAction(type) { return function (event) { const shape = elementFactory.createShape({ type }) create.start(event, shape) } } return { 'hand-tool': { group: 'tools', className: 'bpmn-icon-hand-tool', title: '拖拽画布', action: { click: (event) => handTool.activate(event) } }, 'lassoTool': { group: 'tools', className: 'bpmn-icon-lasso-tool', title: '框选', action: { click: (event) => lassoTool.activate(event) } }, 'bpmn-start-event': { group: 'events', className: 'bpmn-icon-start-event-none', title: '开始事件', action: { click: createAction('bpmn:StartEvent') } }, 'bpmn-user-task': { group: 'activities', className: 'bpmn-icon-user-task', title: '审批节点', action: { click: createAction('bpmn:UserTask') } }, 'bpmn-exclusive-gateway': { group: 'gateways', className: 'bpmn-icon-gateway-xor', title: '排他网关', action: { click: createAction('bpmn:ExclusiveGateway') } }, 'bpmn-end-event': { group: 'events', className: 'bpmn-icon-end-event-none', title: '结束事件', action: { click: createAction('bpmn:EndEvent') } } } } }然后在初始化时把这个 provider 加进 additionalModules:
import CustomPaletteProvider from './CustomPaletteProvider' new BpmnModeler({ additionalModules: [CustomPaletteProvider] })同理,ContextPad(右键/选中节点时弹出的操作栏)也可以自定义 provider,隐藏不想要的入口,比如"替换类型"这类高级操作对业务用户就不太友好。
6. 踩坑实录:六类高频问题的完整排查链路
6.1 画布空白或样式错乱
这类问题我见得太多了,第一反应永远是"三查":查容器高度、查 CSS 引入、查 DOM 是否被替换。
容器高度是最常见的。如果外层容器用height: auto,Bpmn-js 初始化时量到的高度是 0,画布根本不会渲染。排查方法:打开控制台查看.djs-container的实际尺寸,为 0 就说明父级高度链断了。解决办法是给画布容器设定明确高度,或者用flex布局并保证父级有高度。
CSS 引入的问题也很隐蔽。很多人只引入了 bpmn-js-properties-panel 的样式,漏掉 diagram-js.css,结果连线箭头、拖动框显示异常。另一个是 Vue 单文件组件里的 scoped 样式,如果你把.djs-container写在 scoped 里,属性选择器会让它失效。解决办法是放到全局样式,或者用:deep()穿透。
6.2 组件销毁后事件仍然触发的内存泄漏
这个问题在我们内部分两个层级出现过。第一次是组件被切换后,用户还能在控制台看到 old modeler 的日志,说明实例没有被正确销毁,监听器还活在内存里。当时排查链路是:先确认组件的 onBeforeUnmount 是否执行,再确认 destroy() 是否被调用,最后检查是否有外部模块(比如属性面板 provider 里注册的全局事件)没有被释放。
第二次更隐蔽:初始化时用了ref()保存 modeler 实例,Vue 的 Proxy 包了一层,内部模块注册的事件引用关系变乱,导致销毁时清理不干净。这类问题在浏览器 performance 内存面板里能看到"每次打开关闭设计器,内存只增不减"的曲线。解决办法就是前面的:shallowRef+markRaw+destroy()。
如果设计器是在某些 SPA 路由里反复进入退出,建议写一个包裹 layer 或 composable,把创建和销毁收敛到同一个出口,避免忘记调用。
6.3 自定义属性在保存后被引擎忽略
这个问题非常典型:页面上配置的自定义属性,导出 XML 时确实在节点上,但流程引擎加载后读不到。排查链路是这样的:
先看导出的 XML 里有没有xmlns:custom这个命名空间声明。如果 moddle 扩展注册成功,XML 根节点应该自动带上命名空间声明。如果没有,问题多半出在 moddleExtensions 的 key 和 JSON 里的 prefix 不一致,或者 additionalModules 没加上。
再看属性写入方式。如果直接改businessObject的属性而没有走modeling.updateProperties,很多属性不会触发 XML 序列化的脏检查,导出时可能丢失或不对。一定要走建模 API。
最后还有一种情况:引擎侧解析器不支持自定义前缀。比如我们后端用的引擎默认只认custom前缀,换成别的就忽略。这个属于契约问题,最好在设计器端和引擎端维护一份"属性字典",避免各写各的。
6.4 弹窗/抽屉中打开设计器的尺寸问题
在 Modal 或 Drawer 里嵌入设计器,最常见的坑是:弹窗打开时画布渲染不出来,或者渲染成一个很窄的条。原因是弹窗初始状态v-if或display: none,Bpmn-js 初始化时容器宽高为 0,之后弹窗显示,但画布不会自动重算尺寸。
解决方案有三种,按推荐程度排:
- 弹窗完全显示后再挂载组件:用
v-if控制,打开弹窗时把visible设为 true,让设计器组件在弹窗渲染完成后才 mount。这样初始化时容器已经有真实尺寸。 - 延迟初始化:
nextTick或setTimeout(() => init(), 0),给弹窗动画留出完成时间。 - 监听弹窗生命周期,在
after-open钩子里调用canvas.zoom('fit-viewport')并触发一次 resize。
如果你用了自适应布局,窗口尺寸变化时还应该监听 resize 事件:
const resizeObserver = new ResizeObserver(() => { modeler.value?.get('canvas').zoom('fit-viewport', 'auto') }) resizeObserver.observe(canvasRef.value)6.5 暗黑主题下画布配色适配
现在很多后台管理系统支持暗黑模式,Bpmn-js 默认的高亮蓝、白底背景在暗色主题下会非常刺眼。最麻烦的是它部分样式写在行内样式或者 SVG 属性里,光靠覆盖 CSS 不够。
实践下来比较可行的方案是:用 CSS 变量覆盖关键颜色。新版 diagram-js 支持通过 CSS 变量控制部分颜色,比如:
.dark-theme .djs-container { --color-directory: #1e1e1e; --color-white: #2d2d2d; --color-black: #e0e0e0; --color-blue: #409eff; }对于节点填充色、边框色这类写进 SVG 属性的样式,可以在导入流程后遍历元素批量调整,或者用自定义渲染器覆写 BaseRenderer 的绘制方法。但要注意:直接修改 BPMN 元素的内部 fill 属性,XML 里不会持久化,只在画布层做视觉覆盖,这样不会破坏流程数据,这个思路很重要。
6.6 Vite 构建时 bpmn-js 报错的处理
最后说下构建阶段遇到的坑。Vite 对大型 CJS 依赖做预构建时,偶尔会报Can't resolve 'clipboard'或者process is not defined这类错误。常规处理手段有两个:
一是调整优化配置:
export default defineConfig({ optimizeDeps: { include: ['bpmn-js'], exclude: ['bpmn-moddle'] }, resolve: { dedupe: ['bpmn-js'] } })二是严格按照模块路径引入,避免走包入口的浏览器产物。比如bpmn-js/lib/Modeler、bpmn-js-properties-panel/lib/PropertiesPanel,这些路径指向的是源码模块,让 Vite 自己打包,兼容性更好。
如果用了 bpmnlint 一类的辅助包,建议检查它们的 main 字段是否指向 ESM,必要时也用optimizeDeps兜底。
整个流程设计器从选型到落地,最核心的一条经验是:把 Bpmn-js 当作"建模引擎"而非"UI 组件库"来用,所有业务字段、校验规则、存储结构都建立在标准 BPMN 模型之上,而不是另搞一套自定义数据结构再去做转换。这样设计器前端、引擎后端、流程监控端拿到的始终是同一种"语言"。
如果你也是第一次在 Vue3 项目里接入 Bpmn-js,建议先把最小 demo 跑通,再逐步叠加属性面板、自定义扩展、校验这些能力,每加一层就保存一次 XML 到后端确认数据链路没有被破坏。这套流程走完,你会对整个 BPMN 建模体系的理解上一个台阶,后面再做流程回显、版本对比、流程模拟这些进阶功能,就都是水到渠成的事了。