ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue3集成Bpmn-js:打造可定制流程设计器实战

Vue3集成Bpmn-js:打造可定制流程设计器实战 1. 项目背景与方案选型1.1 为什么要在 Vue3 里选 Bpmn-js做流程类系统绕不开流程设计器。不管是审批流、工单流转还是业务编排前端总需要一个能拖拽节点、连线、设置属性、生成标准流程文件的画布。我把技术栈锁在 Vue3 上之后调研了一圈可用的流程设计器方案最后落在 Bpmn-js 上原因很实在它是目前开源社区里对 BPMN 2.0 标准支持最完整的库没有之一。有人会问项目里已经有很多现成的流程设计器比如基于 Ant Design 的 Flowable 模型设计器或者各种自研的 JSON 流程方案为什么还要自己用 Bpmn-js 再搭一遍我的答案是自研方案通常只能满足自己系统的 JSON 格式数据出不去也进不来而 Bpmn-js 操作的是标准 BPMN 2.0 XML这个格式是行业通用的后端引擎能解析、其他建模工具能打开、甚至跨团队协作时也能直接交换文件。如果你的流程引擎基于 Activiti、Flowable 或者 Camunda那 Bpmn-js 基本就是标配。再补充一点技术层面的理由Bpmn-js 的架构是“核心画布 模块扩展”的插件化设计内部通过依赖注入组织各个模块。这意味着你可以在不修改核心代码的情况下自定义渲染器、自定义属性面板、自定义工具栏定制自由度非常高。相比那些封装成黑盒的组件库Bpmn-js 更适合做二次封装和深度定制这也是我最后选择了它的核心原因。1.2 BPMN 规范基础与设计器能做什么很多刚接触流程设计的同学会把 BPMN 和“画流程图”划等号这个理解不完整。BPMN 是 Business Process Model and Notation 的缩写它不是简单的绘图格式而是一套可执行、可语义解析的流程建模标准。BPMN 2.0 定义了流程中每个元素的含义——事件、活动、网关、顺序流、泳道等这些元素组合起来表达的不只是一幅图而是一个可执行的业务逻辑模型。我们在 Vue3 里集成的 Bpmn-js本质上是一个完整的 BPMN 2.0 建模工具。它能做的不只是拖几个矩形和箭头可以导入标准 XML 文件可以拖拽各种事件节点、任务节点、网关节点可以配置每个节点的属性比如任务类型、表单标识、负责人策略可以连接节点并设置条件流可以校验流程合法性还能把最终结果导出成 XML 或 SVG交给后端流程引擎去执行。这套设计器适合谁用如果你是做低代码平台、审批系统、工作流引擎配套前端的开发者或者你需要在自己产品里嵌入一套可定制的流程建模能力这篇文章的方法可以直接用。如果你只是临时画个流程图那用现成的在线绘图工具可能更快Bpmn-js 的优势在于“模型可执行、数据可流转”。2. 环境搭建与依赖集成2.1 初始化 Vue3 项目与版本选型我这边的项目用的是 Vite Vue3 TypeScript 的组合。初始化项目很简单一行命令npm create vitelatest bpmn-designer -- --template vue-ts这里有个细节值得注意Bpmn-js 对 Vue 的版本没有硬性依赖它是纯 JavaScript 库跑在 DOM 层不依赖 Vue 的响应式系统因此在 Vue2、Vue3 里都能用。但我在实操中强烈建议搭配 Vue3 的 Composition API 来封装因为 Bpmn-js 实例的创建、事件监听、销毁这一套生命周期用onMounted和onBeforeUnmount来管理非常顺滑比 Options API 的mounted钩子更符合逻辑聚合的原则。遇到一个新手容易踩的坑如果项目里混用了 Vue2 的全局挂载方式比如Vue.use()注册了一个插槽插件很容易把 Bpmn-js 的初始化搞乱。我的做法是把 Bpmn-js 的集成代码单独放在src/components/BpmnDesigner目录下面内部自行管理生命周期不依赖任何全局插件这样无论是嵌入现有后台管理系统还是独立运行都能保证干净隔离。依赖版本方面截至我写这篇文章时bpmn-js 的稳定版本已经到 16.x 甚至 17.x 了安装时建议直接装最新版npm install bpmn-js同时为了后续的样式和图标支持还建议安装npm install diagram-js bpmn-js-properties-panel bpmn-io/properties-panel其中bpmn-js-properties-panel是官方属性面板扩展后来被拆成了两部分核心组件库和样式包要分开引入这个细节很多人第一次都会卡住。2.2 Bpmn-js 核心模块结构解析Bpmn-js 最需要理解的是它的模块体系。简单来说它内部由 diagram-js 提供底层的图形渲染和交互能力bpmn-js 本身在 diagram-js 之上构建了 BPMN 语义层的建模逻辑。使用时通过extraModules参数可以向内部注册自定义模块这个机制被大量用于功能扩展。我初次接触时最大的困惑点就在这为什么new BpmnModeler()之后默认就拥有了移动、连线、删除这些能力因为 bpmn-js 在底层自动加载了一系列默认模块比如 canvas、overlays、modeling、palette、context-pad、keyboard 等。这些模块各司其职canvas 负责管理画布和 viewportmodeling 负责图形元素的创建和修改palette 负责左侧工具栏节点面板context-pad 负责元素选中时弹出的上下文菜单keyboard 负责快捷键操作理解这个结构对后续做定制非常关键。比如你要自定义左侧节点面板本质上不是改 CSS而是重写 palette 模块你要给节点增加右键菜单就要扩展 context-pad。搞清模块边界后面改起来就很有方向感。3. 核心功能实现从空白画布到可编辑流程图3.1 创建画布与加载 BPMN 文件万事开头难Bpmn-js 集成第一步是把画布渲染到页面上。在 Vue3 组件里我通常在模板中放一个 div 作为容器template div classbpmn-container div refcanvasRef classcanvas/div /div /template然后在onMounted里初始化 Modelerimport { 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 canvasRef refHTMLElement() let modeler: BpmnModeler | null null onMounted(async () { modeler new BpmnModeler({ container: canvasRef.value!, keyboard: { bindTo: document } }) // 创建一个最简单的空白流程图 const emptyBpmn ?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:definitions await modeler.importXML(emptyBpmn) // 导入成功后让画布自适应居中 const canvas modeler.get(canvas) canvas.zoom(fit-viewport) })这里有几个细节值得展开。第一importXML返回的是一个 Promise最好用await处理这样能捕获 XML 解析错误。第二导入成功之后fit-viewport很重要不然空白 XML 导入后画布可能是空的或者位置不对。第三keyboard: { bindTo: document }这一步决定了快捷键是否能全局生效我试过不传这个参数键盘事件在某些浏览器下就没响应这个问题后面排查了半天。生命周期销毁也别忘了onBeforeUnmount(() { modeler?.destroy() modeler null })3.2 新手必看快速封装一个可复用的工具栏画布渲染出来之后接下来自然会想到加工具栏。Bpmn-js 本身不自带 UI 工具栏它给的是功能接口按钮样式和布局全靠自己搭。我封装了一套工具栏创建新流程、打开本地 XML、保存并下载 XML、导出 SVG、撤销、重做、放大、缩小、自动适应画布。核心代码如下// 工具栏事件处理 function handleCreate() { const emptyBpmn createEmptyBpmn() modeler!.importXML(emptyBpmn) } function handleOpen() { const fileInput document.createElement(input) fileInput.type file fileInput.accept .xml,.bpmn fileInput.onchange async (e: Event) { const file (e.target as HTMLInputElement).files![0] const text await file.text() await modeler!.importXML(text) } fileInput.click() } function handleSave() { modeler!.saveXML({ format: true }).then(({ xml }) { const blob new Blob([xml], { type: application/xml }) const url URL.createObjectURL(blob) const a document.createElement(a) a.href url a.download process.bpmn a.click() URL.revokeObjectURL(url) }) } function handleUndo() { const undo modeler!.get(commandStack) undo.undo() } function handleZoomIn() { const canvas modeler!.get(canvas) canvas.zoom({ x: 0, y: 0 }, 1.2) }注意撤销、重做走的是commandStack模块而不是自己维护一个历史堆栈。Bpmn-js 内部已经做了命令记录你只需要调用它的undo()和redo()。缩放则通过 canvas 模块的zoom()方法控制参数可以传绝对比例值也可以传缩放方向。实际体验下来工具栏部分最影响使用感受的是“导出”按钮的交互。很多用户导出 SVG 是为了贴到文档里所以我还额外做了一版“下载高清 PNG”的功能。原理是通过 SVG 序列化后绘制到 canvas 再转 PNG遇到元素过多时可能会模糊需要注意设置缩放比例。3.3 节点面板与元素拖拽Bpmn-js 自带的左侧 palette节点面板默认支持常见元素开始事件、结束事件、任务、网关、中间事件等。如果不做定制直接用默认面板也够用但产品要求高的话一般都会根据业务瘦身比如只保留“审批人任务”“条件网关”“开始/结束事件”这几个。自定义 palette 有两种思路。第一种是通过paletteProvider重写整个面板第二种是用additionalModules扩展默认面板只增删部分元素。我习惯走第二种因为改动量小还能保留原有的拖拽逻辑。import { customPaletteProvider } from ./custom-palette const modeler new BpmnModeler({ container: canvasRef.value!, additionalModules: [ { paletteProvider: [type, customPaletteProvider] } ] })自定义 paletteProvider 的核心是重写getPaletteEntries()方法返回你期望的节点映射表。每个条目包含action点击行为、title悬浮提示、className图标类名。节点拖拽到画布上本质上是调用create操作这是 diagram-js 内部能力不需要你手动处理。这里有一个巨坑自定义 paletteProvider 如果写了类型声明最好用any放宽类型因为 bpmn-js 内部模块接口的 TypeScript 定义经常跟随版本变动精确类型反而会导致编译报错。我在升级 bpmn-js 版本后遇到过PaletteProvider接口变化导致类型不兼容的问题后来统一用// ts-ignore兜底把精力放在功能实现上。4. 进阶玩法与实践细节4.1 流程数据导入导出与 XML 解析流程设计器最核心的数据交换格式就是 BPMN 2.0 XML。保存和读取看似简单里面有几个细节如果不处理后续流程引擎对接时会被坑。第一个细节是“扩展属性”的处理。BPMN XML 在标准的流程节点之外经常需要携带业务自定义属性比如任务节点的审批人类型、表单地址、超时时间。Bpmn-js 的建模引擎在导入导出时会保留节点上的业务对象businessObject但如果你通过非正规手段给节点加自定义属性导出后可能丢失。正确做法是使用moddle扩展来定义自定义属性。第二个细节是命名空间。一个标准的 BPMN XML 头里包含多个命名空间bpmn、bpmndi、dc、di等。如果你在后端拼 XML 时漏了bpmndi命名空间Bpmn-js 导入时虽然不会崩溃但流程图的坐标信息DI 信息会丢失画布上一片空白。我自己就踩过这个坑后来排查到是后端返回的 XML 里缺了 DI 部分。// 导出带格式的 XML必要时保留 XML 声明 const { xml } await modeler.saveXML({ format: true }) const resultXml ?xml version1.0 encodingUTF-8?\n${xml}第三个细节是导入前的校验。Bpmn-js 自带importXML的错误回调能解析出 XML 节点的合法性问题但它不会校验“一个流程是否只有一个开始事件”“结束事件数量是否大于零”这些业务规则。所以在导入外部文件时最好先用moddle解析 XML 并走一遍基本规则校验给用户明确提示。4.2 自定义渲染让节点更改样式与形状默认的节点样式比较朴素很多项目希望让任务节点呈现不同的颜色、圆角甚至替换成自定义形状。Bpmn-js 提供了渲染器扩展机制通过customRenderer模块重写getShapePath和getShapeStyle方法。我举个业务例子在审批流里我想把“会签任务”渲染成黄色背景、红色边框“或签任务”渲染成蓝色背景。实现思路如下class CustomRenderer extends BaseRenderer { constructor(eventBus, bpmnRenderer) { super(eventBus, 2000) this.bpmnRenderer bpmnRenderer } canRender(element) { return element.type bpmn:UserTask || element.type bpmn:ServiceTask } drawShape(parentNode, element) { const shape this.bpmnRenderer.drawShape(parentNode, element) const color element.businessObject.$attrs[custom:color] || #fff const rect shape.getBoundingClientRect() // 这里可以做更精细的 SVG 操作 shape.style.fill color return shape } }这个方案的关键点是继承BaseRenderer并设置渲染优先级数字越大越优先。canRender决定哪些元素走自定义渲染其余元素回落到默认渲染器不干扰标准元素。我在做自定义渲染时最大的体会是尽量只改 SVG 的样式属性不要轻易修改整体绘制路径否则流程图中节点间的连接线位置可能对不齐排查起来相当费劲。4.3 属性面板如何让流程配置回归可视化一个脱离属性配置的流程设计器只能算画板真正要能驱动流程引擎执行必须让用户能配置每个节点的业务属性。这里我选了官方推荐的bpmn-js-properties-panel做基础再通过 camunda 的 moddle 扩展定义自定义属性项。安装依赖时已经提到新版属性面板拆分成两部分除了核心库之外还需要引入样式import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from bpmn-js-properties-panel import propertiesPanelCSS from bpmn-js-properties-panel/dist/assets/properties-panel.css在 Modeler 初始化时注册const modeler new BpmnModeler({ container: canvasRef.value!, additionalModules: [ BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, ], propertiesPanel: { parent: propertiesPanelRef.value! } })这里有个布局上的坑属性面板的容器必须和画布容器是同级别的 DOM 节点而且要在初始化之前就渲染到页面上。如果面板容器是 v-if 控制显隐的初始化之后才插入 DOM面板会挂载失败控制台也不容易看出原因。我最后是在外层写死了两列布局左侧画布、右侧面板保证两者始终存在。属性面板的配置项是通过getPropertiesProvider或者自定义 provider 来生成的。官方提供的默认面板能编辑 id、name、documentation 等基础内容但远远不够业务用。我自定义了“审批人策略”“超时时间”“是否允许撤回”这些字段它们最终会被序列化到 XML 的extensionElements区域。4.4 节点中的网关、事件与条件流流程图中最容易让人混淆的就是网关和中间事件。在 Bpmn-js 里的建模逻辑并不复杂但新手往往会漏掉一个关键设置——条件流。在 BPMN 规范里排他网关ExclusiveGateway的后续所有顺序流必须定义conditionExpression否则流程引擎不知道走哪条分支。在 Bpmn-js 画布上选中一条连线右侧属性面板里可以设置条件表达式但默认面板对表达式的编辑支持并不友好。我给连线扩展了两类条件一类是自定义脚本表达式比如${amount 5000}另一类是流程变量比较。扩展方式同样是注册一个自定义属性 tab在属性面板里新增条目。另一个常见问题是子流程的展开与折叠。Bpmn-js 对子流程SubProcess有基本的边角展开操作双击可以展开到子流程内部编辑。如果业务上不需要子流程建议从 palette 里移除该入口避免用户画出无法执行的嵌套流程。我自己做审批场景时就是这么干的流程层级一多校验复杂度成倍增加。5. 常见问题与踩坑实录5.1 版本兼容问题影响最大Bpmn-js 的版本迭代速度很快大版本之间 API 会有破坏性变化。我遇到过最典型的是bpmn-js13升级到16时属性面板的依赖模块从bpmn-js-properties-panel旧版本切到了新架构很多基于旧版自定义面板的代码直接失效。因此上线项目中一定要锁版本号不要用^范围号随意升级否则一次npm install就可能让整个设计器罢工。保险做法是在package.json里锁定精确版本甚至使用package-lock.json提交到仓库。对于新功能升级单独拉分支测试通过后再合并。5.2 样式污染与冲突Bpmn-js 自带一套洗练的 CSS但它的类名是全局的比如djs-container、djs-canvas等。如果项目里同时引入了其他 UI 库或全局样式很容易出现覆盖问题。比如某些后台管理系统中全局设置了对svg的样式重置导致流程图的连接线被加粗或者箭头消失。排查方法很直接用浏览器 DevTools 检查对应 SVG 元素的最终样式看是哪一条全局规则影响了它。解决方案是给 Bpmn-js 容器加一个私有命名空间类比如.bpmn-editor然后在自己的样式表里针对这个命名空间下的svg做样式隔离和覆盖。还有一个国产化环境常见的坑部分老旧浏览器或特定内核浏览器对 SVG 的某个特性支持不完整导致渲染异常。Bpmn-js 整体对浏览器兼容要求不高但遇到画布空白时优先检查控制台是否有 SVG 相关的错误日志。5.3 内存泄漏与组件销毁大型单页应用里切换路由如果不销毁 BpmnModeler 实例内存会持续攀升页面越来越卡。前面说过在onBeforeUnmount里调用modeler.destroy()这是必须的一步。但只做这一步还不够如果注册了自定义事件监听、keyboard事件绑定到了全局 document 上销毁 Bpmn-js 时这些监听不会自动清理。我的处理方法是所有自定义事件监听器在销毁前手动移除keyboard配置的bindTo不要指向全局 document尽量限定在画布容器内有 setTimeout 定时任务比如自动保存流程组件卸载时也要清掉。这样能基本杜绝内存泄漏问题。还有一个容易忽略的地方如果用了v-show控制设计器显示画布容器并未真正销毁只是 CSS 隐藏。这种情况下 Bpmn-js 实例不会自动销毁要自己判断是否需要按逻辑重置。5.4 常见问题速查表问题现象可能原因解决方案导入 XML 后画布空白XML 缺少 DI 信息或命名空间错误检查 XML 头与实际节点是否完整快捷键无效keyboard 没有绑定到 document初始化时配置keyboard: { bindTo: document }属性面板不显示面板容器未提前渲染确保父容器在初始化前已挂载自定义节点后连线错位渲染器返回的 getShapePath 偏差优先只改样式不重画路径保存的 XML 引擎不能解析缺少必要命名空间或扩展属性定义使用 moddle 扩展自定义属性不要手动拼 XMLVite 启动时报 CSS 注入错误新版本 bpmn-js 需要特定样式包检查是否需要显式引入 properties-panel 样式6. 拓展延伸如何把设计器接入后台系统6.1 组件化封装与状态管理如果只是单独一个页面演示把代码全写在组件里没问题但接入真正的后台管理系统时需要把 Bpmn-js 流程设计器封装成可复用组件。我的做法是抽象出三个模块BpmnDesigner.vue负责画布渲染、工具栏、导入导出等基础能力BpmnPropertiesPanel.vue负责属性面板并且通过事件向父组件抛送节点选中、属性变更通知useBpmnDesigner.ts组合式函数封装 modeler 实例、常用操作和状态组合式函数是 Vue3 比较方便的部分。比如我封装了一个useBpmnDesigner它接收容器 ref 和初始化配置返回 modeler 实例以及importXml、saveXml、undo、redo等方法。各个组件引用了同一个 hook就能保证画布和属性面板操作的是同一个 modeler 实例避免出现“画布上有节点但属性面板一片空白”的经典问题。6.2 与后端流程引擎的数据协同前端流程设计器只是流程生命周期的一部分。用户设计完流程之后XML 需要保存到后端由流程引擎解析并部署。这里我建议前端保存的 XML 必须经过一次“标准净化”去除空节点、补齐缺失命名空间、压缩无意义空白。否则后端引擎解析时很容易因为非标准内容报错。我与后端协作时的接口设计通常是两个一个接口保存 BPMN XML另一个接口查询已部署的流程定义列表。前端加载已经存在的流程时直接拿后端返回的 XML 调用importXML即可。需要注意跨域、编码和特殊字符转义问题尤其是 XML 内容中存在中文或特殊符号时一定要确保后端以 UTF-8 编码返回。如果想做得更完善可以加一个“校验并预览”的环节前端保存之前调用后端提供的模拟执行接口把流程跑一遍看是否走到某个任务节点卡死。这个能力对复杂分支非常有用但实现成本较高可以作为二期迭代计划。6.3 后续扩展思路Bpmn-js 的扩展点还有很多值得尝试的方向集成自研的表单设计器任务节点的表单地址直接指向你已有的表单组件流程版本对比基于 XML diff 做流程版本差异可视化协作与评论在泳道中增加评论锚点多人协同评审流程流程仿真模拟给节点配置随机耗时和分支概率模拟流程整体耗时分布我在实际项目中已经做到了前两个后面的仿真模拟是正在探索的方向。Bpmn-js 社区里也有很多成熟的插件可供参考比如 bpmn-js-nyan、bpmn-js-theme 等思路都是通过额外模块注入实现差异效果。我个人在实际操作中的体会是Bpmn-js 的学习曲线不是陡峭在 API 使用而是陡峭在理解“建模引擎 渲染引擎 模块注入”这三层架构逻辑。一旦理解了模块化的边界后续几乎所有功能需求都能顺着这个体系找到扩展点。最后再分享一个小技巧调试 Bpmn-js 时在控制台打印modeler._components虽然组件内部是下划线前缀能直接看到当前注册了哪些模块排查问题快得不是一点半点。希望这篇文章能帮你少踩几个坑。
返回列表