简介:这份资源面向需要在 Vue.js 项目中集成 BPMN 2.0 流程图的开发者,尤其适合具备一定前端基础、希望实现业务流程可视化展示与交互编辑的中级学习者。它解决了在 Vue 环境下引入 bpmn.js 时组件封装、模型加载与画布渲染等常见问题,可作为流程设计器或审批流前端模块的起步模板。压缩包共 20 个文件,约 211KB,以 vue 组件、js 脚本和 json 配置为主,另含 html 入口、图标与说明文档,覆盖从项目配置到视图组件的完整结构。目前已有 1595 人学习下载。读者可参考其中的组件写法,掌握在 mounted 钩子中实例化 viewer、导入 XML 并自适应缩放,以及借助 moddle 与 modeler 扩展编辑能力,同时了解依赖管理与工程配置的组织方式,为后续接入数据存储、错误处理和自定义样式提供可复用的起点。
1. 当流程图不再是图片:在 Vue 里把 bpmn.js 跑起来
很多管理后台的“审批流程”页面,本质上就是一张静态图片加几个按钮,改流程得让后端重新画图、前端重新发版。但真实业务里,流程是会变的:多一个审批节点、换一个条件分支、调整一下会签顺序,如果每次都要动代码,开发和业务都得疯。bpmn.js 就是来解决这个问题的——它把 BPMN 2.0 规范做成了浏览器里可拖拽、可连线、可导出 XML 的建模器。配合 Vue 的组件化能力,你可以把流程设计器嵌进任何页面,让业务人员自己画流程图,前端只负责把 XML 存下来、渲染出来、跑起来。这篇笔记拆的就是一个 Vue 2 + bpmn.js 的完整集成项目,从目录结构到核心代码,再到二次开发时怎么改属性面板、怎么接后端接口,都会落到能直接抄的代码块上。
2. 项目骨架与依赖:Vue 2 工程里怎么塞进一个建模器
拿到源码包后,第一件事不是急着npm run serve,而是先看package.json和目录结构。这个项目是典型的 Vue CLI 脚手架产物,但围绕 bpmn.js 做了几层封装,不先理清楚,后面改代码会找不到北。
2.1 目录结构:建模器被拆成了哪几块
项目根目录下,和流程设计相关的代码集中在src/components和src/views里。我一般会按下面这个结构去认:
src/ ├── components/ │ ├── bpmn/ │ │ ├── BpmnDesigner.vue # 建模器主组件,初始化 canvas │ │ ├── BpmnViewer.vue # 只读预览组件,用于详情页 │ │ ├── properties/ # 右侧属性面板 │ │ │ ├── PropertiesPanel.vue │ │ │ └── panels/ # 各类节点属性子面板 │ │ └── palette/ # 左侧工具栏定制 │ └── ... ├── views/ │ └── process/ │ ├── Design.vue # 流程设计页,引用 BpmnDesigner │ └── Detail.vue # 流程详情页,引用 BpmnViewer ├── utils/ │ └── bpmnUtils.js # XML 导入导出、节点遍历工具 └── api/ └── process.js # 流程保存、加载接口BpmnDesigner.vue是核心,它负责实例化BpmnModeler、挂载到 DOM、暴露保存和导入方法。BpmnViewer.vue更轻,只实例化BpmnViewer并加载 XML,适合详情页只读展示。properties目录是二次开发改得最多的地方,因为每个业务节点要填的表单字段都不一样。
2.2 依赖安装:版本对齐是第一个坑
bpmn.js 的包名是bpmn-js,但属性面板需要额外装bpmn-js-properties-panel和@bpmn-io/properties-panel。这个项目用的是 Vue 2,所以还要注意bpmn-js的版本不能太新,否则默认导出方式会变。
# 核心建模器,锁定 8.x 版本,兼容 Vue 2 的 CommonJS 引入 npm install bpmn-js@8.9.0 --save # 属性面板,提供右侧表单容器 npm install bpmn-js-properties-panel@0.46.0 --save # 属性面板依赖的底层组件库 npm install @bpmn-io/properties-panel@0.9.0 --save # BPMN 2.0 的元模型,属性面板扩展要用 npm install bpmn-moddle@7.1.2 --save装完之后,在vue.config.js里加一条transpileDependencies: ['bpmn-js', 'bpmn-js-properties-panel'],否则打包时这些包里的 ES6 语法可能报错。这一步很多新手会漏,现象是npm run serve能起来,但npm run build直接崩。
2.3 初始化建模器:一个 DOM 容器加一次 new
BpmnDesigner.vue的mounted钩子里做初始化,核心就三行:拿容器、new 实例、importXML。但顺序和参数有讲究。
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 camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda' export default { data() { return { bpmnModeler: null, container: null, xmlStr: '' // 从后端拿到的流程 XML } }, mounted() { // 1. 拿到画布容器,必须是真实 DOM this.container = this.$refs.canvas // 2. 实例化建模器,同时挂载属性面板 this.bpmnModeler = new BpmnModeler({ container: this.container, propertiesPanel: { parent: '#js-properties-panel' // 右侧面板的挂载点 }, additionalModules: [ propertiesPanelModule, propertiesProviderModule ], moddleExtensions: { camunda: camundaModdleDescriptor // 支持 camunda 扩展属性 } }) // 3. 导入初始 XML,没有就创建一个空流程图 this.createNewDiagram() }, methods: { async createNewDiagram() { const xml = this.xmlStr || `<?xml version="1.0" encoding="UTF-8"?> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn"> <bpmn:process id="Process_1" isExecutable="false"> <bpmn:startEvent id="StartEvent_1" /> </bpmn:process> <bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"> <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <dc:Bounds x="173" y="102" width="36" height="36" /> </bpmndi:BPMNShape> </bpmndi:BPMNPlane> </bpmndi:BPMNDiagram> </bpmn:definitions>` try { await this.bpmnModeler.importXML(xml) // 导入成功后调整画布,让流程图居中 const canvas = this.bpmnModeler.get('canvas') canvas.zoom('fit-viewport') } catch (err) { console.error('流程图加载失败', err) } } } }container参数必须传一个已经渲染的 DOM 节点,用$refs拿最稳。propertiesPanel.parent是右侧面板的容器选择器,如果页面上没有这个元素,属性面板不会报错但也不会显示。additionalModules里两个模块缺一不可,少了propertiesProviderModule面板就是空的。moddleExtensions决定了你能在 XML 里写哪些扩展属性,camunda 是最常用的,如果后端用的是 activiti 或 flowable,要换成对应的描述文件。
3. 核心交互拆解:从拖拽节点到导出 XML 的完整链路
建模器跑起来只是第一步,真正要接进业务,得把“用户操作 → 数据变化 → 持久化”这条链路打通。这一章拆三个关键动作:监听元素变化、导出 XML、以及只读预览。
3.1 事件监听:怎么知道用户改了流程图
bpmn.js 的EventBus会抛出很多事件,但业务上最关心的是“元素变了”和“选中了某个节点”。前者用来做自动保存或脏标记,后者用来驱动右侧属性面板。
// 在 mounted 里初始化完 bpmnModeler 之后绑定事件 const eventBus = this.bpmnModeler.get('eventBus') // 监听元素变化:新增、删除、移动、改属性都会触发 eventBus.on('commandStack.changed', () => { this.isDirty = true // 标记有未保存修改 // 可以在这里做防抖自动保存 this.debouncedSave() }) // 监听选中变化:点击节点或空白处 eventBus.on('selection.changed', (e) => { const selectedElements = e.newSelection if (selectedElements.length === 1) { // 把选中的节点传给属性面板组件 this.$refs.propertiesPanel.setSelectedElement(selectedElements[0]) } else { this.$refs.propertiesPanel.clearSelectedElement() } }) // 监听元素点击,用于自定义右键菜单或快捷操作 eventBus.on('element.click', (e) => { console.log('点击了节点', e.element.id, e.element.type) })commandStack.changed是最可靠的变化信号,它覆盖了所有通过命令栈执行的操作。但要注意,importXML本身也会触发一次,所以初始化时要先解绑或加个标志位跳过。selection.changed的newSelection是个数组,多选时长度大于 1,属性面板一般只处理单选。
3.2 导出 XML:保存前必须做的一次校验
用户点“保存”时,不能直接把内存里的 XML 扔给后端,得先saveXML拿到字符串,再检查有没有语法错误。
methods: { async saveProcess() { try { // 1. 导出 XML,{ format: true } 会格式化缩进,方便排查 const { xml } = await this.bpmnModeler.saveXML({ format: true }) // 2. 简单校验:至少有一个开始节点和一个结束节点 const parser = new DOMParser() const doc = parser.parseFromString(xml, 'text/xml') const startEvents = doc.getElementsByTagName('bpmn:startEvent') const endEvents = doc.getElementsByTagName('bpmn:endEvent') if (startEvents.length === 0) { this.$message.warning('流程图缺少开始节点') return } if (endEvents.length === 0) { this.$message.warning('流程图缺少结束节点') return } // 3. 调用后端接口保存 await saveProcessApi({ processId: this.processId, xmlContent: xml }) this.isDirty = false this.$message.success('保存成功') } catch (err) { console.error('保存失败', err) this.$message.error('流程图保存失败,请检查连线是否完整') } } }saveXML返回的是 Promise,解构出的xml就是标准 BPMN 2.0 字符串。校验那一步不是必须的,但血泪经验告诉我,用户经常画一半就点保存,后端拿到不完整的 XML 解析会报错,前端先拦一道能省很多沟通成本。format: true在开发阶段很有用,线上可以关掉减小体积。
3.3 只读预览:详情页不该能拖拽
详情页用BpmnViewer而不是Modeler,这样用户只能看不能改,也省掉了属性面板的依赖。
import BpmnViewer from 'bpmn-js/lib/Viewer' export default { mounted() { this.viewer = new BpmnViewer({ container: this.$refs.canvas }) this.loadXml() }, methods: { async loadXml() { const { data } = await getProcessDetailApi(this.processId) try { await this.viewer.importXML(data.xmlContent) const canvas = this.viewer.get('canvas') canvas.zoom('fit-viewport') // 高亮当前正在执行的节点 if (this.currentNodeId) { const elementRegistry = this.viewer.get('elementRegistry') const element = elementRegistry.get(this.currentNodeId) if (element) { canvas.addMarker(element.id, 'highlight') } } } catch (err) { console.error('预览加载失败', err) } } } }Viewer的 API 和Modeler基本一致,但没有saveXML和命令栈。高亮节点用canvas.addMarker,配合 CSS 类.highlight { fill: #ffcc00; }就能把当前审批节点标黄。这个技巧在流程跟踪页面特别实用,用户一眼就能看到卡在哪一步。
4. 属性面板定制:让每个节点都能填业务字段
默认的属性面板只有 id、name 这些基础字段,业务上肯定不够。比如审批节点要填审批人、审批方式,条件分支要填表达式。这一章讲怎么在现有面板上扩展自定义属性。
4.1 属性面板的渲染机制
bpmn-js-properties-panel是基于分组(group)和条目(entry)的。每个分组对应面板上的一个折叠块,条目就是里面的输入框、下拉框。要加自定义字段,就是往对应节点的分组里塞条目。
// properties/panels/CustomProps.js import { is } from 'bpmn-js/lib/util/ModelUtil' // 导出一个函数,返回分组配置 export default function customProps(group, element, bpmnFactory, translate) { // 只对用户任务节点生效 if (!is(element, 'bpmn:UserTask')) { return } // 往分组里添加一个“审批人”输入框 group.entries.push({ id: 'approver', label: translate('审批人'), modelProperty: 'approver', get: function(element) { const businessObject = element.businessObject return { approver: businessObject.approver || '' } }, set: function(element, values) { const businessObject = element.businessObject return { approver: values.approver } } }) }get负责从节点上读值,set负责写回。modelProperty是字段名,会直接映射到businessObject上。这个函数要在初始化建模器时通过propertiesProvider注入,具体做法是自定义一个 provider 模块,把customProps加到CamundaPropertiesProvider的getGroups返回值里。
4.2 注册自定义属性提供者
光有面板还不够,得让建模器知道这个 provider 的存在。
// properties/CustomPropertiesProvider.js import { is } from 'bpmn-js/lib/util/ModelUtil' import customProps from './panels/CustomProps' export default function CustomPropertiesProvider(propertiesPanel, injector, translate) { this.getGroups = function(element) { return function(groups) { // 找到“常规”分组,把自定义字段加进去 const generalGroup = groups.find(g => g.id === 'general') if (generalGroup) { customProps(generalGroup, element, injector, translate) } return groups } } } // 依赖注入声明 CustomPropertiesProvider.$inject = ['propertiesPanel', 'injector', 'translate']然后在BpmnDesigner.vue的additionalModules里加上这个 provider,注意要放在propertiesProviderModule之后,否则分组还没生成。
4.3 属性面板与 Vue 组件的通信
如果不想用原生面板,也可以完全自己写一个 Vue 组件当属性面板,通过selection.changed事件拿到选中节点,直接操作businessObject。
// 在 BpmnDesigner.vue 中 eventBus.on('selection.changed', (e) => { const element = e.newSelection[0] if (element) { // 把节点对象传给 Vue 属性面板 this.selectedElement = element this.$refs.vuePanel.loadElement(element) } }) // Vue 属性面板里修改值 methods: { updateApprover(val) { const modeling = this.bpmnModeler.get('modeling') modeling.updateProperties(this.selectedElement, { approver: val }) } }modeling.updateProperties是官方推荐的修改方式,它会自动触发commandStack.changed,保证撤销重做可用。直接改businessObject虽然也能生效,但撤销栈会乱,这是个容易翻车的地方。
5. 避坑与排查:集成 bpmn.js 时最容易踩的五个雷
这一章全是实际调试时遇到的问题,每个都按“现象 → 原因 → 解决”写,照着排查能省不少时间。
5.1 画布不显示或高度为 0
现象:页面加载后建模器区域一片空白,控制台没有报错。原因:container的父元素没有设置高度,bpmn.js 初始化时拿不到有效尺寸。解决:给画布容器加height: 100%或固定像素高度,并确保从html、body到容器每一层都有高度。
5.2 属性面板不显示或显示不全
现象:右侧面板区域是空的,或者只有标题没有内容。原因:propertiesPanel.parent选择器对应的 DOM 不存在,或者additionalModules里漏了propertiesProviderModule。解决:检查模板里有没有id="js-properties-panel"的元素,并确认两个模块都引入了。
5.3 打包后报 “Cannot read property 'get' of undefined”
现象:开发环境正常,npm run build后页面白屏,控制台报模块内部错误。原因:bpmn-js及其依赖没有加入transpileDependencies,ES6 语法没被转译。解决:在vue.config.js里加上transpileDependencies: ['bpmn-js', 'bpmn-js-properties-panel', 'bpmn-moddle']。
5.4 导入 XML 报 “unparsable content” 错误
现象:从后端拿到的 XML 调importXML直接 reject。原因:XML 字符串里有非法字符,或者命名空间不完整。解决:先用DOMParser解析一遍,看parsererror节点;确保 XML 包含bpmn和bpmndi两个命名空间,且每个元素都有对应的BPMNShape。
5.5 自定义属性保存后丢失
现象:属性面板填了值,保存 XML 后再加载,值没了。原因:自定义属性没有写进moddleExtensions的 descriptor 里,导出时被过滤掉了。解决:在moddleExtensions中注册自定义属性,或者直接用 camunda 的扩展字段,它默认会被保留。
6. 二次开发改造:从能跑到好用还差哪几步
项目跑通之后,真正花时间的是把它改造成贴合自己业务的样子。我一般会从三个方向下手:工具栏定制、后端接口对接、以及流程校验规则。
工具栏定制是最直观的。默认的 palette 里有几十个节点,业务上可能只用得到开始、审批、条件、结束这四种。在BpmnDesigner.vue里通过additionalModules覆盖PaletteProvider,把不需要的条目过滤掉。
// palette/CustomPaletteProvider.js export default function CustomPaletteProvider(palette, create, elementFactory, spaceTool, lassoTool) { this._create = create this._elementFactory = elementFactory // 只保留四种节点 const actions = { 'create.start-event': createAction('bpmn:StartEvent', '开始'), 'create.user-task': createAction('bpmn:UserTask', '审批'), 'create.exclusive-gateway': createAction('bpmn:ExclusiveGateway', '条件'), 'create.end-event': createAction('bpmn:EndEvent', '结束') } palette.registerProvider(this) }后端接口对接要注意的是,保存时传的 XML 最好同时存一份 JSON 格式的节点列表,方便后端做流程引擎解析。我一般会在saveProcess里加一步,用elementRegistry遍历所有节点,生成一个{ id, type, name, approver }的数组一起提交。
流程校验规则是提升体验的关键。除了开始和结束节点,还要检查孤立节点、没有出口的条件分支、以及审批人未填的节点。这些校验放在saveProcess里,用elementRegistry.filter遍历,每条规则给一个明确的提示。
// 校验孤立节点:既没有入线也没有出线 const elementRegistry = this.bpmnModeler.get('elementRegistry') const allElements = elementRegistry.filter(el => { return el.type !== 'bpmn:Process' && el.type !== 'bpmn:Definitions' }) const isolated = allElements.filter(el => { const incoming = el.incoming || [] const outgoing = el.outgoing || [] return incoming.length === 0 && outgoing.length === 0 }) if (isolated.length > 0) { this.$message.warning(`有 ${isolated.length} 个节点没有连线`) return }从那以后,我每次集成 bpmn.js 都会先把transpileDependencies和容器高度这两件事检查一遍,再开始写业务代码。这两个坑不解决,后面所有调试都是白费。希望帮到你。
本文还有配套的精品资源,点击获取