bpmn.js是一个BPMN2.0渲染工具包和Web建模器。使用JavaScript编写,在不需要后端服务器支持的前提下向现代浏览器中查看、创建、嵌入和扩展BPMN2.0流程图。

使用

有两种方式使用bpmn.js:

  • 通过URL引入预先打包的库
  • 通过NPM以模块的方式引入

通过URL引入预先打包的库

<!DOCTYPE html>
<html>
<head>
  <title>bpmn-js-viewer</title>
  <!-- BPMN-JS viewer 查看器 --> 
  <script src="https://unpkg.com/bpmn-js@6.5.1/dist/bpmn-viewer.development.js"></script>
  <!-- diagram-js样式 -->
  <link rel="stylesheet" href="https://unpkg.com/bpmn-js@6.5.1/dist/assets/diagram-js.css" />
  <!-- 字体样式 -->
  <link rel="stylesheet" href="https://unpkg.com/bpmn-js@6.5.1/dist/assets/bpmn-font/css/bpmn.css" />
</head>
<body>
  <!-- BPMN 流程图容器 --> 
  <div id="diagram"></div> 

  <script type="text/javascript">
    // 初始化,见下面
  </script>
</html>

通过NPM以模块的方式引入

在应用中执行下面命令安装bpmn-js到当前应用中

npm i bpmn-js
import BpmnJS from 'bpmn-js/lib/Modeler';

初始化

var bpmnxml; // BPMN 2.0 xml
var viewer = new BpmnJS({
  container: 'diagram'
});

try {
  const { warnings } = await viewer.importXML(xml);
  console.log('rendered');
  var canvas = viewer.get('canvas');
  canvas.zoom('fit-viewport');
} catch (err) {
  console.log('error rendering', err);
}

动态初始化(Attach/Detach)

您也可以将viewer动态地附加或分离到页面上的任何元素:

var viewer = new BpmnJS();

// attach it to some element
viewer.attachTo('#container');

// detach the panel
viewer.detach();

模型扩展

在使用bpmn-js进行可视化编排业务流程的过程中,为了添加业务中所需要的数据,我们需要对BPMN-JS的模型进行扩展,模型扩展(Model Extension)允许我们读取、修改和编写包含扩展属性和元素的BPMN 2.0流程图。

创建模型扩展

模型扩展被定义在一个的JSON文件中,格式如下:

../resources/qaPackage.json

{
  "name": "QualityAssurance",
  "uri": "http://some-company/schema/bpmn/qa",
  // 前缀
  "prefix": "qa",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "types": [
    // 新类型:qa:AnalyzedNode
    {
      "name": "AnalyzedNode",
      // 使用extends扩展现有类型
      "extends": [
        "bpmn:FlowNode"
      ],
      "properties": [
        {
          "name": "suitable",
          "isAttr": true,
          "type": "Integer"
        }
      ]
    },
    // 新类型:qa:AnalysisDetails
    {
      "name": "AnalysisDetails",
      // 如果将扩展元素添加到bpmn:ExtensionElements,则需要声明superClass属性
      "superClass": [ "Element" ],
      "properties": [
        {
          "name": "lastChecked",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "nextCheck",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "comments",
          "isMany": true,
          "type": "Comment"
        }
      ]
    },
    {
      "name": "Comment",
      "properties": [
        {
          "name": "author",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "text",
          "isBody": true,
          "type": "String"
        }
      ]
    }
  ],
  "emumerations": [],
  "associations": []
}

将模型扩展添加到bpmn-js

创建bpmn-js的实例时,通过moddleextensions属性配置添加模型扩展:

import BpmnModeler from 'bpmn-js/lib/Modeler';
import qaExtension from '../resources/qaPackage.json';

const bpmnModeler = new BpmnModeler({
  moddleExtensions: {
    qa: qaExtension
  }
});

BPMN-XML示例

<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
                   xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
                   xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
                   xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
                   xmlns:qa="http://some-company/schema/bpmn/qa"
                   targetNamespace="http://activiti.org/bpmn"
                   id="ErrorHandling">
  <bpmn2:process id="Process_1">
    <bpmn2:task id="Task_1" name="Examine Situation" qa:suitable="0.7">
      <bpmn2:outgoing>SequenceFlow_1</bpmn2:outgoing>
      <bpmn2:extensionElements>
        <qa:analysisDetails lastChecked="2015-01-20" nextCheck="2015-07-15">
          <qa:comment author="Klaus">
            Our operators always have a hard time to figure out, what they need to do here.
          </qa:comment>
          <qa:comment author="Walter">
            I believe this can be split up in a number of activities and partly automated.
          </qa:comment>
        </qa:analysisDetails>
      </bpmn2:extensionElements>
    </bpmn2:task>
    ...
  </bpmn2:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    ...
  </bpmndi:BPMNDiagram>
</bpmn2:definitions>

获取扩展模型属性和元素

// 通过事件触发的形式,我们获取到当前的element

// 获取element的businessObject, 元素属性在此对象下
let businessObject = element.businessObject;

//获取businessObject.extensionElements,如果获取不到说明当前元素的扩展模型为空
let extensionElements = businessObject.extensionElements;

// 获取扩展模型数据
let analysisDetails = extensionElements.values.filter((extensionElement) => {
  return extensionElement.$instanceOf('qa:AnalysisDetails');
})[0];

// 数据原型结构如下:
{
  $children,
  $instanceOf,
  $model,
  $parent: {
    $type: "bpmn:ExtensionElements",
    // 循环引用
    values: [{}]
  },
  lastChecked,
  nextCheck,
  $descriptor: {
    isGeneric: true
    name: "qa:analysisDetails",
    ns: {
      localName: "analysisDetails"
      prefix: "qa"
      uri: "http://some-company/schema/bpmn/qa"
    }
  }
}

修改扩展模型属性和元素

const moddle = bpmnModeler.get("moddle");
const modeling = bpmnModeler.get("modeling");
// 创建 bpmn:ExtensionElements 模型
let extensionElements = moddle.create("bpmn:ExtensionElements");
// 创建 qa:AnalysisDetails 模型
let analysisDetails = moddle.create('qa:AnalysisDetails');
// 将analysisDetails模型附加到extensionElements
extensionElements.get('values').push(analysisDetails);

// 设置 AnalysisDetails 模型属性lastChecked的值
analysisDetails.lastChecked = new Date().toISOString();
// 设置 AnalysisDetails 模型属性nextCheck的值
analysisDetails.nextCheck = new Date().toISOString();
// 更新元素的模型数据
modeling.updateProperties(element, {
  extensionElements,
  suitable: 123
});

自定义建模规则

自定义建模规则(Custom Modeling Rules)用来控制元素在画布被创建的规则。这样可以限制或扩展用户的建模操作

上图展示了创建bpmn:Task类型的元素时,根据定义的规则,我们只能在:

<bpmn:sequenceFlow id="SequenceFlow_1" vendor:allowDrop="bpmn:Task" />

线上创建添加,在其他区域则无法添加。

创建自定义建模规则

通过扩展RuleProvider类,我们可在该类的init方法中通过addRules(actions, priority, fn)添加规则。

file:./customModelingRules.js

import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider';

/**
 * A custom rule provider that decides what elements can be
 * dropped where based on a `vendor:allowDrop` BPMN extension.
 *
 * See {@link BpmnRules} for the default implementation
 * of BPMN 2.0 modeling rules provided by bpmn-js.
 *
 * @param {EventBus} eventBus
 */
export default class CustomRules extends RuleProvider {
  constructor(eventBus) {
    super(eventBus);

  }

  init() {
    // there exist a number of modeling actions
    // that are identified by a unique ID. We
    // can hook into each one of them and make sure
    // they are only allowed if we say so
    this.addRule('shape.create', 1500, function(context) {
      var shape = context.shape,
          target = context.target;
  
      var shapeBo = shape.businessObject,
          targetBo = target.businessObject;
  
      var allowDrop = targetBo.get('vendor:allowDrop');
      console.log(allowDrop);
  
      if (!allowDrop || !shapeBo.$instanceOf(allowDrop)) {
        return false;
      }
  
      // not returning anything means other rule
      // providers can still do their work
      //
      // this allows us to reuse the existing BPMN rules
    });
  }
}

CustomRules.$inject = [ 'eventBus' ];

将自定义建模规则添加到BPMN-JS

创建bpmn-js的新实例时,我们需要使用additionalModules属性添加自定义建模规则。

import BpmnModeler from 'bpmn-js/lib/Modeler';

import customModelingRules from './customModelingRules';

const bpmnModeler = new BpmnModeler({
  additionalModules: [
    customModelingRules
  ]
});

自定义渲染

自定义渲染(Customize Rendering)允许您按所需方式渲染任何形状或连接。

上图展示了将bpmn:Taskbpmn:Event类型的元素渲染成跟之前不同的元素样式。我们看下通过代码是怎么实现的

创建自定义渲染

通过扩展BaseRenderer类,我们可以确保每当要渲染形状或连接元素时都会调用自定义的渲染器。注,我们还指定了高于默认优先级1000的优先级,因此我们的渲染程序将优先被调用。

const HIGH_PRIORITY = 1500;

export default class CustomRenderer extends BaseRenderer {
  constructor(eventBus, bpmnRenderer) {
    super(eventBus, HIGH_PRIORITY);
    this.bpmnRenderer = bpmnRenderer;
    ...
  }
  // 当渲染器被调用时,我们要判断当前元素是否要自定义渲染还是通过默认渲染器渲染
  canRender(element) {
    // bpmn:tasks 和 bpmn:events 类型的元素通过自定义渲染 (并且元素的labelTarget属性为空)
    return isAny(element, [ 'bpmn:Task', 'bpmn:Event' ]) && !element.labelTarget;
  }
  
  // 当上面的方法返回值为true时,下面的 drawShape() 和 drawConnection() 将会被调用
  drawShape(parentNode, element) {
    // 创建并获取元素的默认形状
    const shape = this.bpmnRenderer.drawShape(parentNode, element);
    
    // 如果是bpmn:Task类型的元素,则用我们自己创建的矩形替换元素原来的矩形
    if (is(element, 'bpmn:Task')) {
      // 创建一个圆角矩形
      const rect = drawRect(parentNode, 100, 80, TASK_BORDER_RADIUS, '#52B415');
			// 将圆角矩形附加到父节点
      prependTo(rect, parentNode);
      // 删除原来默认的矩形
      svgRemove(shape);

      return shape;
    }
    
    // 非bpmn:Task类型的元素,将新增的矩形相对父元素左上角偏移
    const rect = drawRect(parentNode, 30, 20, TASK_BORDER_RADIUS, '#cc0000');

    svgAttr(rect, {
      transform: 'translate(-20, -10)'
    });

    return shape;
  },
  // 该方法将在裁剪连接时调用
  drawConnection(parentNode, element) {
  }
}
// 依赖注入,将数组中声明的对象注入到CustomRenderer的构造方法参数
CustomRenderer.$inject = [ 'eventBus', 'bpmnRenderer' ];

将自定义渲染添加到BPMN-JS

创建bpmn-js的新实例时,我们需要使用additionalModules属性添加自定义渲染类。

import BpmnModeler from 'bpmn-js/lib/Modeler';

import customRendererModule from './custom';

const bpmnModeler = new BpmnModeler({
  additionalModules: [
    customRendererModule
  ]
});

自定义属性面板

自定义属性面板(Custom Properties Panel)可以让用户对当前所选元素属性值进行编辑操作,完成数据和BPMN样式的更新。

监听BPMN元素选中事件

import Modeler from 'bpmn-js/lib/Modeler';
const modeler = new Modeler({
  container: $container,
  moddleExtensions: {
    //custom: customModdleExtension
  },
  keyboard: {
    bindTo: document.body
  }
});

let selectedElements = [];
let element = null;

// 监听元素选中
modeler.on('selection.changed', (e) => {
  selectedElements = e.newSelection;
  element = e.newSelection[0];
});

// 监听元素变化
modeler.on('element.changed', (e) => {
  const { element } = e;
});

更新元素属性值

const modeling = modeler.get('modeling');
// 更新元素name/label
modeling.updateLabel(element, name);
// 更新边框颜色
modeling.updateProperties(element, {
  di: {
    'bioc:stroke': color,
  },
  'bioc:stroke': color,
});
// 更新字体颜色
modeling.updateProperties(element, {
  di: {
    'bioc:color': color,
  },
  'bioc:color': color,
});
// 更新填充色
modeling.updateProperties(element, {
  di: {
    'bioc:fill': color,
  },
  'bioc:fill': color,
});
// 更新自定义模型属性信息
modeling.updateProperties(element, {
  'custom:topic': topic
});

自定义控件

创建自定义工具控件

bpmn-js的控件包括,工具面板(palette)和元素右键菜单控件,我们可以通过bpmn-js提供的接口,来添加/修改/删除默认的控件。下面示例展示了如何在左上角的工具面板(palette)中添加一个bpmn:Service Task类型的创建元素。

export default class CustomPalette {
  constructor(create, elementFactory, palette, translate) {
    this.create = create;
    this.elementFactory = elementFactory;
    this.translate = translate;
    // 将该类注册为palette中条目的提供者
    palette.registerProvider(this);
  }

  getPaletteEntries(element) {
    const {
      create,
      elementFactory,
      translate
    } = this;

    function createServiceTask(event) {
      const shape = elementFactory.createShape({ type: 'bpmn:ServiceTask' });
      create.start(event, shape);
    }

    return {
      'create.service-task': {
        // 对多个条目进行分组
        group: 'activity',
        // 定义icon样式
        className: 'bpmn-icon-service-task',
        // 鼠标hover提示
        title: translate('Create ServiceTask'),
        // 支持用户点击和拖动条目
        action: {
          dragstart: createServiceTask,
          click: createServiceTask
        }
      }
    }
  }
}

CustomPalette.$inject = [
  'create',
  'elementFactory',
  'palette',
  'translate'
];

将自定义控件添加到bpmn-js

import CustomPalette from './CustomPalette';

const customControlsModule = {
  __init__: [ 'CustomPalette' ],
  CustomPalette: [ 'type', CustomPalette ]
};

const bpmnModeler = new BpmnModeler({
  container: containerEl,
  additionalModules: [
    customControlsModule
  ]
});

...

创建自定义上下文面板控件

export default class CustomContextPad {
  constructor(config, contextPad, create, elementFactory, injector, translate) {
    this.create = create;
    this.elementFactory = elementFactory;
    this.translate = translate;

    if (config.autoPlace !== false) {
      this.autoPlace = injector.get("autoPlace", false);
    }

    contextPad.registerProvider(this);
  }

  getContextPadEntries(element) {
    const { autoPlace, create, elementFactory, translate } = this;

    function appendServiceTask(event, element) {
      if (autoPlace) {
        const shape = elementFactory.createShape({ type: "bpmn:ServiceTask" });

        autoPlace.append(element, shape);
      } else {
        appendServiceTaskStart(event, element);
      }
    }

    function appendServiceTaskStart(event) {
      const shape = elementFactory.createShape({ type: "bpmn:ServiceTask" });

      create.start(event, shape, element);
    }

    return {
      "append.service-task": {
        group: "model",
        className: "bpmn-icon-service-task",
        title: translate("Append ServiceTask"),
        action: {
          click: appendServiceTask,
          dragstart: appendServiceTaskStart,
        },
      },
    };
  }
}

CustomContextPad.$inject = [
  "config",
  "contextPad",
  "create",
  "elementFactory",
  "injector",
  "translate",
];

将自定义的上下文面板添加到bpmn-js

import CustomContextPad from './CustomContextPad';
import CustomPalette from './CustomContextPad';

// 此处可以将上面自定义的工具控件进行合并
const customControlsModule = {
  __init__: [ 'CustomContextPad' ],
  CustomContextPad: [ 'type', CustomContextPad ]
};
//
export default {
  __init__: [ 'customContextPad', 'customPalette' ],
  // 这是我们自定义的contextPadProvider, 我们可通过见此处名字改为contextPadProvider来覆盖contextPad的默认provider,并且还可以注册多个Provider
  customContextPad: [ 'type', CustomContextPad ],
  // 这是我们自定义的paletteProvider, 我们可通过将此处名字改为paletteProvider来覆盖palette的默认provider(同上)
  customPalette: [ 'type', CustomPalette ]
};

const bpmnModeler = new BpmnModeler({
  container: containerEl,
  additionalModules: [
    customControlsModule
  ]
});

...

根据上面所列举的示例,可以知道bpmn-js建模器(Modeler),通过additionalModules选项来修改和替换现有的功能,包括不限于如下功能:

  • 自定义模型扩展
  • 自定义模型规则
  • 自定义元素渲染
  • 自定义属性面板
  • 自定义控件面板

事件总线

事件备注
diagram.destroy / diagram.clear
selection.changed
elements.changed
element.click
element.hover