简介
Activiti Bpmn Designer 是一个基于 Vue 3 与 bpmn-js、适配 Activiti 流程引擎的 BPMN2.0 流程图绘制工具。
使用 Vue3、Arco Design Vue、bpmn-js 的相关库进行开发,采用 pnpm workspace 模式进行代码管理。
可在 pnpm workspace 中详细了解。
源码运行前请先确保安装 node-v16+, pnpm-v9+ 作为开发环境。
一、源码说明
Bpmn Designer 源码位于 ~/packages/bpmn-designer
中,整个项目源码结构如下:
其中 shared
目录为全局公共组件和方法库,提供给其他两个模块使用。
在 pnpm workspace 中,@activiti/shared
(@activiti
前缀即代表引入依赖是当前工作空间下的内容,@activiti/xxx
与工作空间下子包的 package.json
中的 name
对应)即代表引入了该目录的内容。
在 packages/bpmn-designer
中,代码结构如下:
在 .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
)。
注意:
- 需要等待 iframe 节点挂载结束之后才能正常访问
contentWindow.bpmnModeler
。- 需要设置导入时重置流程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
中。
默认情况下,提供 esm
与 commonjs
两种打包结果。
注意:
这种方式下,部分插件会在打包时剔除,需要在使用项目中进行安装。
2.3. 引入并使用
这里主要说明两个注意事项:
- 部分组件没有打包到 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')
- 组件使用时,接收两个参数
use-save
与reset-id-with-import
,分别控制 保存按钮显示状态 与 外部文件导入时是否替换Process节点ID属性。
2.4 注意事项
当需要将其编译为组件使用时,需要 十分注意 以下内容:
- 默认该方式使用
vite.lib.config.ts
文件,该模式 默认排除vue,arco-design,bpmn-js,diagram-js
等依赖。所以在 使用时需要单独安装对应的依赖项,或者 修改该文件,将其添加到默认打包依赖中一起编译;但是,vue 和 vue-i18n 是 必须排除 的选项。 - 如果 没有使用 pnpm worspace 模式,在使用时,需要 将生成的
lib
中对应的bpmn-designer.js
(esm 模式)或者bpmn-designer.cjs
(commonjs 模式),以及style.css
复制到项目中,然后使用import {BpmnFullDesigner} from '@/xx/xx/bpmn-designer.js'
的方式引入该组件。 - 编译时默认会生成
.d.ts
文件
如果使用这种方式,个人更加推荐 pnpm workspace
开发模式,以减少代码和配置改动。
如果使用的不是
@arco-design/web-vue
作为 UI 组件库的话,建议将vite.lib.config.ts
中build.rollupOptions.external
内的@arco-design/web-vue
移除。
3. 源码嵌入
编码层面的使用,对于后续迭代来说比较复杂,但是适合需要进行定制化开发的项目。
3.1. 组件复制
下载本项目,将 packages
中对应工具的 src
目录(除 App.vue
外其他内容)以及 shared/*
复制到项目中。
其中
bpmn-designer
中包含BPMN XML
的图形编辑器,bpmn-viewer
中包含BPMN XML
的查看器和模拟器。可以根据业务进行相应的选择并复制对应内容,也可以将两者统一合并到一个项目中,但是此时需要注意styles
中的重复样式处理。
3.2. 依赖安装与配置
目前提供三个模式的 BPMN 编辑/预览模式:
Designer
Mocker
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-import
与unplugin-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-js
的bpmn-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 整体分为三个部分:BpmnToolbar
、BpmnDesigner
、BpmnPanel
。
其中,BpmnDesigner
为必要组件,BpmnToolbar
提供快捷功能支持,BpmnPanel
则用来支撑流程元素的属性编辑与查看;BpmnPanel
与 BpmnToolbar
依赖 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 参数
name | type | desc | default |
---|---|---|---|
xml | string | 默认初始 xml | - |
theme | 'dark' | 'light' | 初始颜色主题 | 'light' |
local | 'zh_CN' | 'en_US' | 初始国际化于语言 | 'zh_CN' |
useSave | boolean | 是否显示工具栏保存按钮 | false |
resetIdWithImport | boolean | 是否在通过工具栏打开 xml 时更新导入文件的 id 为当前流程 id | true |
1.2 events 事件
name | params | desc |
---|---|---|
xml-changed | xml: string | 当 xml 更新时触发 |
1.3 methods 方法
name | params | desc | returns |
---|---|---|---|
createNewProcess | xml: string | 新的 xml 字符串 | - |
toggleLang | lang?: 'zh_CN' | 'en_US' | 目标语言, 为空时相互切换 | - |
toggleTheme | theme?: 'dark' | 'light' | 切换颜色主题 | - |
2. BpmnDesigner
2.1 props 参数
name | type | desc | default |
---|---|---|---|
xml | string | 默认初始 xml | - |
2.2 events 事件
name | params | desc |
---|---|---|
modeler-init | modeler: Modeler | 当 bpmn-js 的编辑器实例初始化时触发, 参数是当前编辑器实例 |
modeler-destroy | modeler: Modeler | 当 bpmn-js 的编辑器实例销毁前触发, 参数是当前编辑器实例 |
root-init | base: { name: string; id: string }[] | 当新的 root 元素解析时, 解析到的根节点的 id 和 name 组成的数组 |
2.3 methods 方法
name | params | desc | returns |
---|---|---|---|
createNewProcess | xml: string | 新的 xml 字符串 | - |
initModeler | - | 重新初始化一次编辑器实例 | - |
destroyModeler | - | 销毁当前编辑器实例 | - |
2.4 slots 插槽
name | desc | params |
---|---|---|
default | 默认插入到画布区域内部的内容 | - |
3. BpmnToolbar
3.1 props 参数
name | type | desc | default |
---|---|---|---|
useSave | boolean | 是否显示工具栏保存按钮 | false |
showLabel | boolean | 是否显示第一组按钮的文字部分, 为 false 时将通过 popover 显示 | false |
resetIdWithImport | boolean | 是否在通过工具栏打开 xml 时更新导入文件的 id 为当前流程 id | true |
3.2 slots 插槽
name | desc | params |
---|---|---|
default | 工具栏默认工具后的插槽 | - |
4. BpmnPanel
4.1 props 参数
name | type | desc | default |
---|---|---|---|
local | 'zh_CN' | 'en_US' | 初始国际化于语言 |