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 => {