简介

Activiti Bpmn Designer 是一个基于 Vue 3 与 bpmn-js、适配 Activiti 流程引擎的 BPMN2.0 流程图绘制工具。

使用 Vue3、Arco Design Vuebpmn-js 的相关库进行开发,采用 pnpm workspace 模式进行代码管理。

可在 pnpm workspace 中详细了解。

源码运行前请先确保安装 node-v16+, pnpm-v9+ 作为开发环境。

一、源码说明

Bpmn Designer 源码位于 ~/packages/bpmn-designer 中,整个项目源码结构如下:

image-20240818160311443

其中 shared 目录为全局公共组件和方法库,提供给其他两个模块使用。

在 pnpm workspace 中,@activiti/shared@activiti 前缀即代表引入依赖是当前工作空间下的内容,@activiti/xxx 与工作空间下子包的 package.json 中的 name 对应)即代表引入了该目录的内容。

packages/bpmn-designer 中,代码结构如下:

image-20240818160643373

.env 相关环境变量配置文件中,支持 4个配置变量:

  • VITE_APP_BASE_ADDRESS :http 请求的 base 路径,开发模式(.env)中具有前置配置,其他模式下暂未配置,可根据实际情况调整
  • VITE_APP_USE_SAVE:是否使用工具栏保存按钮,设置 false 时会隐藏保存按钮,并将 modeler 对象暴露到 window 对象上,作为 bpmnModeler 属性。
  • VITE_APP_ID_EDITABLE:是否开启 id 和 流程 name 的编辑,默认为 false 禁止状态,.env.other 模式下为 true
  • VITE_APP_USE_THEME_TOGGLE: 是否显示工具栏上的 语言切换和主题切换按钮; 正常情况下这两个按钮仅作为演示用, 所以需要设置为 false

以上参数可以根据实际情况,在对应的 .env 文件中进行调整,也可以新建相关文件,但是需要修改 package.json 中的对应脚本命令。

Tips:

目前 designer 内部 DMN 选择依赖 dmn-viewer,在运行或者编译 bpmn-designer 前,请先安装依赖,并执行 pnpm run dmn-viewer:build:compo 打包 dmn-viewer 组件。

二、使用方式

1. iframe 嵌入

使用 iframe 嵌入时,可以有效的隔离编辑器与当前项目代码,避免样式和全局变量等干扰,且后续升级更加方便。

1.1. 依赖安装

这种方式需要获取源码后进行编译,将编译结果作为页面静态资源使用。

首先,需要全局安装 pnpm,然后进入根目录,执行 pnpm install 安装依赖。

1.2. 编译

在根目录的 package.json 中配置了编辑器的打包命令 bpmn:build:lib

执行 pnpm run bpmn:build:lib,Bpmn Designer 的编译结果将会放置在 /packages/bpmn-designer/dist

1.3. 使用

将上文中编译结果 dist 目录中的所有内容复制到当前开发项目的 静态资源目录(不会参与编译的文件目录,也可以自行部署到服务器) 中,需要注意的是,建议创建一个新的目录来存放这些资源文件,避免入口文件冲突。

然后,在项目的流程图设计页面中,插入 iframe 标签,并配置对应地址。

注意:

使用 iframe 嵌入时,内置请求地址与演示环境存在差异,且工具栏保存按钮默认隐藏。

当需要调用内部方法获取 xml 时,可以在 iframe 标签增加 ref 属性绑定标签对象,然后获取 iframe 内部的 Modeler 实例。

<template>
  <iframe src="./flowable-designer/index.html" ref="designerRef" @load="toggleIframeState" />
</template>
<script setup lang="ts">
  const designerRef = ref()
  const iframeLoaded = ref(false)
  
  const toggleIframeState = () => {
      iframeLoaded.value = true
  }
  
  const setXml = async (xml: string) => {
    if (!iframeLoaded.value) {
      return console.warn('iframe 还未加载结束')
    }
    const { warnings } = await designerRef.value.contentWindow?.bpmnModeler.importXML(xml)
    console.warn(warnings)
  }
  
  const getXml = async () => {
    if (!iframeLoaded.value) {
      return console.warn('iframe 还未加载结束')
    }
    const { xml } = await designerRef.value.contentWindow?.bpmnModeler.saveXML({format: true})
  }
</script>

这种方式下,会将 bpmn-js 编辑器实例 modeler 对象绑定到 iframe 的全局对象 window 中(即上述示例中用到的 contentWindow?.bpmnModeler)。

注意:

  1. 需要等待 iframe 节点挂载结束之后才能正常访问 contentWindow.bpmnModeler
  2. 需要设置导入时重置流程id时,需要使用 designerRef.value.contentWindow?.bpmnModeler.importNewXML(xml, true) 方法,直接使用 bpmnModeler.importXML(xml) 将会保留导入的新 xml 流程 id。

如果需要实现其他操作,可以使用该编辑器实例提供的方法来完成。

常用方法和使用方式见:Bpmn.js 中文文档 或者 掘金专栏: bpmn.js 进阶指南

2. 编译为单个组件

2.1. 依赖安装

这种方式需要获取源码后进行编译,将编译结果作为页面静态资源使用。

首先,需要全局安装 pnpm,然后进入根目录,执行 pnpm install 安装依赖。

2.2. 编译

在根目录的 package.json 中配置了编辑器的打包命令 bpmn:build:compo

执行 pnpm run bpmn:build:compo,Designer 的编译结果将会放置在 /packages/bpmn-designer/lib 中。

默认情况下,提供 esmcommonjs 两种打包结果。

image-20240611154556246

注意:

这种方式下,部分插件会在打包时剔除,需要在使用项目中进行安装。

2.3. 引入并使用

这里主要说明两个注意事项:

  1. 部分组件没有打包到 Designer 组件中,需要在 main.ts 中重新引入:
import { createApp } from 'vue'
import App from './App.vue'

import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
// @ts-ignore: worker
self.MonacoEnvironment = {
  getWorker(_: string, label: string) {
    if (['javascript'].includes(label)) {
      return new tsWorker()
    }
    return new EditorWorker()
  }
}

// 引入组件库
import ArcoVue from '@arco-design/web-vue'
// 引入组件库默认图标库
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
// 代码高亮组件
import hljs from 'highlight.js/lib/core'
import hljsVuePlugin from '@highlightjs/vue-plugin'
// 重新声明国际化内容
import i18n from '@/i18n'
// 自定义组件库
import FL from '@activiti/shared/components'

// 引入组件库样式
import '@arco-design/web-vue/dist/arco.css'

// 编译后设计器的样式部分
import '@activiti/bpmn-designer/lib/style.css'

// 注册代码高亮插件
import xml from 'highlight.js/lib/languages/xml'
hljs.registerLanguage('xml', xml)

const app = createApp(App)

app.use(i18n)
app.use(ArcoVue)
app.use(ArcoVueIcon)
app.use(hljsVuePlugin)
app.use(FL)

app.mount('#app')
  1. 组件使用时,接收两个参数 use-savereset-id-with-import,分别控制 保存按钮显示状态外部文件导入时是否替换Process节点ID属性

2.4 注意事项

当需要将其编译为组件使用时,需要 十分注意 以下内容:

  1. 默认该方式使用 vite.lib.config.ts 文件,该模式 默认排除 vue,arco-design,bpmn-js,diagram-js 等依赖。所以在 使用时需要单独安装对应的依赖项,或者 修改该文件,将其添加到默认打包依赖中一起编译;但是,vue 和 vue-i18n 是 必须排除 的选项。
  2. 如果 没有使用 pnpm worspace 模式,在使用时,需要 将生成的 lib 中对应的 bpmn-designer.js(esm 模式)或者 bpmn-designer.cjs(commonjs 模式),以及 style.css 复制到项目中,然后使用 import {BpmnFullDesigner} from '@/xx/xx/bpmn-designer.js' 的方式引入该组件。
  3. 编译时默认会生成 .d.ts 文件

如果使用这种方式,个人更加推荐 pnpm workspace 开发模式,以减少代码和配置改动。

如果使用的不是 @arco-design/web-vue 作为 UI 组件库的话,建议将 vite.lib.config.tsbuild.rollupOptions.external 内的 @arco-design/web-vue 移除。

3. 源码嵌入

编码层面的使用,对于后续迭代来说比较复杂,但是适合需要进行定制化开发的项目。

3.1. 组件复制

下载本项目,将 packages 中对应工具的 src 目录(除 App.vue 外其他内容)以及 shared/* 复制到项目中。

image-20240818161330038

其中 bpmn-designer 中包含 BPMN XML 的图形编辑器,bpmn-viewer 中包含 BPMN XML 的查看器和模拟器。可以根据业务进行相应的选择并复制对应内容,也可以将两者统一合并到一个项目中,但是此时需要注意 styles 中的重复样式处理。

3.2. 依赖安装与配置

目前提供三个模式的 BPMN 编辑/预览模式:

  1. Designer
  2. Mocker
  3. Viewer

不同模式下需要安装的依赖各有差异。

Designer

需要安装 @arco-design/web-vue, @arco-plugins/vite-vue, @highlightjs/vue-plugin, highlight.js, lucide-vue-next, min-dash, monaco-editor, quill, quill-delta, radash, tiny-svg, bpmn-js, diagram-js, bpmn-js-color-picker, bpmn-js-i18n-zh, bpmn-js-token-simulation, diagram-js-grid-bg, diagram-js-minimap, ids, vue-i18n, bpmn-moddle, vite-plugin-svg-icons

npm install @arco-design/web-vue @arco-plugins/vite-vue @highlightjs/vue-plugin highlight.js lucide-vue-next min-dash monaco-editor quill quill-delta radash tiny-svg bpmn-js diagram-js bpmn-js-color-picker bpmn-js-i18n-zh bpmn-js-token-simulation diagram-js-grid-bg diagram-js-minimap ids vue-i18n bpmn-moddle vite-plugin-svg-icons

然后,需要配置 Vite 插件:

import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import * as path from 'path'
import { vitePluginForArco } from '@arco-plugins/vite-vue'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src/'),
      'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
    }
  },
  // ...
  plugins: [
  	// ...
    AutoImport({
      dts: './src/auto-imports.d.ts',
      imports: ['vue', 'vue-router', 'pinia']
    }),
    vitePluginForArco({
      style: 'css'
    }),
    Components({
      dts: './src/components.d.ts'
    }),
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), 'src/assets/bpmnIcons')],
      symbolId: '[name]',
      inject: 'body-last',
      customDomId: '__svg__icons__dom__'
    })
  ],
  // ...
})

注意:如果没有使用 unplugin-auto-importunplugin-vue-components,则需要在组件中手动引入 vue 相关 API 与依赖组件文件。

例如:packages/bpmn-designer/src/components/Panel/index.vue

// 原有的
import type { Component } from 'vue'

需要修改为:

import { inject, ref, shallowRef, watchEffect, provide, nextTick, type Component } from 'vue'

在 vite 配置修改之后,需要在 main.ts 中引用全局内容:

import { createApp } from 'vue'

import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'

// 为 monaco edtor 添加 service worker 支持
// @ts-ignore: worker
self.MonacoEnvironment = {
  getWorker(_: string, label: string) {
    if (['javascript'].includes(label)) {
      return new tsWorker()
    }
    return new EditorWorker()
  }
}

import App from './App.vue'

// arco design 组件库样式
import '@arco-design/web-vue/dist/arco.css'
// 代码高亮样式
import 'highlight.js/styles/stackoverflow-light.css'
// bpmn 设计器相关样式(如果修改了目录,这里需要做对应修改)
import '@/assets/styles/index.scss'

// bpmn 设计器国际化相关配置,如果本身项目有国际化配置,需要进行相应合并
import i18n from '@/i18n'

// 代码高亮组件及配置
import hljs from 'highlight.js/lib/core'
import xml from 'highlight.js/lib/languages/xml'
import hljsVuePlugin from '@highlightjs/vue-plugin'

// shared 中的公共组件(目前只有 monaco editor),非 pnpm workspace 模式,需要修改 @activiti/shared 为相应路径
import FL from '@activiti/shared/components'

hljs.registerLanguage('xml', xml)

//svg 引用
import 'virtual:svg-icons-register'

const app = createApp(App)

app.use(i18n)
app.use(hljsVuePlugin)

app.use(FL)

app.mount('#app')

最后,参照 bpmn-designer/src/App.vue 使用设计器相关组件即可。

<script setup lang="ts">
  import Modeler from 'bpmn-js/lib/Modeler'
  import BpmnDesigner from '@/components/Designer/index.vue'
  import BpmnPanel from '@/components/Panel/index.vue'
  import { modelerKey } from '@/injection-keys'

  defineOptions({ name: 'App' })

  const provideModeler = shallowRef<Modeler | undefined>()

  const setCurrentModeler = (modeler: Modeler) => {
    provideModeler.value = modeler
  }

  provide(modelerKey, provideModeler)
</script>

<template>
  <BpmnToolbar />
  <BpmnDesigner @modeler-init="setCurrentModeler" />
  <BpmnPanel />
</template>

需要注意的是,BpmnPanel 组件与 BpmnToolbar 组件在使用时需要依赖 BpmnDesigner 组件提供的 modeler 实例(来自于 bpmn-jsbpmn-js/lib/Modeler 构造函数),然后将该实例通过 provide(modelerKey, provideModeler) 注入到组件树中。

如果需要动态更新 BpmnDesigner 中的 xml,可以通过 BpmnDesigner 组件实例 createNewProcess 方法来实现:

<script setup lang="ts">
  import BpmnDesigner from '@/components/Designer/index.vue'
  
  const designerIns = shallowRef()
  
  // 在某个时候调用这个方法,实现 xml 更新。newXmlString 为外部获取的 xml 字符串,可以为空
  const updateXml = () => {
    designerIns.value.createNewProcess(newXmlString)
  }
</script>

<template>
	<!-- 通过 ref 获取该组件实例 -->
  <BpmnDesigner ref="designerIns" />
</template>

3.3. 组件使用

Designer 整体分为三个部分:BpmnToolbarBpmnDesignerBpmnPanel

image-20240519155707616

其中,BpmnDesigner 为必要组件,BpmnToolbar 提供快捷功能支持,BpmnPanel 则用来支撑流程元素的属性编辑与查看;BpmnPanelBpmnToolbar 依赖 BpmnDesigner 组件。

并且,在使用时如上文所说,必须使用 provide 向组件树中注入 bpmn-js 的 Modeler 实例,否则两者功能将无法正常使用。

BpmnDesigner 组件接收一个 xml 参数,可以用来设置画布的初始 xml。

三、不同使用方式的差别

使用方式iframe嵌入编译为单个组件源码嵌入
优势1. 完全解耦
2. 迭代快速
1. 基本上解耦
2. 迭代快速
3. 支持二开
1. 任意二开
劣势1. 无法二开
2. 弹窗遮罩层会稍显突兀
3. 编辑器实例API调用比较复杂
1. 二开后合并官方迭代较为复杂
2. 可能产生样式冲突
1. 合并官方迭代较为复杂
2. 容易产生样式冲突
3. 对bpmn-js和TS不熟悉的话难以改造
建议使用官方的流程后台、并且需要实时更新官方迭代;原有系统比较复杂不会进行大量改动,但是需要统一样式与弹窗效果;需要实时更新官方迭代不需要实时更新官方迭代,对二开需求较大

四、组件 API

这种方式只提供给作 组件化 使用,

1. IntegralDesigner

1.1 props 参数

nametypedescdefault
xmlstring默认初始 xml-
theme'dark' | 'light'初始颜色主题'light'
local'zh_CN' | 'en_US'初始国际化于语言'zh_CN'
useSaveboolean是否显示工具栏保存按钮false
resetIdWithImportboolean是否在通过工具栏打开 xml 时更新导入文件的 id 为当前流程 idtrue

1.2 events 事件

nameparamsdesc
xml-changedxml: string当 xml 更新时触发

1.3 methods 方法

nameparamsdescreturns
createNewProcessxml: string新的 xml 字符串-
toggleLanglang?: 'zh_CN' | 'en_US'目标语言, 为空时相互切换-
toggleThemetheme?: 'dark' | 'light'切换颜色主题-

2. BpmnDesigner

2.1 props 参数

nametypedescdefault
xmlstring默认初始 xml-

2.2 events 事件

nameparamsdesc
modeler-initmodeler: Modeler当 bpmn-js 的编辑器实例初始化时触发, 参数是当前编辑器实例
modeler-destroymodeler: Modeler当 bpmn-js 的编辑器实例销毁前触发, 参数是当前编辑器实例
root-initbase: { name: string; id: string }[]当新的 root 元素解析时, 解析到的根节点的 id 和 name 组成的数组

2.3 methods 方法

nameparamsdescreturns
createNewProcessxml: string新的 xml 字符串-
initModeler-重新初始化一次编辑器实例-
destroyModeler-销毁当前编辑器实例-

2.4 slots 插槽

namedescparams
default默认插入到画布区域内部的内容-

3. BpmnToolbar

3.1 props 参数

nametypedescdefault
useSaveboolean是否显示工具栏保存按钮false
showLabelboolean是否显示第一组按钮的文字部分, 为 false 时将通过 popover 显示false
resetIdWithImportboolean是否在通过工具栏打开 xml 时更新导入文件的 id 为当前流程 idtrue

3.2 slots 插槽

namedescparams
default工具栏默认工具后的插槽-

4. BpmnPanel

4.1 props 参数

nametypedescdefault
local'zh_CN' | 'en_US'初始国际化于语言