Vue 2 集成 bpmn.js 实战:从流程设计器到属性面板定制 简介这份资源面向需要在 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-js8.9.0 --save # 属性面板提供右侧表单容器 npm install bpmn-js-properties-panel0.46.0 --save # 属性面板依赖的底层组件库 npm install bpmn-io/properties-panel0.9.0 --save # BPMN 2.0 的元模型属性面板扩展要用 npm install bpmn-moddle7.1.2 --save装完之后在vue.config.js里加一条transpileDependencies: [bpmn-js, bpmn-js-properties-panel]否则打包时这些包里的 ES6 语法可能报错。这一步很多新手会漏现象是npm run serve能起来但npm run build直接崩。2.3 初始化建模器一个 DOM 容器加一次 newBpmnDesigner.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 version1.0 encodingUTF-8? bpmn:definitions xmlns:bpmnhttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:bpmndihttp://www.omg.org/spec/BPMN/20100524/DI idDefinitions_1 targetNamespacehttp://bpmn.io/schema/bpmn bpmn:process idProcess_1 isExecutablefalse bpmn:startEvent idStartEvent_1 / /bpmn:process bpmndi:BPMNDiagram idBPMNDiagram_1 bpmndi:BPMNPlane idBPMNPlane_1 bpmnElementProcess_1 bpmndi:BPMNShape id_BPMNShape_StartEvent_2 bpmnElementStartEvent_1 dc:Bounds x173 y102 width36 height36 / /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。解决检查模板里有没有idjs-properties-panel的元素并确认两个模块都引入了。5.3 打包后报 “Cannot read property get of undefined”现象开发环境正常npm run build后页面白屏控制台报模块内部错误。原因bpmn-js及其依赖没有加入transpileDependenciesES6 语法没被转译。解决在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和容器高度这两件事检查一遍再开始写业务代码。这两个坑不解决后面所有调试都是白费。希望帮到你。本文还有配套的精品资源点击获取