Vue 3集成BPMN.js:从基础集成到深度自定义的实战指南

📅 2026/8/12 15:34:43
Vue 3集成BPMN.js:从基础集成到深度自定义的实战指南
1. 项目缘起为什么要在Vue里折腾BPMN.js最近在做一个后台管理系统的审批流模块产品经理甩过来一张用某在线工具画的流程图问我“这个功能咱们前端能自己做一个设计器出来吗要能拖拽、能连线、能保存那种。”我一看这不就是典型的业务流程建模嘛。市面上现成的流程图库不少但要么功能太简单比如用gojs或antv X6画个基础拓扑图要么就是封装好的SaaS服务没法深度定制业务属性。我们的需求很明确需要一个符合BPMN 2.0标准的、可深度自定义节点与属性的流程图设计器并且要无缝集成到现有的Vue 3技术栈里。找了一圈bpmn-js这个库进入了视线。它是Camunda开源的工作流引擎的前端建模工具本身就是用JavaScript写的对BPMN规范的支持非常完整。但它的官方示例和文档基本都是基于原生JS或简单构建工具的如何把它优雅地、可维护地集成到一个现代的Vue项目中并且实现业务层面的自定义比如改变节点外观、增加业务属性表单这里面有不少门道。网上能找到的教程要么太旧要么只讲了“跑起来”对于实际开发中遇到的样式污染、模型扩展、属性面板联动等深水区问题避而不谈。这篇文章我就结合最近这个项目的实战把vuebpmn-js这套组合拳的集成、定制和避坑经验掰开揉碎了讲清楚。2. 环境搭建与基础集成从零到一渲染画布首先明确我们的技术选型Vue 3 Composition API TypeScript Vite。bpmn-js本身对框架没有要求我们的目标是将它封装成一个受Vue响应式系统管理的、可复用的组件。2.1 安装依赖与初步封装创建项目这些基础步骤跳过我们直接从安装依赖开始。bpmn-js的核心包是bpmn-js我们还需要它的一个查看器如果不需要编辑只用bpmn-js即可和对应的流程图样式。npm install bpmn-js bpmn-js-properties-panel camunda-bpmn-moddle --save # 注意bpmn-js-properties-panel 是官方属性面板camunda-bpmn-moddle 是Camunda扩展的模型描述对于基础BPMN也常用 diagram-js 和 bpmn-moddle更常见的组合是安装以下包它们提供了更清晰的分层npm install bpmn-js diagram-js bpmn-moddle --save为了在Vue组件中使用我们先创建一个基础的BpmnModeler.vue组件。核心思路是在Vue组件的mounted生命周期或onMounted钩子中初始化bpmn-js实例并将其挂载到一个DOM容器通常是div上。template div classbpmn-container div refcontainerRef classbpmn-canvas/div !-- 后续可以在这里添加属性面板、工具栏等 -- /div /template script setup langts import { ref, onMounted, onBeforeUnmount } from vue; import BpmnModeler from bpmn-js/lib/Modeler; import bpmn-js/dist/assets/diagram-js.css; import bpmn-js/dist/assets/bpmn-font/css/bpmn.css; const containerRef refHTMLElement(); const bpmnModeler refBpmnModeler | null(null); onMounted(() { if (!containerRef.value) return; // 初始化BPMN建模器 bpmnModeler.value new BpmnModeler({ container: containerRef.value, // 其他配置项如属性面板、附加模块等后续会扩展 }); // 创建一个空的BPMN图 createNewDiagram(); }); onBeforeUnmount(() { // 销毁实例释放内存 if (bpmnModeler.value) { bpmnModeler.value.destroy(); bpmnModeler.value null; } }); const createNewDiagram async () { if (!bpmnModeler.value) return; try { const xmlStr ?xml version1.0 encodingUTF-8? bpmn2:definitions xmlns:bpmn2http://www.omg.org/spec/BPMN/20100524/MODEL xmlns:bpmndihttp://www.omg.org/spec/BPMN/20100524/DI xmlns:dchttp://www.omg.org/spec/DD/20100524/DC xmlns:dihttp://www.omg.org/spec/DD/20100524/DI idsample-diagram targetNamespacehttp://bpmn.io/schema/bpmn bpmn2:process idProcess_1 isExecutablefalse bpmn2:startEvent idStartEvent_1 / /bpmn2:process bpmndi:BPMNDiagram idBPMNDiagram_1 bpmndi:BPMNPlane idBPMNPlane_1 bpmnElementProcess_1 bpmndi:BPMNShape id_BPMNShape_StartEvent_2 bpmnElementStartEvent_1 dc:Bounds x150 y100 width36 height36 / /bpmndi:BPMNShape /bpmndi:BPMNPlane /bpmndi:BPMNDiagram /bpmn2:definitions; const { warnings } await bpmnModeler.value.importXML(xmlStr); if (warnings warnings.length) { console.warn(导入BPMN XML时出现警告:, warnings); } console.log(BPMN画布渲染成功); } catch (err) { console.error(渲染BPMN图失败:, err); } }; /script style scoped .bpmn-container { width: 100%; height: 600px; border: 1px solid #ccc; position: relative; } .bpmn-canvas { width: 100%; height: 100%; } /style这个组件已经可以渲染一个带有一个开始事件的空流程图了。这里有几个关键点样式引入必须引入bpmn-js自带的两个CSS文件否则图形无法正确显示。这两个CSS包含了画布布局、图形样式和图标字体。初始化时机必须在DOM元素挂载完成后onMounted再初始化bpmn-js因为它需要操作具体的DOM容器。资源释放在组件销毁前onBeforeUnmount调用destroy()方法防止内存泄漏。XML导入bpmn-js的核心是操作符合BPMN 2.0标准的XML。我们通过importXML方法导入一个初始的XML字符串来创建画布。这个XML字符串即使只有一个开始事件也包含了完整的BPMN定义结构。2.2 解决样式冲突与画布控制在实际项目中你很快会遇到第一个坑全局样式污染。bpmn-js的CSS是全局的其中像.djs-palette左侧工具栏、.djs-overlay这类类名可能会和你项目中的其他样式冲突。更棘手的是Vue的单文件组件中scoped样式对bpmn-js动态生成的内部DOM元素是无效的。我的解决方案是采用CSS Modules或者更精细的全局样式控制。这里推荐一个实用技巧为BPMN容器创建一个具有特定命名空间的全局样式块。首先在项目的全局样式文件如src/assets/css/bpmn-custom.css中覆写关键样式/* 重置bpmn-js内部一些可能冲突的样式 */ .bpmn-engine-container .djs-palette { border-right: 1px solid #e0e0e0 !important; background-color: #fafafa !important; } .bpmn-engine-container .djs-element .djs-element-selected { stroke-width: 3px !important; stroke: #1890ff !important; } /* 隐藏默认的logo */ .bpmn-engine-container .bjs-powered-by { display: none !important; }然后在Vue组件中为容器根元素添加一个特定的类名比如bpmn-engine-container并确保全局样式已引入。template div classbpmn-engine-container div refcontainerRef classbpmn-canvas/div /div /template这样通过父级容器的类名进行约束既能覆盖bpmn-js的默认样式又避免了全局污染。另一个常见需求是控制画布缩放与自适应。bpmn-js实例提供了canvas对象来进行画布控制。import { ref } from vue; const zoomLevel ref(1); const handleZoomIn () { if (!bpmnModeler.value) return; const canvas bpmnModeler.value.get(canvas); canvas.zoom(zoomLevel.value 0.1); }; const handleZoomOut () { if (!bpmnModeler.value) return; const canvas bpmnModeler.value.get(canvas); canvas.zoom(zoomLevel.value - 0.1); }; const handleZoomToFit () { if (!bpmnModeler.value) return; const canvas bpmnModeler.value.get(canvas); canvas.zoom(fit-viewport); };通过bpmnModeler.get(canvas)可以获取到画布控制器进而实现缩放、滚动、获取视口内所有元素等高级操作。3. 核心定制一自定义建模器与扩展模块基础画布有了但默认的建模器可能不符合我们的业务需求。比如我们想禁用某些默认功能如标签编辑或者想集成官方的属性面板。这就需要我们在初始化Modeler时进行配置。3.1 集成属性面板bpmn-js-properties-panel是官方提供的属性编辑面板但它需要配合对应的moddle扩展如camunda-bpmn-moddle才能工作因为属性定义是基于BPMN扩展规范的。对于通用BPMN我们可以使用更基础的bpmn-js-properties-panel和bpmn-moddle。首先安装必要的包npm install bpmn-js-properties-panel bpmn-moddle --save然后修改建模器的初始化配置将属性面板作为附加模块引入并指定其容器。template div classbpmn-engine-container div classmodeler-container div refcanvasRef classbpmn-canvas/div div refpropertiesRef classproperties-panel/div /div /div /template script setup langts import { ref, onMounted } from vue; import BpmnModeler from bpmn-js/lib/Modeler; import propertiesPanelModule from bpmn-js-properties-panel; import propertiesProviderModule from bpmn-js-properties-panel/lib/provider/camunda; // 使用Camunda的属性提供器功能较全 import camundaModdleDescriptor from camunda-bpmn-moddle/resources/camunda; // ... 其他导入和ref声明 onMounted(() { if (!canvasRef.value || !propertiesRef.value) return; bpmnModeler.value new BpmnModeler({ container: canvasRef.value, propertiesPanel: { parent: propertiesRef.value // 指定属性面板的容器 }, additionalModules: [ propertiesPanelModule, propertiesProviderModule // 引入属性面板模块 ], moddleExtensions: { camunda: camundaModdleDescriptor // 引入Camunda的moddle扩展以支持其定义的属性 } }); // ... 后续导入XML }); /script style scoped .modeler-container { display: flex; width: 100%; height: 700px; } .bpmn-canvas { flex: 1; border-right: 1px solid #e8e8e8; } .properties-panel { width: 300px; overflow-y: auto; background: #fafafa; } /style这样右侧就会出现一个属性面板当你点击画布上的元素如任务、网关时面板会显示对应的属性字段如ID、名称、Camunda扩展属性等。这里有个大坑bpmn-js-properties-panel的样式也需要单独引入并且它的样式文件可能和你项目的CSS预处理语言如Sass不兼容。你需要在入口文件或组件中引入import bpmn-js-properties-panel/dist/assets/properties-panel.css;如果遇到样式问题一个粗暴但有效的方法是直接复制其CSS内容到你的全局样式中进行修改。3.2 自定义模块与禁用功能bpmn-js是高度模块化的。我们可以通过additionalModules添加功能也可以通过覆盖模块来禁用或修改默认行为。例如我想禁用元素的直接编辑双击修改文字功能可以自定义一个空模块来覆盖原来的directEditing服务。// 自定义模块禁用直接编辑 const customDirectEditingModule { __init__: [directEditing], directEditing: [value, null] // 将directEditing服务设为null }; // 在初始化Modeler时加入 bpmnModeler.value new BpmnModeler({ container: canvasRef.value, additionalModules: [ propertiesPanelModule, propertiesProviderModule, customDirectEditingModule // 加入自定义模块 ], // ... 其他配置 });同理如果你想自定义左侧工具栏Palette可以创建一个自定义的paletteProvider模块。这涉及到对diagram-js底层机制的理解需要你继承默认的PaletteProvider并重写getPaletteEntries方法。这里篇幅所限不展开但思路是在additionalModules中提供一个高优先级的paletteProvider返回你定制的工具栏按钮配置。4. 核心定制二自定义节点渲染与上下文菜单业务系统通常不满足于标准的BPMN图形圆、方、菱形。我们可能需要为“用户任务”节点加上头像图标为“服务任务”加上API图标甚至完全改变其外观。这需要通过自定义Renderer渲染器来实现。4.1 创建自定义渲染模块bpmn-js的图形渲染是由BaseRenderer模块管理的。我们可以创建一个自定义渲染器针对特定的BPMN元素类型type返回自定义的SVGshape。首先定义一个自定义渲染模块// customRenderer.js export default function CustomRenderer(config, eventBus, styles, pathMap, canvas, textRenderer) { // 调用父类构造函数如果存在 // 这里简化实际需要注入依赖 this._config config; this._eventBus eventBus; } // 声明此模块依赖的服务 CustomRenderer.$inject [ config.customRenderer, eventBus, styles, pathMap, canvas, textRenderer ]; // 判断是否可以渲染某个元素 CustomRenderer.prototype.canRender function(element) { // 只针对‘bpmn:UserTask’类型进行自定义渲染 return element.type bpmn:UserTask; }; // 绘制形状 CustomRenderer.prototype.drawShape function(parentNode, element) { // 使用diagram-js的绘图工具 const shape this._bpmnRenderer.drawShape(parentNode, element); // 在标准图形上添加自定义内容例如一个图标 const customIcon document.createElementNS(http://www.w3.org/2000/svg, image); customIcon.setAttributeNS(http://www.w3.org/1999/xlink, href, /path/to/user-icon.svg); customIcon.setAttribute(width, 20); customIcon.setAttribute(height, 20); customIcon.setAttribute(x, 10); customIcon.setAttribute(y, 5); parentNode.appendChild(customIcon); return shape; }; // 获取形状的路径定义可选用于更复杂的自定义图形 CustomRenderer.prototype.getShapePath function(shape) { // 返回自定义的SVG Path数据 };然后在Vue组件中引入并注册这个自定义模块import CustomRenderer from ./customRenderer; bpmnModeler.value new BpmnModeler({ container: canvasRef.value, additionalModules: [ // ... 其他模块 { __init__: [customRenderer], customRenderer: [type, CustomRenderer] } ] });这种做法需要对diagram-js的渲染机制有较深理解并且要小心处理与默认渲染器的优先级关系。一个更简单实用的方法是使用叠加层Overlays。4.2 使用Overlays添加自定义内容bpmn-js提供了overlays服务可以在任意图形元素上附加HTML内容。这非常适合添加轻量级的图标、标签或按钮。import { ref } from vue; const addCustomOverlay (elementId: string, content: string) { if (!bpmnModeler.value) return; const overlays bpmnModeler.value.get(overlays); const overlayId overlays.add(elementId, { position: { top: -10, right: -10 }, html: div classcustom-badge${content}/div }); // 保存overlayId便于后续更新或移除 return overlayId; }; // 示例为所有用户任务添加一个“高优”角标 const markHighPriorityTasks () { if (!bpmnModeler.value) return; const elementRegistry bpmnModeler.value.get(elementRegistry); const userTasks elementRegistry.filter((element: any) element.type bpmn:UserTask); userTasks.forEach((task: any) { addCustomOverlay(task.id, 高优); }); };在图形被创建或更新后可以监听element.changed事件调用markHighPriorityTasks就能在每个用户任务右上角看到一个“高优”角标。你可以通过CSS完全控制这个角标的外观。4.3 自定义上下文菜单右键菜单默认的上下文菜单可能包含“删除”、“复制粘贴”等操作。我们可能需要增加像“设置处理人”、“查看历史”这样的业务操作。这需要通过自定义contextPad上下文面板和popupMenu弹出菜单来实现。以自定义contextPad为例我们需要创建一个模块来提供新的上下文按钮。// customContextPad.js export default function CustomContextPad(contextPad, modeling, elementFactory, connect, create) { this._contextPad contextPad; this._modeling modeling; // 覆盖原有方法或注册新的提供器 contextPad.registerProvider(this); } CustomContextPad.$inject [ contextPad, modeling, elementFactory, connect, create ]; CustomContextPad.prototype.getContextPadEntries function(element) { const actions {}; // 保留原有的删除动作 if (element.type ! bpmn:Process) { actions[delete] { group: edit, className: bpmn-icon-trash, title: 删除, action: { click: (event, element) { this._modeling.removeElements([element]); } } }; } // 添加自定义动作为任务设置处理人 if (element.type bpmn:UserTask) { actions[set-assignee] { group: custom, className: bpmn-icon-user, // 可以使用自定义图标类 title: 设置处理人, action: { click: (event, element) { // 这里可以触发一个Vue组件内的模态框或事件 console.log(为任务设置处理人:, element.id); // 例如emit(show-assignee-dialog, element) } } }; } return actions; };在Vue组件中注册这个模块import CustomContextPad from ./customContextPad; bpmnModeler.value new BpmnModeler({ container: canvasRef.value, additionalModules: [ // ... 其他模块 CustomContextPad ] });这样当你在一个用户任务上右键时上下文菜单里就会多出一个“设置处理人”的按钮。点击它可以触发我们在Vue组件中定义的方法从而打开一个表单对话框实现业务逻辑的闭环。5. 核心定制三业务属性扩展与数据持久化BPMN标准属性如id, name远不能满足业务需求。我们需要为任务节点扩展“处理人”、“截止时间”、“表单Key”等业务属性。这涉及到BPMN的扩展模型Moddle Extension。5.1 定义自定义属性扩展我们需要创建一个JSON文件定义自己的命名空间和属性。例如创建一个custom.json{ name: Custom, uri: http://custom-bpmn, prefix: custom, xml: { tagAlias: lowerCase }, types: [ { name: Process, extends: [bpmn:Process], properties: [ { name: businessKey, isAttr: true, type: String } ] }, { name: UserTask, extends: [bpmn:UserTask], properties: [ { name: assignee, isAttr: true, type: String }, { name: dueDate, isAttr: true, type: String }, { name: formKey, isAttr: true, type: String } ] } ] }这个扩展定义了一个custom命名空间并为Process和UserTask类型添加了自定义属性。isAttr: true表示这些属性会以XML属性的形式保存。5.2 在建模器中加载扩展修改建模器初始化配置加载我们的自定义扩展import customModdleDescriptor from ./custom.json; // 假设json文件已导出 bpmnModeler.value new BpmnModeler({ container: canvasRef.value, moddleExtensions: { camunda: camundaModdleDescriptor, // 原有的Camunda扩展 custom: customModdleDescriptor // 我们的自定义扩展 } });现在当你通过属性面板或者编程方式为一个用户任务设置assignee属性时这个属性会被保存到BPMN XML中类似于bpmn:userTask idTask_1 custom:assignee张三 /5.3 创建自定义属性面板提供器为了让自定义属性能在右侧属性面板中显示和编辑我们需要创建一个自定义的属性提供器Properties Provider。这需要继承自PropertiesPanel的基类或模仿其提供器结构。// customPropertiesProvider.js export default function CustomPropertiesProvider(propertiesPanel, translate) { // 调用父类方法注册提供器 propertiesPanel.registerProvider(this); } CustomPropertiesProvider.$inject [propertiesPanel, translate]; // 获取属性面板的组Groups和字段Properties CustomPropertiesProvider.prototype.getGroups function(element) { return (groups) { // 为UserTask类型添加一个“业务属性”组 if (element.type bpmn:UserTask) { // 找到原有的“常规”组在其后插入我们的组 const generalGroup groups.find(g g.id general); if (generalGroup) { const index groups.indexOf(generalGroup); groups.splice(index 1, 0, { id: custom-business, label: 业务属性, entries: [ { id: assignee, label: 处理人, modelProperty: assignee, widget: textField // 使用文本输入框 }, { id: dueDate, label: 截止时间, modelProperty: dueDate, widget: textField } ] }); } } return groups; }; };在初始化建模器时将这个自定义提供器模块加入import CustomPropertiesProvider from ./customPropertiesProvider; bpmnModeler.value new BpmnModeler({ container: canvasRef.value, propertiesPanel: { parent: propertiesRef.value }, additionalModules: [ propertiesPanelModule, // 注意顺序自定义提供器要在默认提供器之后以便覆盖或扩展 CustomPropertiesProvider, propertiesProviderModule ], moddleExtensions: { custom: customModdleDescriptor } });现在点击一个用户任务属性面板里就会出现“业务属性”组里面可以编辑“处理人”和“截止时间”字段。这些值会实时同步到BPMN模型的custom命名空间下。5.4 数据的获取、保存与回显所有操作最终都体现在BPMN XML上。我们需要提供获取当前XML和导入已有XML的方法。// 获取当前流程图XML定义 const getProcessXML async (): Promisestring { if (!bpmnModeler.value) return ; try { const { xml } await bpmnModeler.value.saveXML({ format: true }); // format: true 会美化输出 return xml; } catch (err) { console.error(保存XML失败, err); return ; } }; // 将XML字符串导入并渲染流程图 const loadProcessXML async (xmlStr: string): Promiseboolean { if (!bpmnModeler.value) return false; try { const { warnings } await bpmnModeler.value.importXML(xmlStr); if (warnings warnings.length) { console.warn(导入警告:, warnings); } // 导入成功后可以执行一些后置操作如自适应视图 bpmnModeler.value.get(canvas).zoom(fit-viewport); return true; } catch (err) { console.error(导入XML失败, err); return false; } }; // 在Vue组件中可以将getProcessXML绑定到保存按钮 const handleSave async () { const xml await getProcessXML(); if (xml) { // 调用API保存到后端 // await saveDiagramToBackend(xml); console.log(保存的XML:, xml); } };这里有一个至关重要的细节XML的编码问题。如果XML中包含中文字符确保在保存和传输过程中使用UTF-8编码。bpmn-js的saveXML方法返回的字符串是UTF-8的。但在某些老旧后端系统中可能需要显式处理。另外导入的XML字符串必须是一个完整的、符合BPMN 2.0 Schema的XML文档而不仅仅是process片段。6. 实战避坑与性能优化指南集成bpmn-js到Vue项目除了上述功能实现在实际开发中还踩过不少坑这里集中总结一下。6.1 样式隔离与冲突的终极方案前面提到了用父容器类名隔离但对于bpmn-js内部大量动态生成的、类名非常通用的元素如.djs-element项目全局样式可能无意中影响到它。最彻底的方案是利用Vue的scoped样式特性但需要通过深度选择器来穿透。style scoped /* 使用 /deep/ 或 ::v-deep 来影响子组件或动态生成的元素 */ .bpmn-engine-container ::v-deep .djs-palette { background-color: #f5f5f5; } .bpmn-engine-container ::v-deep .djs-shape:hover { stroke: #1890ff !important; } /style同时建议将bpmn-js相关的所有CSS文件通过import引入到一个全局的非scoped样式文件中并在该文件中将所有规则都嵌套在.bpmn-engine-container类下实现物理隔离。6.2 事件监听与Vue响应式数据同步bpmn-js内部使用EventBus进行通信。我们需要监听画布变化如元素添加、删除、属性修改来同步更新Vue组件的状态或触发保存。import { onMounted } from vue; const setupEventListeners () { if (!bpmnModeler.value) return; const eventBus bpmnModeler.value.get(eventBus); // 监听元素变化 eventBus.on(element.changed, (event) { const { element } event; console.log(元素发生变化:, element.id, element.type); // 可以在这里触发自动保存需防抖或更新本地状态 }); // 监听画布变化 eventBus.on(commandStack.changed, (event) { console.log(操作栈变化表示有撤销/重做或修改); // 可以用来更新“脏数据”状态 }); // 监听选择变化 eventBus.on(selection.changed, (event) { const { newSelection } event; console.log(当前选中的元素:, newSelection); // 可以同步到Vue的data中用于控制属性面板的显示 }); }; onMounted(() { // ... 初始化建模器 setupEventListeners(); });注意eventBus的回调函数是脱离Vue响应式系统的。如果需要在回调中修改Vue的ref或reactive数据可能需要使用nextTick或确保在正确的上下文中。6.3 处理大量节点时的性能问题当流程图非常复杂节点和连线数量庞大例如超过200个元素时渲染和操作可能会变慢。可以采取以下优化措施懒加载/分步渲染对于超大型流程图可以考虑只渲染可视区域内的元素。但这需要修改bpmn-js的渲染逻辑难度较大。一个更可行的方案是后端生成流程图图片前端用bpmn-js的查看器BpmnViewer而非建模器BpmnModeler查看器性能更好。禁用非必要模块在初始化建模器时只引入必需的模块。例如如果不需要属性面板、标签编辑、某些交互就不要引入对应的模块。优化操作反馈在监听commandStack.changed进行自动保存时务必加上防抖debounce避免频繁的异步请求。谨慎使用Overlays每个Overlay都是一个DOM元素大量使用会影响性能。考虑是否可以用自定义渲染修改SVG来代替。6.4 与后端工作流引擎的对接如果你的流程图最终要用于驱动一个像Camunda、Flowable或Activiti这样的工作流引擎那么XML中的扩展属性就必须严格符合引擎的规范。例如Camunda引擎识别的是camunda:assignee属性而不是我们自定义的custom:assignee。因此在定义自定义扩展时最好直接复用目标引擎的命名空间和属性名或者在后端保存前进行一次XML转换将custom:命名空间的属性映射到引擎识别的命名空间下。6.5 版本兼容性与打包大小bpmn-js及其相关依赖包体积不小。使用Vite或Webpack打包时要注意tree-shaking是否生效。检查最终打包产物确保没有引入未使用的模块。另外bpmn-js不同版本间的API可能有细微变化建议在package.json中锁定一个稳定的主版本升级时仔细阅读变更日志。最后分享一个我常用的调试技巧在浏览器控制台中通过$0获取当前选中的DOM元素然后输入$0.__vue__或直接查看其父级组件可以快速定位Vue组件实例。而对于bpmn-js内部状态可以在控制台通过window.bpmnModeler bpmnModeler.value将实例挂到全局然后直接调用bpmnModeler.get(elementRegistry).getAll()等方法查看所有元素对于排查问题非常方便。