ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Bpmn.js与Vue 2.x构建可定制化流程设计器的实践指南

基于Bpmn.js与Vue 2.x构建可定制化流程设计器的实践指南 简介这是一套面向中高级前端开发者与工作流平台建设者的BPMN流程编辑器前端实现方案基于bpmn.js、Vue 2.x与ElementUI深度集成解决企业级流程建模中监听器配置、扩展属性绑定、动态表单嵌入等核心定制需求。资源包共257个文件涵盖80个JavaScript逻辑模块含流程渲染、事件监听、引擎适配、28个Vue组件如节点配置面板、表单设计器、引擎切换器、17个TypeScript类型定义及89个SVG图标资源辅以SCSS样式、JSON配置示例与PNG/JPG说明素材整体仅2.46MB轻量易集成。已有1846人学习下载项目已内置Activiti、Flowable、Camunda三大主流引擎的BPMN文件兼容支持并提供完整可运行的演示代码、自定义节点扩展方法、表单字段映射逻辑及监听器注册机制目录结构按功能分层清晰便于二次开发与模块化复用。1. 项目概述与核心价值最近在做一个内部流程审批系统的重构前端技术栈是经典的 Vue 2.x ElementUI。老系统用的是 Activiti 官方那个比较“古朴”的 Modeler用户体验和定制能力都跟不上业务需求了。业务方提了一堆需求流程节点要能配置复杂的表单、要能挂接各种监听器执行自定义逻辑、节点上还得能扩展一堆业务属性。这摆明了需要一个高度可定制、且能无缝嵌入我们现有技术栈的前端流程设计器。找了一圈开源方案要么太重集成了一整套后端要么太轻画个图就完事了要么技术栈不匹配。最终我们把目光锁定在了 Bpmn.js 上。它是一个基于原生 BPMN 2.0 规范的流程建模工具库只提供最核心的绘图与基础模型能力就像给了你一套乐高积木的基本颗粒至于搭成城堡还是飞船完全由开发者自己决定。这个“白盒”特性正好契合我们“深度定制”的需求。于是就有了这个基于 Bpmn.js、Vue 2.x 和 ElementUI 的流程编辑器前端项目。它不是一个全功能的流程平台而是一个可嵌入、可配置、可扩展的流程设计器组件。核心目标是让开发者能在熟悉的 VueElementUI 环境下快速搭建出一个满足业务需求的流程设计界面重点实现监听器、扩展属性和表单配置这三块业务最关心的功能并且确保整个架构是开放、易于二次开发的。2. 技术选型与整体架构设计2.1 为什么是 Bpmn.js Vue 2.x ElementUI这个组合不是凭空想象的是经过技术和业务双重权衡的结果。首先Bpmn.js是基石。它由 bpmn-io 团队维护是业界事实上的标准 BPMN 2.0 前端建模库。选择它有几个硬核理由第一它严格遵循 BPMN 2.0 规范导出的 XML 可以被 Activiti、Flowable、Camunda 等主流工作流引擎直接解析和执行保证了前后端协议的统一性。第二它采用分层架构将绘图diagram-js、属性面板properties-panel、模型bpmn-moddle等模块解耦。这意味着我们可以替换或深度定制其中任何一部分比如用 Vue 完全重写属性面板。第三社区活跃遇到坑的时候有地方可以找答案。其次Vue 2.x是主体框架。项目立项时 Vue 3 尚未完全普及且团队对 Vue 2 的熟悉度极高生态成熟。Vue 的响应式系统和组件化开发模式非常适合用来构建复杂的、状态驱动的交互界面。我们将流程画布、属性配置面板、工具栏、侧边栏等都封装成 Vue 组件通过 Vuex 或 Provide/Inject 来管理全局的流程模型状态逻辑清晰维护方便。最后ElementUI是 UI 骨架。作为国内最流行的 Vue UI 库之一ElementUI 提供了丰富、美观且一致的桌面端组件。我们的属性配置面板、表单设计器、各种弹窗和提示几乎全部基于 ElementUI 构建极大地提升了开发效率并保证了与系统其他部分一致的视觉体验。整个架构的核心思想是用 Bpmn.js 负责画布渲染和 BPMN 模型管理用 Vue 组件封装交互和业务配置用 ElementUI 呈现用户界面。三者通过精心设计的接口进行通信Bpmn.js 的模型变更触发 Vue 的响应式更新Vue 中的用户操作通过 Bpmn.js 的 API 修改模型并更新画布。2.2 项目核心模块拆解为了达成高内聚、低耦合的目标我们将编辑器前端拆分为以下几个核心模块BpmnModeler 实例管理模块这是最底层的一环。负责初始化 Bpmn.js 的BpmnModeler实例挂载到 DOM并封装一系列对其的增删改查操作。这个模块会暴露出一个简洁的 API 供上层调用例如addEventListener(element, listenerConfig)、updateElementExtensions(element, properties)。流程画布容器组件 (Canvas.vue)一个纯粹的容器组件内部创建并管理 BpmnModeler 实例。它主要处理画布的生命周期创建、销毁、缩放、自适应等视图层逻辑并将 BpmnModeler 实例通过 Vue 的 Provide/Inject 机制共享给子孙组件。属性配置面板组件 (PropertiesPanel.vue)这是业务逻辑最重的部分。它监听画布中元素如用户任务、网关的选中事件。当选中某个元素时面板动态加载并渲染该元素对应的属性配置表单。这个表单完全由 Vue ElementUI 构建分为多个标签页如“基本属性”、“监听器”、“扩展字段”、“表单配置”等。自定义 Palette (工具栏) 组件 (CustomPalette.vue)Bpmn.js 默认的左侧工具栏图标是固定的。我们可能需要根据业务隐藏一些不用的元素如复杂网关或者添加一些自定义的、非 BPMN 标准的“业务节点”比如一个“调用外部 API”的节点。这个组件通过继承和覆盖 Bpmn.js 的PaletteProvider来实现自定义工具栏。自定义 Renderer (渲染器) 组件 (可选)如果你需要改变默认元素的视觉样式比如给特定类型的任务节点加上一个独特的图标背景就需要通过自定义Renderer来实现。状态管理 (Vuex Store)集中管理流程模型的数据。包括当前流程的 XML 字符串、选中的元素信息、所有元素的扩展属性集合、监听器列表等。这样画布组件和属性面板组件可以共享和同步状态避免复杂的组件间通信。公共工具与扩展点模块提供一系列工具函数如 XML 解析与格式化、监听器配置的验证、扩展属性的序列化与反序列化等。同时这里定义了如何“扩展”编辑器例如注册一个新的属性配置标签页的接口。注意在模块设计时一个关键决策是属性面板的实现路径。Bpmn.js 官方提供了一个properties-panel模块和一套基于原生 DOM 的渲染体系。但我们放弃了它因为它的定制成本依然很高且无法与 Vue/ElementUI 深度集成。我们选择完全自己用 Vue 重写属性面板虽然前期工作量稍大但获得了绝对的灵活性和与现有技术栈的完美融合长期来看更划算。3. 核心功能实现细节与实操3.1 BpmnModeler 的初始化与集成初始化 BpmnModeler 是第一步这里有很多细节需要注意。// 在 Canvas.vue 的 mounted 生命周期中 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; export default { mounted() { this.initBpmnModeler(); }, methods: { async initBpmnModeler() { // 1. 创建实例传入容器ID和配置 this.modeler new BpmnModeler({ container: #bpmn-canvas, // 关键禁用默认的属性面板 propertiesPanel: { parent: null }, // 引入必要的附加模块如标签编辑、对齐线等 additionalModules: [ // 自定义的模块在这里注入例如自定义的 PaletteProvider CustomPaletteModule, CustomContextPadModule ], // 配置扩展用于支持我们自定义的扩展属性命名空间 moddleExtensions: { camunda: camundaModdleDescriptor, // 如果要用Camunda扩展属性 myapp: myAppModdleDescriptor // 我们自定义的扩展命名空间 } }); // 2. 将实例通过 Provide 共享出去 this.provide(bpmnModeler, this.modeler); // 3. 监听重要事件 const eventBus this.modeler.get(eventBus); // 元素被选中事件用于驱动属性面板更新 eventBus.on(element.click, (event) { const element event.element; if (element.type ! bpmn:Process) { // 通常不处理整个流程根节点 this.$store.commit(setSelectedElement, element); } }); // 4. 加载一个初始的空流程或后端传来的流程XML try { const xmlStr this.defaultXml || this.getEmptyDiagram(); const { warnings } await this.modeler.importXML(xmlStr); if (warnings.length) { console.warn(BPMN import warnings:, warnings); } // 初始化后可以调整画布视图 this.modeler.get(canvas).zoom(fit-viewport); } catch (err) { console.error(Failed to import BPMN diagram, err); this.$message.error(流程图加载失败); } }, getEmptyDiagram() { // 返回一个最简单的、只包含一个开始事件和一个空流程的BPMN 2.0 XML字符串 return ?xml version1.0 encodingUTF-8? bpmn2:definitions ... bpmn2:process idProcess_1 isExecutabletrue bpmn2:startEvent idStartEvent_1 / /bpmn2:process bpmn2:BPMNDiagram idBPMNDiagram_1 !-- 省略图形信息 -- /bpmn2:BPMNDiagram /bpmn2:definitions; } } }实操要点CSS 引入务必引入 Bpmn.js 自带的两个 CSS 文件否则画布样式会错乱。禁用默认面板通过propertiesPanel: { parent: null }明确关闭官方面板避免冲突。事件总线eventBus是 Bpmn.js 内部通信的核心。除了element.click常用的还有element.changed元素属性变更、shape.added添加形状、connection.added添加连线等用于实现更复杂的交互逻辑。错误处理importXML是异步操作必须用 try-catch 包裹。导入警告warnings通常是一些非致命性问题但最好记录一下。3.2 监听器 (Listeners) 的可视化配置监听器是流程引擎在特定时间点如任务创建、完成时回调外部代码的机制。在前端我们需要提供一个友好的界面来配置它们。数据结构设计一个监听器配置通常包含类型如 Java Class、Expression、Delegate Expression、事件如 start、end、具体值类全名、表达式等以及注入字段Fields。我们在 Vuex 中这样设计状态// store/modules/process.js state: { selectedElement: null, listeners: { // 以元素ID为key存储该元素上所有的监听器列表 UserTask_1: [ { id: listener_1, event: start, type: class, // class, expression, delegateExpression value: com.example.MyTaskListener, fields: [ { name: field1, type: string, value: 固定值 }, { name: field2, type: expression, value: ${someVariable} } ] } ] } }属性面板实现 (PropertiesPanel.vue 片段):template div v-ifselectedElement classproperties-panel el-tabs v-modelactiveTab !-- 其他标签页... -- el-tab-pane label监听器 namelisteners div classlistener-section el-button clickaddListener sizesmall typeprimary添加监听器/el-button el-table :datacurrentListeners sizemini border el-table-column propevent label事件 width100 template slot-scopescope el-select v-modelscope.row.event sizemini el-option label开始 valuestart/el-option el-option label结束 valueend/el-option el-option label创建 valuecreate/el-option !-- 其他事件 -- /el-select /template /el-table-column el-table-column proptype label类型 width120 template slot-scopescope el-select v-modelscope.row.type changeonListenerTypeChange(scope.row) sizemini el-option labelJava类 valueclass/el-option el-option label表达式 valueexpression/el-option el-option label代理表达式 valuedelegateExpression/el-option /el-select /template /el-table-column el-table-column propvalue label值 template slot-scopescope el-input v-ifscope.row.type class v-modelscope.row.value placeholder请输入完整类名如 com.xx.Listener/el-input el-input v-else v-modelscope.row.value placeholder请输入表达式如 ${service.doSomething()}/el-input /template /el-table-column el-table-column label操作 width80 template slot-scopescope el-button clickeditListenerFields(scope.row) typetext字段/el-button el-button clickremoveListener(scope.$index) typetext stylecolor:#F56C6C删除/el-button /template /el-table-column /el-table !-- 字段编辑对话框 -- el-dialog title监听器字段配置 :visible.syncfieldDialogVisible !-- 字段配置表单使用动态表单渲染 -- fields-editor :fieldseditingListener.fields changeonFieldsChange/fields-editor /el-dialog /div /el-tab-pane /el-tabs /div /template script import { mapState, mapGetters } from vuex; export default { computed: { ...mapState(process, [selectedElement, listeners]), currentListeners() { return this.listeners[this.selectedElement?.id] || []; } }, methods: { addListener() { const newListener { id: listener_${Date.now()}, event: start, type: class, value: , fields: [] }; this.$store.commit(process/addListener, { elementId: this.selectedElement.id, listener: newListener }); }, // 当监听器类型改变时清空旧值提供更合适的placeholder提示 onListenerTypeChange(row) { row.value ; }, async editListenerFields(listener) { this.editingListener listener; this.fieldDialogVisible true; }, // 将配置同步到BPMN模型 saveListenersToModel() { const modeling this.modeler.get(modeling); const elementRegistry this.modeler.get(elementRegistry); const element elementRegistry.get(this.selectedElement.id); const bpmnListeners this.currentListeners.map(l ({ event: l.event, // 根据type将配置转换为BPMN标准的扩展元素 ...this.buildBpmnListenerObject(l) })); // 使用 modeling.updateProperties 更新元素的扩展属性 modeling.updateProperties(element, { // 这里假设使用camunda扩展命名空间 camunda:taskListener: bpmnListeners }); } } } /script关键点与避坑数据同步属性面板的修改Vue 数据需要手动同步到 Bpmn.js 内部模型。我们通常在用户点击“保存”按钮或切换选中元素时触发saveListenersToModel这样的方法。BPMN 扩展标准的 BPMN 2.0 XML 本身没有taskListener定义。这是 Camunda/Activiti 等引擎通过“扩展元素(ExtensionElements)”引入的。因此在调用modeling.updateProperties时需要使用正确的命名空间如camunda:。字段编辑监听器的字段配置可能很复杂最好抽离成一个独立的动态表单组件 (FieldsEditor)支持添加、删除、修改字段名、类型和值。3.3 扩展属性 (Extension Properties) 的动态表单管理除了监听器业务还需要在流程元素上挂载各种自定义属性比如“审批人角色”、“超时时间”、“业务分类”等。这些属性没有固定的 schema需要动态管理。实现思路在 Vuex 中为每个元素维护一个扩展属性 Map。在属性面板提供一个“扩展属性”标签页允许用户以键值对的形式添加、删除、修改。同样在保存时将这些属性序列化到 BPMN 模型的extensionElements中。属性面板扩展属性部分el-tab-pane label扩展属性 nameextensions div classextensions-section el-button clickaddExtensionProperty sizesmall typeprimary添加属性/el-button el-table :datacurrentExtensions sizemini border el-table-column propname label属性名 template slot-scopescope el-input v-modelscope.row.name placeholder如approvalRole/el-input /template /el-table-column el-table-column propvalue label属性值 template slot-scopescope el-input v-modelscope.row.value placeholder如部门经理/el-input /template /el-table-column el-table-column proptype label值类型 width100 template slot-scopescope el-select v-modelscope.row.type sizemini el-option label字符串 valuestring/el-option el-option label整数 valueint/el-option el-option label布尔值 valueboolean/el-option el-option label表达式 valueexpression/el-option /el-select /template /el-table-column el-table-column label操作 width60 template slot-scopescope el-button clickremoveExtensionProperty(scope.$index) typetext stylecolor:#F56C6C删除/el-button /template /el-table-column /el-table /div /el-tab-pane序列化到 BPMN XML这是关键一步需要将我们前端管理的 JSON 格式的属性转换成符合 BPMN 2.0 规范的 XML 结构。// utils/xmlExtensions.js export function serializeExtensionsToXml(extensions) { // extensions 是一个数组: [{name: approvalRole, value: manager, type: string}, ...] if (!extensions || extensions.length 0) { return null; } // 构建 extensionElements 结构 const extensionElements { camunda:properties: { camunda:property: extensions.map(ext ({ _name: ext.name, _value: ext.value // 如果需要类型可以放在另一个属性里或者用命名空间区分 // _type: ext.type })) } }; return extensionElements; } // 在保存时调用 const extensionElements serializeExtensionsToXml(this.currentExtensions); if (extensionElements) { modeling.updateProperties(element, { extensionElements: extensionElements }); } else { // 如果扩展属性为空可能需要清除已有的 extensionElements modeling.updateProperties(element, { extensionElements: undefined }); }踩坑记录直接操作extensionElements这个属性有时会遇到问题因为 Bpmn.js 内部对复杂结构的更新可能不彻底。一个更稳健的做法是使用modeling.updateModdleProperties(element, extensionElements)方法它是专门为更新 Moddle模型元素设计的。3.4 表单 (Form) 配置的集成这里的“表单”指的是用户任务User Task关联的办理表单。一种常见做法是配置一个表单的“键”Form Key这个键可以是一个外部表单的ID、一个URL或者一个表达式。前端配置器只需要提供一个输入框让用户填写这个键即可。对于更复杂的、需要在前端直接设计表单布局和字段的场景我们可以集成一个独立的表单设计器组件。这个组件可以输出一个表单的 JSON Schema。然后我们将这个 JSON Schema 的 ID 或内容作为一个特殊的“扩展属性”存储在该用户任务节点上。简化实现el-tab-pane label任务表单 nameform div v-ifisUserTask el-form sizemini label-width80px el-form-item label表单类型 el-radio-group v-modelformConfig.type el-radio labelkey外部表单Key/el-radio el-radio labelembedded嵌入式表单/el-radio /el-radio-group /el-form-item el-form-item v-ifformConfig.type key labelForm Key el-input v-modelformConfig.key placeholder如leaveApplicationForm/el-input /el-form-item el-form-item v-ifformConfig.type embedded label表单设计 el-button clickopenFormDesigner打开表单设计器/el-button div v-ifformConfig.schema span已设计表单字段数{{ formConfig.schema.fields.length }}/span /div /el-form-item /el-form /div div v-else el-alert title当前选中元素不是用户任务无法配置表单。 typeinfo :closablefalse/el-alert /div /el-tab-pane数据关联无论是简单的 Form Key 还是复杂的表单 Schema最终都通过updateProperties方法以扩展属性的形式绑定到 BPMN 元素上。后端引擎在解析流程定义时再根据这个属性去渲染对应的表单。4. 自定义扩展与高级功能实现4.1 自定义 Palette工具栏与 ContextPad右键菜单业务方可能觉得标准的 BPMN 元素不够用比如想增加一个“发送短信”的节点。这就需要自定义 Palette。步骤创建一个自定义的 Palette Provider。在初始化 BpmnModeler 时通过additionalModules注入。// custom/CustomPaletteProvider.js export default function CustomPaletteProvider(palette, create, elementFactory, globalConnect) { this._create create; this._elementFactory elementFactory; this._globalConnect globalConnect; palette.registerProvider(this); } CustomPaletteProvider.$inject [palette, create, elementFactory, globalConnect]; CustomPaletteProvider.prototype.getPaletteEntries function() { const create this._create; const elementFactory this._elementFactory; const globalConnect this._globalConnect; return { // 重写默认的‘create’分组可以隐藏或调整顺序 create.start-event: { group: create, className: bpmn-icon-start-event-none, title: 创建开始事件, action: { dragstart: function(event) { // 处理拖拽开始 }, click: function(event) { // 处理点击创建 const shape elementFactory.createShape({ type: bpmn:StartEvent }); create.start(event, shape); } } }, // 添加一个自定义的分组和元素 create.custom-task: { group: custom, // 自定义分组名 className: custom-icon-sms-task, // 自定义CSS类用于显示图标 title: 短信任务, action: { click: function(event) { const shape elementFactory.createShape({ type: bpmn:ServiceTask, // 复用ServiceTask类型 businessObject: { // 在这里可以定义自定义的业务对象属性 // 这些属性会保存在BPMN XML的扩展中 myapp:taskType: smsTask } }); create.start(event, shape); } } } }; };关键点图标需要自己准备 CSS 或 SVG 图标并通过className指定。业务对象在创建形状时通过businessObject可以附加自定义属性。为了能让这些属性被正确序列化到 XML你需要在初始化BpmnModeler时通过moddleExtensions注册自定义的命名空间和 Schema (myAppModdleDescriptor)。4.2 自定义渲染与样式如果你想让你添加的“短信任务”在画布上看起来与众不同就需要自定义渲染器。// custom/CustomRenderer.js import BaseRenderer from diagram-js/lib/draw/BaseRenderer; export default class CustomRenderer extends BaseRenderer { constructor(eventBus, styles) { super(eventBus, styles); // 指定渲染优先级数字越大优先级越高 this.PRIORITY 1500; } canRender(element) { // 只渲染我们自定义类型的元素 return element.businessObject element.businessObject.get(myapp:taskType) smsTask; } drawShape(parentNode, element) { // 使用SVG绘制一个自定义图形 const shape this._drawCustomSmsTask(parentNode, element); return shape; } _drawCustomSmsTask(parentNode, element) { const { x, y, width, height } element; const customGroup this._create.g(g, { transform: translate(${x}, ${y}) }); // 画一个圆角矩形作为背景 const rect this._create.rect({ width, height, rx: 5, ry: 5, stroke: #1890ff, fill: #e6f7ff, strokeWidth: 2 }); // 添加一个短信图标可以用SVG Path或图片 const iconPath this._create.path({ d: M10 5..., // 简化的SVG路径 fill: #1890ff }); this._create.append(parentNode, customGroup); this._create.append(customGroup, rect); this._create.append(customGroup, iconPath); // 返回这个组Bpmn.js会用它作为该元素的视觉表现 return customGroup; } } CustomRenderer.$inject [eventBus, styles];同样这个自定义渲染器也需要通过additionalModules注入到 BpmnModeler 中。5. 常见问题、性能优化与部署实践5.1 开发与调试中的常见问题画布不显示或样式错乱原因忘记引入 Bpmn.js 的 CSS 文件或者引入顺序有误。解决确保在初始化 BpmnModeler 的组件中正确引入diagram-js.css和bpmn.css。属性面板修改后画布没有实时更新原因修改了 Vue 中的数据但没有调用 Bpmn.js 的 API 去更新底层模型。解决任何对流程元素的修改属性、监听器、扩展字段都必须最终通过modeling.updateProperties或modeling.updateModdleProperties来同步。可以设计一个防抖或节流的保存机制避免频繁操作。自定义元素或属性在导出 XML 中丢失原因没有正确注册自定义的 Moddle 扩展描述符 (moddleDescriptor)。解决创建一个 JSON 文件定义你的命名空间和属性结构并在初始化时传入moddleExtensions。// myapp.json { name: MyApp, uri: http://myapp.com/schema/1.0, prefix: myapp, xml: { tagAlias: lowerCase }, types: [ { name: SmsTask, extends: [bpmn:ServiceTask], properties: [ { name: phoneNumber, type: String, isAttr: true }, { name: templateId, type: String, isAttr: true } ] } ] }然后在初始化时引入moddleExtensions: { myapp: myappDescriptor }。流程复杂时操作卡顿原因Bpmn.js 在渲染大量元素或监听过多事件时可能有性能压力。优化对element.changed这类高频事件进行节流监听。在不需要的时候及时销毁监听器。考虑对非常复杂的流程进行分步加载或懒加载部分图形。5.2 与后端交互与数据持久化编辑器最终需要将设计好的流程保存到后端。获取流程 XMLasync saveDiagram() { try { const { xml } await this.modeler.saveXML({ format: true }); // format: true 会美化输出 return xml; } catch (err) { console.error(Failed to save XML, err); throw err; } }这个xml字符串就是完整的 BPMN 2.0 XML包含了我们通过扩展属性添加的所有自定义配置。向后端发送数据通常我们会将 XML 和一份更结构化的 JSON 摘要包含流程名称、版本、关键节点信息等一起提交给后端。从后端加载流程后端返回流程 XML 字符串直接调用modeler.importXML(xmlStr)即可渲染。5.3 打包与部署注意事项按需引入Bpmn.js 及其相关模块体积不小。如果只用到核心的建模功能可以考虑按需引入模块而不是引入完整的bpmn-js。CSS 隔离Bpmn.js 的 CSS 可能会影响全局样式。建议将画布容器放在一个具有特定类名的 div 内并使用 CSS 模块化或scoped样式来避免冲突。组件化封装将整个编辑器封装成一个独立的 Vue 组件 (BpmnEditor.vue)通过 Props 接收初始 XML 和配置通过 Events 发出保存、错误等信号。这样可以在项目中像使用普通组件一样使用它。这个基于 Bpmn.js 的流程编辑器其核心价值在于平衡了标准性与灵活性。我们既享受了 BPMN 2.0 标准带来的兼容性红利又能通过 Vue 和 ElementUI 快速构建出贴合业务的交互界面。整个开发过程就像在搭积木Bpmn.js 提供了坚实的地基和基础颗粒而 Vue 生态则让我们能自由地建造出任何想要的建筑外观和内部装修。本文还有配套的精品资源点击获取
返回列表