Business Process Model and Notation(bpmn.js)
i. 安装
本文使用的 bpmn-js 版本是 ^11.0.1,bpmn-js-properties-panel 版本是 ^1.16.0
yarn add bpmn-js@11.0.1
ii. 创建
<div id="container"></div>
然后再创建实例并挂载
import Modeler from "bpmn-js/lib/Modeler";
const container = document.querySelector('#container');
var modeler = new BpmnModeler({
container,
additionalModules: [],
BaseModeler 的方法
iii. Palette 与 ContentPad
这两个组件在实例化建模器的时候已经渲染到了页面上,只需要引入对应的样式文件即可。
import "bpmn-js/dist/assets/diagram-js.css";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn.css";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css";
const modeler = new Modeler({
/* 扩展*/
additionalModules: [],
/* 在 vi 中会提及 */
modeler.attachTo(container);
/* 在 ix 中会提及 */
modeler.createDiagram();
iv. 销毁
modeler.destory()
v. 清空
modeler.clear();
vi. 添加DOM
const modeler = new Modeler({
/* 扩展*/
additionalModules: [],
modeler.attachTo(container);
vii. 移除DOM
modeler.detach();
viii. 获取所有 element
modeler.get('elementRegistry');
ix. 获取扩展功能模块列表
modeler.getModules();
x. 读取默认XML
// 创建空白流程图
// 内部调用了importXML方法,读取内部的默认xml字符串
modeler.createDiagram();
xi. 事件注册/注销
// 注销事件监听器
modeler.off(eventName, callback);
// 注册事件监听,同名将删除以前的监听器,privorty可不传,程序会自动替换回调函数
modeler.on(eventName, priority, callback, target);
xii. 画布
modeler.get('canvas').zoom('fit-viewport', 'auto'); //画布自适应居中
modeler.get('canvas').zoom(2.0); //放大至2倍
xiii. XML转图
modeler.importXML(xml, (err) => {
if (err) {
/* 错误处理 */
} else {
/* 导入成功处理 */
xiv. 文件导入
结合 xii 的步骤,通过读取 XML 文件
<input type="file" accept=".xml,.bpmn," id="upload" style="display: none;" />
然后通过绑定点击事件
const upload = document.querySelector('#upload');
* @description 其他按钮点击事件
const handleBtnClick = () => {
upload.click();
* @description upload的input事件
const handleFileInput = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.readAsText(file);
render.onload = (event) => {
/* 读取到的文件内容 */
const text = evet.target.result;
upload.value = '';
upload.addEeventListener('input', handleFileInput);
xv. 图转XML
modeler.saveXML({ format: true }, (err, data) => {
if (err) {
/* 错误处理 */
} else {
/* data 就是导出的XML文本 */
xvi. 图转SVG
modeler.saveSVG({ format: true }, (err, data) => {
if (err) {
/* 错误处理 */
} else {
/* data 就是导出的SVG文本 */
xvii. 文件导出
结合 xiv、xv 的步骤,导出 bpmn 图文件
/**
* @description 下载
* @params { object } [obj]
* @params { string } [obj.filname]
* @params { string } [obj.data]
* @return { void }
const exportFile = ({ filename, data }) => {
const url = URL.createObjectURL(new Blob([ data ]));
const aLink = document.createElement('a');
aLink.href = url;
aLink.download = filename;
/* 点击下载 */
aLink.click();
/* 释放 */
URL.revokeObjectURL(url);
* @description 下载文件事件
const handleDownload = () => {
/* 导出 XML */
exportFile({ filename: 'test.xml', data: 'XML文本数据' });
/* 导出 SVG */
exportFile({ filename: 'test.svg', data: 'SVG文本数据' });
xviii. undo
撤销
modeler.get('commandStack').undo();
xix. redo
重做
modeler.get('commandStack').redo();
xx. modeling
const getModeling = modeler => {
return modeler.get('modeling');
xxi. modeling.updateProperties
更新 element 属性值
const updateElement = (modeler, element, properties) => {
getModeling(modeler).updateProperties(element, properties);
xxii. modeling.removeElements
移除元素
const removeElements = (modeler, elements = []) => {
getModeling(modeler).removeElements(elements);
const removeElement = (modeler, element) => {
removeElements(modeler, [element]);
xxiii. properties-panel
安装
yarn add bpmn-js-properties-panel
Using Vite
yarn add bpmn-js-properties-panel @bpmn-io/properties-panel
使用
/* vue3 */
import { defineComponent, ref, onMounted } from "vue";
/* bpmn-js */
import Modeler from "bpmn-js/lib/Modeler";
import "bpmn-js/dist/assets/diagram-js.css";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn.css";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css";
/* bpmn-js-properties-panel */
import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, } from 'bpmn-js-properties-panel';
import "bpmn-js-properties-panel/dist/assets/properties-panel.css";
import "bpmn-js-properties-panel/dist/assets/element-templates.css";
/* css module */
import $style from "./index.module.scss";
export default defineComponent({
setup() {
* @description bpmn container Ref
* @type { object }
const modelerRef = ref(null);
onMounted(() => {
const modeler = new Modeler({
/* 扩展*/
propertiesPanel: {
parent: "#properties"
additionalModules: [BpmnPropertiesPanelModule, BpmnPropertiesProviderModule]
modeler.attachTo(modelerRef.value);
modeler.createDiagram();
return () => (
<div class={$style.container}>
<div class={$style.modeler} ref={modelerRef} />
<div id="properties"></div>
样式
/* index.module.scss */
.container{
height: 100%;
width: 100%;
display: flex;
.modeler{
width: 100%;
height: 100%;
:global{
#properties{
border-left: .1rem solid var(--el-border-color);
box-sizing: border-box;
min-width: 30rem;
}
xxiv. 添加快捷键
const modeler = new Modeler({
/* 快捷键 */
keyboard: { bindTo: document },
以下快捷键是自带的
快捷键 | 说明 |
---|---|
Ctrl+A | 全选 |
Ctrl+C | 复制 |
Ctrl+V | 粘贴 |
Ctrl+Z | Undo |
Ctrl+Y | Redo |
Ctrl+Shift+Z | Redo |
Ctrl+Plus | Zoom In |
Ctrl+Minus | Zoom Out |
Ctrl+Delete | 删除 |
Ctrl+Up | 上移 |
Ctrl+Down | 下移 |
Ctrl+Left | 左移 |
Ctrl+Right | 右移 |
Mouse Wheel | 上/下移 |
Shift+Mouse Wheel | 左/右移 |
Ctrl+Mouse Wheel | 缩放 |
L | Lasso Tool(切换工具) |
S | Space Tool(切换工具) |
为了避免与外部快捷键冲突
//开启快捷键
modeler.get('keyboard').bind(document);
//关闭快捷键
modeler.get('keyboard').unbind();
xxv. 发布-订阅
1. class
使用
import EventEmitter from '@/utils/EventEmitter.js';
/* 订阅 */
EventEmitter.on('test', (description) => { console.log('test', description) });
/* 发布 */
EventEmitter.emit('test', '第1次发布');
写一个 EventEmitter 实用类(不必实例化)
/**
* @description function(){}
* @param {*} val
* @returns { boolean }
export const isFunction = val => '[object Function]' === toString.call(val);
* @description 发布-订阅
* @description 静态方法调用直接在类上进行,不能在类的实例上调用
* @description 静态方法通常用于创建实用程序函数
export default class EventEmitter {
static _events = {};
constructor() { }
* @description 订阅
* @param {*} name 事件名
* @param {*} fn 事件函数
* @param {*} context 事件调用者(this 指向)
* @returns
static on(name, fn, context) {
return EventEmitter.addListener(name, fn, context)
* @description 订阅(只触发一次)
static once(name, fn, context) {
return EventEmitter.addListener(name, fn, context, true)
* @description 发布
static emit(name, ...params) {
const fns = EventEmitter._events[name] || [];
fns.forEach(item => {
item.call(item.context, ...params);
if(item.once){
EventEmitter.removeListener(name, item);
return true;
static addListener(name, fn, context = null, once = false) {
if (!isFunction(fn)) {
throw new TypeError('fn:第二个参数必须是一个function');
fn.context = context;
fn.once = once;
const fns = EventEmitter._events[name] || [];
EventEmitter._events[name] = [...fns, fn];
return EventEmitter;
static removeListener(name, fn) {
if (!isFunction(fn)) {
throw new TypeError('fn:第二个参数必须是一个function');
const fns = EventEmitter._events[name] || [];
const idx = fns.findIndex(item => item === fn);
if(idx > -1){
fns.splice(idx, 1);
if(fns.length === 0){
delete EventEmitter._events[name];
return EventEmitter;
static removeAllListeners(name){
delete EventEmitter._events[name];
static getAllListeners(name){
return EventEmitter._events[name] || [];
static getListenerCount(name){
const fns = EventEmitter._events[name] || [];
return fns.length;
static getEventNames(){
return Object.keys(EventEmitter._events);
2. function
import { emitter } from './EventEmitter';
/* 订阅 */
emitter.on('test', (description) => { console.log('test', description) });
/* 发布 */
emitter.emit('test', '第1次发布');
貌似比 class 语法糖更加灵活
/**
* @description 发布订阅
function EventEmitter() { };
EventEmitter.prototype = {
on(name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx,
return this;
once(name, callback, ctx) {
const self = this;
function listener() {
self.off(name, listener);
callback.apply(ctx, arguments);
// 标识(指向原来绑定的事件)
listener._ = callback;
return this.on(name, listener, ctx);
emit(name) {
var data = [].slice.call(arguments, 1);
var e = this.e || (this.e = {});
var evtArr = (e[name] || []).slice();
for (var i = 0; i < evtArr.length; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
return this;
off(name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback) {
liveEvents.push(evts[i]);
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
export default EventEmitter;
export const emitter = new EventEmitter();
xxvi. 数据类型判断
/**
* @description Object.prototype.toString
* @param {*} val
* @returns { boolean }
export const toString = Object.prototype.toString;
export const isNumber = val => '[object Number]' === toString.call(val);
export const isBoolean = val => '[object Boolean]' === toString.call(val);
export const isUndefined = val => '[object Undefined]' === toString.call(val);
export const isString = val => '[object String]' === toString.call(val);
export const isArray = val => '[object Array]' === toString.call(val);
export const isAsyncFunction = val => '[object AsyncFunction]' === toString.call(val);
export const isFunction = val => '[object Function]' === toString.call(val);
export const isNull = val => '[object Null]' === toString.call(val);
export const isGeneratorFunction = val => '[object GeneratorFunction]' === toString.call(val);
export const isSymbol = val => '[object Symbol]' === toString.call(val);
export const isBigInt = val => '[object BigInt]' === toString.call(val);
xxvii. 自定义 Palette
1. 自定义 CustomPalette
将 node_modules/bpmn-js/lib/palette 拷贝出来,进行修改、添加
/* CustomPalette/PaletteProvider.js 添加如下代码 */
* @description 自定义 Palette
* @param {*} event
function createVueTask(event) {
const vueTask = elementFactory.createShape({ type: "custom:VueTask" });
create.start(event, vueTask);
assign(actions, {
"create.vue-task": {
group: "activity",
className: "icon-custom vue-task",
title: "自定义Palette (VueTask)",
action: {
click: createVueTask,
dragstart: createVueTask
2. 自定义 EnhancementRenderer
继承自 node_modules/bpmn-js/lib/draw/BpmnRenderer
这个时候并不会绘制这个自定义的 custom:VueTask 类型,所以还需要定义 renderer
/* EnhancementRenderer/EnhancementRenderer.js */
import BpmnRenderer from "bpmn-js/lib/draw/BpmnRenderer";
import { append as svgAppend, attr as svgAttr, create as svgCreate } from "tiny-svg";
import vueIcon from "@/assets/vue.svg";
class EnhancementRenderer extends BpmnRenderer {
constructor(config, eventBus, styles, pathMap, canvas, textRenderer) {
super(config, eventBus, styles, pathMap, canvas, textRenderer, 3000);
this._styles = styles;
// 自定义节点的绘制
this.handlers["custom:VueTask"] = (parentGfx, element, attr) => {
// 渲染外层边框
const task = this.handlers["bpmn:Activity"](parentGfx, element);
// 自定义节点
const customIcon = svgCreate("image");
svgAttr(customIcon, {
...(attr || {}),
width: element.width,
height: element.height,
href: vueIcon
svgAppend(parentGfx, customIcon);
return task;
EnhancementRenderer.$inject = ["config.bpmnRenderer", "eventBus", "styles", "pathMap", "canvas", "textRenderer"];
export default EnhancementRenderer;
3. 自定义 moddle
虽然 render 已经自定义了,但是这个时候还是会报错说 custom 类型未定义
需要增加定义 moddle:moddle-extensions/custom.json
{
"name": "custom",
"prefix": "custom",
"uri": "http://custom.org/schema",
"xml": {
"tagAlias": "lowerCase"
"associations": [],
"types": [
"name": "VueTask",
"superClass": ["bpmn:Task"],
"properties": [
"name": "address",
"type": "String",
"isAttr": true
"name": "username",
"type": "String",
"isAttr": true
"name": "port",
"type": "String",
"isAttr": true
"name": "tables",
"type": "Table",
"isMany": true,
"isBody": true
}
4. 使用
在创建 Modeler 实例时引入上面自定义的内容
/* 自定义 Palette */
import CustomPalette from '@/utils/CustomBpmn/CustomPalette';
/* 自定义 Renderer */
import EnhancementRenderer from '@/utils/CustomBpmn/EnhancementRenderer';
/* moddle */
import customModdleDescriptions from '@/utils/CustomBpmn/moddle-extensions/custom.json'
const modeler = new Modeler({
// ...
additionalModules: [
// ...
CustomPalette,
EnhancementRenderer,
/* moddle */
moddleExtensions: { custom: customModdleDescriptions },
// ...
xxviii. 选中 element
1. 第一种:根据 element 选中
const selectElement = element => {
modeler.get('selection').select(element);
const selectElements = elements => {
modeler.get('selection').select(elements);
2. 第二种:根据 element 的 id 选中
const selectElement = elementId => {