尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Vue3集成bpmn-js与Activiti工作流设计器实战

发布时间:2026/9/29 17:36:44

资讯中心
01
ARTICLE

Vue3集成bpmn-js与Activiti工作流设计器实战

Vue3集成bpmn-js与Activiti工作流设计器实战
1. 为什么要在 Vue3 里折腾工作流设计器先说说我为什么会碰这块东西。去年接了一个企业内部审批系统的活儿需求方张口就要“能拖拽画流程图、能配审批人、能跟后端流程引擎联动”。当时第一反应是找个现成的低代码平台套一套但预算和定制化程度都不允许最后还是老老实实自己撸。技术选型上前端是 Vue3 Vite TypeScript后端是 Spring Boot 挂 Activiti 7中间靠 bpmn-js 做流程设计器。这套组合在国内中小型项目里其实非常常见尤其是做 OA、ERP、审批流这类后台管理系统。bpmn-js 负责前端把 BPMN 2.0 标准的流程图“画”出来Activiti 负责后端把这张图“跑”起来Vue3 负责把两者串起来。听起来简单但真正落地的时候坑主要集中在三块bpmn-js 在 Vue3 里的集成方式、BPMN XML 与 Activiti 的字段兼容、以及前后端流程定义的版本同步。这篇文章我打算把整个集成过程从头到尾讲一遍包括环境搭建、设计器封装、属性面板定制、XML 导出、与 Activiti 的对接、以及我踩过的那些坑。适合正在做 Vue3 后台管理系统、需要集成工作流引擎的开发者也适合想了解 bpmn-js 怎么在 Vue3 里用的人。不管你是刚接触工作流的新手还是已经用过 Activiti 但没搞过前端设计器的老手应该都能从里面找到能直接抄的东西。2. 技术选型与整体架构拆解2.1 为什么是 bpmn-js 而不是 LogicFlow热词里有人提到用 LogicFlow 做类似 Dify 的流程图LogicFlow 确实好用但它的定位是“流程图”不是“BPMN 流程设计器”。这两者差别很大。LogicFlow 画出来的图是自定义数据结构你要自己定义节点、连线、属性然后自己写解析逻辑。而 bpmn-js 直接遵循 BPMN 2.0 标准画出来的 XML 可以直接丢给 Activiti、Flowable、Camunda 这些引擎执行不需要你做任何转换。如果你的后端是 Activiti那前端用 bpmn-js 几乎是唯一合理的选择。因为 Activiti 对 BPMN XML 的解析非常严格你自己用 LogicFlow 拼出来的 XML 大概率会在部署时报各种命名空间错误、元素缺失错误。bpmn-js 生成的 XML 天然符合规范省掉大量调试时间。另一个原因是 bpmn-js 的生态。它本身是 bpmn.io 团队维护的配套有 bpmn-js-properties-panel、bpmn-moddle、bpmn-js-token-simulation 等一堆工具属性面板、XML 解析、流程模拟都有现成方案。你自己从零搭一套工作量至少翻三倍。2.2 Vue3 与 bpmn-js 的集成方式选择bpmn-js 是一个纯 JavaScript 库不依赖任何框架。在 Vue3 里集成它核心问题是怎么管理它的生命周期。我试过两种方式第一种是直接在onMounted里 new 一个 Modeler 实例挂到 ref 上onUnmounted时销毁。这种方式最简单适合单个设计器页面。第二种是封装成一个 Vue 组件通过 props 接收初始 XML通过 emit 抛出变更事件。这种方式适合设计器需要在多个页面复用、或者需要跟表单联动的场景。我最后选的是第二种因为审批系统里流程设计器不止一个入口有时候还要在弹窗里打开。封装成组件之后外部只需要传一个xml字符串内部处理好初始化和销毁用起来很干净。2.3 整体数据流设计整个系统的数据流是这样的前端 bpmn-js 设计器负责生成 BPMN XML用户点保存时把 XML 字符串发给后端后端 Activiti 的 RepositoryService 接收 XML调用deploy方法部署流程定义部署成功后返回流程定义 ID 和版本号前端拿到版本号后更新本地状态。运行时前端发起审批时传流程定义 key 和业务表单数据后端用 RuntimeService 启动流程实例后续的节点流转全部由 Activiti 驱动。前端只负责展示当前节点、审批记录和待办列表。这个数据流的关键在于BPMN XML 是前后端唯一的契约。前端画什么后端就执行什么。所以 XML 的规范性至关重要任何自定义属性都必须放在 Activiti 能识别的命名空间里否则部署时会被忽略甚至报错。3. 环境搭建与依赖安装实操3.1 Vue3 项目初始化如果你还没有 Vue3 项目用 Vite 创建一个是最快的。我习惯用 pnpm速度比 npm 快不少pnpm create vite vue3-workflow --template vue-ts cd vue3-workflow pnpm install创建完之后先别急着装 bpmn-js先把基础环境跑通。pnpm dev能正常启动浏览器能打开默认页面再往下走。这一步看起来废话但我见过太多人环境还没跑通就开始装一堆依赖最后报错都不知道是哪一层的问题。Vue3 的版本建议用 3.4 以上Vite 用 5.x。TypeScript 一定要开bpmn-js 的类型定义虽然不完美但有类型提示总比没有强。如果你用的是若依 Vue3 版本或者 JeecgBoot 的 Vue3 前端注意它们的 Vite 配置可能有自定义的 alias 和 proxy装 bpmn-js 之前先确认resolve.alias里指向的是src目录。3.2 bpmn-js 相关依赖安装核心依赖有这几个pnpm add bpmn-js bpmn-js-properties-panel bpmn-io/properties-panel bpmn-moddle pnpm add -D types/bpmn-js这里解释一下每个包的作用。bpmn-js是核心库提供 Modeler、Viewer、NavigatedViewer 三个类。bpmn-js-properties-panel是属性面板用来编辑节点的审批人、条件表达式等。bpmn-io/properties-panel是新版属性面板的底层组件库bpmn-js-properties-panel 依赖它。bpmn-moddle用来读写 BPMN 的 moddle 扩展Activiti 的自定义属性就靠它。版本上有个坑要注意bpmn-js 从 9.x 开始属性面板的 API 有较大变化。如果你在网上搜到的教程是 7.x 或 8.x 的直接照搬会报错。我写这篇文章时用的是 bpmn-js 17.x bpmn-js-properties-panel 5.x这个组合是目前比较稳定的。3.3 Activiti 后端环境准备后端这块我不展开讲 Spring Boot 的搭建重点说 Activiti 的依赖和配置。Maven 里加dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependencyActiviti 7 的版本号比较特殊正式版一直没出M6 是社区用得最多的。如果你用的是 Flowable 或 CamundaAPI 略有不同但 BPMN XML 是通用的。application.yml 里配置数据库和 Activiti 的自动部署spring: datasource: url: jdbc:mysql://localhost:3306/workflow?useUnicodetruecharacterEncodingutf8 username: root password: 123456 activiti: database-schema-update: true check-process-definitions: false process-definition-location-prefix: classpath:/processes/database-schema-update: true会在启动时自动创建 Activiti 的 25 张表开发环境很方便生产环境建议改成false手动管理。check-process-definitions: false是关掉自动扫描因为我们走的是前端上传 XML 动态部署不需要把流程文件放在 resources 目录。4. bpmn-js 设计器在 Vue3 中的封装4.1 基础 Modeler 初始化先写一个最简版本把设计器跑起来。新建src/components/BpmnDesigner/index.vuetemplate div refcanvasRef classbpmn-canvas/div /template script setup langts import { ref, onMounted, onUnmounted } 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 refHTMLDivElement() let modeler: BpmnModeler | null null const initModeler async () { modeler new BpmnModeler({ container: canvasRef.value!, keyboard: { bindTo: document } }) await modeler.createDiagram() } onMounted(() { initModeler() }) onUnmounted(() { modeler?.destroy() modeler null }) /script style scoped .bpmn-canvas { width: 100%; height: 600px; border: 1px solid #dcdfe6; } /style这段代码跑起来之后你应该能看到一个空的 BPMN 画布左侧有工具栏可以拖拽开始事件、任务、网关这些元素。如果画布是空白的八成是 CSS 没引入检查diagram-js.css和bpmn.css这两行。keyboard: { bindTo: document }这行是让快捷键生效比如 CtrlZ 撤销、CtrlY 重做。不配的话快捷键只能在画布聚焦时用体验很差。4.2 属性面板集成光有画布不够审批流需要给每个节点配审批人、条件表达式。这就需要属性面板。bpmn-js-properties-panel 的集成稍微麻烦一点import BpmnModeler from bpmn-js/lib/Modeler import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from bpmn-js-properties-panel import ActivitiModdleDescriptor from activiti-bpmn-moddle/resources/activiti.json modeler new BpmnModeler({ container: canvasRef.value!, propertiesPanel: { parent: #properties-panel }, additionalModules: [ BpmnPropertiesPanelModule, BpmnPropertiesProviderModule ], moddleExtensions: { activiti: ActivitiModdleDescriptor } })这里有几个关键点。propertiesPanel.parent指向一个 DOM 选择器你需要在模板里加一个div idproperties-panel/div作为面板容器。moddleExtensions里的activiti是告诉 bpmn-js除了标准 BPMN 元素外还要识别 Activiti 的扩展属性比如activiti:assignee、activiti:candidateUsers。activiti-bpmn-moddle这个包需要单独装pnpm add activiti-bpmn-moddle如果你用的是 Flowable对应的包是flowable-bpmn-moddledescriptor 文件路径类似。这个 descriptor 本质上是一个 JSON 描述文件定义了 Activiti 命名空间下有哪些属性、什么类型、默认值是什么。bpmn-js 靠它来正确序列化和反序列化 XML。4.3 自定义属性面板扩展默认的属性面板只有 General、Documentation、Execution 这几个分组审批人配置藏在 Execution 里对业务人员不友好。我一般会自定义一个“审批配置”分组把 assignee、candidateUsers、dueDate 这些常用字段拎出来。自定义属性面板需要用到bpmn-io/properties-panel的组件。写一个 providerimport { is } from bpmn-js/lib/util/ModelUtil import { TextFieldEntry, isTextFieldEntryEdited } from bpmn-io/properties-panel function ApprovalPropsProvider(propertiesPanel: any) { propertiesPanel.registerProvider(500, { getGroups(element: any) { return (groups: any[]) { if (is(element, bpmn:UserTask)) { groups.push({ id: approval, label: 审批配置, entries: [ { id: assignee, component: TextFieldEntry, isEdited: isTextFieldEntryEdited, label: 审批人, modelProperty: assignee, get: (element: any) ({ assignee: element.businessObject.assignee || }), set: (element: any, values: any) { element.businessObject.assignee values.assignee } } ] }) } return groups } } }) }然后在 additionalModules 里注册这个 provider。registerProvider的第一个参数是优先级数字越大越靠前500 是个中间值保证我们的分组排在默认分组后面。这段代码看起来有点绕核心逻辑就是当选中元素是 UserTask 时往属性面板的分组列表里塞一个自定义分组分组里的每个 entry 定义了怎么读值、怎么写值。get从 businessObject 里取值set把值写回 businessObject。bpmn-js 会在用户修改时自动触发 XML 更新。5. BPMN XML 的导出、导入与 Activiti 对接5.1 XML 导出与格式化用户点保存时需要把当前画布导出成 XML 字符串const saveDiagram async () { if (!modeler) return try { const { xml } await modeler.saveXML({ format: true }) return xml } catch (err) { console.error(导出 XML 失败, err) throw err } }format: true会让输出的 XML 带缩进和换行方便调试。生产环境如果在意传输体积可以设成 false但一般流程 XML 也就几 KB没必要省这点。导出的 XML 里会包含bpmn:、bpmndi:、activiti:等多个命名空间。bpmndi是图形信息记录每个节点的坐标、大小、连线路径。Activiti 部署时其实不需要bpmndi但保留它没坏处因为流程监控页面可能需要用图形信息来高亮当前节点。5.2 XML 导入与回显编辑已有流程时需要把后端存的 XML 加载回画布const importDiagram async (xml: string) { if (!modeler) return try { await modeler.importXML(xml) const canvas modeler.get(canvas) canvas.zoom(fit-viewport) } catch (err) { console.error(导入 XML 失败, err) } }zoom(fit-viewport)是让画布自动缩放到适应容器大小不然导入的流程可能显示在角落或者超出可视区域。这个细节很多教程都不提但实际用的时候没有它体验很差。导入失败最常见的原因是 XML 格式不合法比如缺少命名空间声明、元素未闭合、或者引用了不存在的 moddle 类型。排查方法是在 catch 里把 err 打印出来bpmn-js 的报错信息其实挺详细的会告诉你哪一行哪个元素有问题。5.3 与 Activiti 的部署接口对接前端导出 XML 后通过 HTTP 发给后端。后端 Controller 大概长这样PostMapping(/deploy) public Result deploy(RequestBody DeployRequest request) { Deployment deployment repositoryService.createDeployment() .name(request.getName()) .addString(request.getKey() .bpmn20.xml, request.getXml()) .deploy(); return Result.success(deployment.getId()); }注意addString的资源名必须以.bpmn20.xml或.bpmn结尾否则 Activiti 不会把它当作流程定义解析。这是 Activiti 的一个硬性约定我第一次踩的时候排查了半天部署成功但流程定义列表是空的就是因为资源名不对。部署成功后Activiti 会解析 XML把流程定义存到ACT_RE_PROCDEF表同时生成流程定义的 key、version、resourceName 等元数据。同一个 key 多次部署version 会自动递增运行时默认使用最新版本。5.4 流程定义版本管理策略版本管理是个容易被忽略但很关键的点。Activiti 的默认行为是同一个 key 每次部署都新增一个版本旧版本保留但不再被新流程实例使用。已经在跑的流程实例继续用旧版本新启动的用最新版本。这个策略在大多数场景下没问题但有两种情况要注意。一是流程定义 key 不能随便改改了就等于新流程历史数据对不上。二是如果部署了错误的 XML想回滚只能重新部署一个正确版本不能删除旧版本Activiti 不允许删除有运行实例的流程定义。我的做法是在前端加一个“版本对比”功能保存前先拉取当前最新版本的 XML用 diff 库对比一下让用户确认改动。这个功能不是必须的但在多人协作的环境里能避免很多误操作。6. 常见问题与排查技巧实录6.1 bpmn-js 在 Vue3 中的典型报错问题一Cannot read property get of undefined这个报错通常出现在modeler.get(canvas)时。原因是 modeler 还没初始化完成或者已经被销毁了。解决办法是在调用前加判断if (!modeler) return并且确保onUnmounted里把 modeler 置为 null。问题二画布高度为 0元素显示不出来bpmn-js 的容器必须有明确的高度不能是height: auto。如果父容器是 flex 布局要确保容器有flex: 1或者固定高度。我一般直接给.bpmn-canvas设height: 100%然后父级设height: calc(100vh - 120px)。问题三属性面板不显示检查三件事propertiesPanel.parent的选择器是否对应到真实 DOMBpmnPropertiesPanelModule和BpmnPropertiesProviderModule是否都注册了CSS 是否引入了bpmn-io/properties-panel/assets/properties-panel.css。第三个最容易漏。6.2 Activiti 部署时的 XML 兼容问题问题部署报XMLException: cvc-complex-type.2.4.a这是 XML 校验失败通常是元素顺序不对。BPMN 2.0 对子元素的顺序有严格要求比如process下面必须先写startEvent再写userTask顺序错了就报这个错。bpmn-js 生成的 XML 一般不会错但如果你手动改过 XML或者用了自定义 moddle 扩展就可能出问题。问题activiti:assignee不生效检查 moddleExtensions 里是否正确注册了 activiti descriptor。如果没注册bpmn-js 会把activiti:assignee当作未知属性丢掉导出的 XML 里就没有这个属性Activiti 自然读不到。问题中文乱码部署时 XML 的编码要统一成 UTF-8。前端导出时saveXML默认就是 UTF-8后端addString时也要确保字符串是 UTF-8。如果数据库连接没配characterEncodingutf8存进去的中文会变问号。6.3 前后端联调中的高频坑问题现象可能原因排查方向部署成功但流程定义列表为空资源名不以 .bpmn20.xml 结尾检查 addString 的文件名启动流程报 key 不存在部署的 key 和启动时传的 key 不一致对比 XML 里 process 的 id 和启动参数审批人字段为空moddle 扩展未注册或属性名拼错检查 XML 里是否有 activiti:assignee流程图在监控页显示错位bpmndi 信息丢失确认导出时保留了图形信息版本号不递增每次部署的 key 不同检查 process id 是否稳定这张表里的每一条我都实际遇到过尤其是第一条和第三条新手几乎必踩。建议在联调阶段把后端部署接口的日志级别调到 DEBUGActiviti 会打印解析 XML 的详细过程出问题时能快速定位。6.4 性能与体验优化心得bpmn-js 在流程节点超过 50 个之后拖拽会开始卡顿。优化手段有几个一是关掉不必要的模块比如bpmn-js-token-simulation如果不用就别引入二是把keyboard.bindTo从 document 改成画布容器减少全局事件监听三是导入大流程时先canvas.zoom(fit-viewport)再渲染属性面板避免同时做两件重活。另一个体验点是自动保存。我一开始没做用户画了半小时没保存浏览器崩了就全没了。后来加了个定时器每 30 秒调一次saveXML存到 localStorage下次打开时提示“检测到未保存的草稿是否恢复”。这个功能实现起来不复杂但用户反馈非常好。7. 一些关于工程化的补充整套东西跑通之后我把它抽成了一个独立的 npm 包内部项目直接pnpm add就能用。抽包的过程中有几个设计决策值得说一下。第一设计器组件不直接发 HTTP 请求所有网络操作通过 props 传进来的回调函数处理。这样组件本身不依赖 axios 或 fetch换项目时不用改代码。第二XML 的校验逻辑放在组件内部。导入时先尝试importXML失败就抛出一个带错误详情的异常外部可以选择弹窗提示或者静默处理。第三属性面板的扩展点做成可配置的。默认提供审批人、候选人、到期时间三个字段如果项目需要更多字段通过 props 传一个配置数组进来组件动态生成对应的 entry。这样既保证了开箱即用又留了扩展空间。如果你也在做类似的事情我的建议是先把最小闭环跑通画一个开始事件、一个用户任务、一个结束事件导出 XML部署到 Activiti启动流程实例看能不能正常流转。这个闭环通了之后再去加属性面板、加自定义节点、加版本管理。顺序反了的话很容易在某个中间环节卡住然后怀疑整个技术选型。最后分享一个调试小技巧Activiti 部署后的流程定义 XML 存在ACT_GE_BYTEARRAY表里字段是BYTES_。如果怀疑前端导出的 XML 和后端存的不一致直接查这张表把二进制转成字符串对比一下比看日志快得多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。