I haven’t updated my article for a long time. During the time when I haven’t updated my article, I have been practicing and improving my skills. After all, my flag in 2020 is to become a great bull.

This article is a summary of my experience in using BPMN-JS to implement activiti process designer. There are not many Chinese documents for BPMN-JS, many people do not start to develop bPMN-JS, and the bPMN-JS backend uses Camunda, how to use Activiti is also a frustration for many developers.

The problem

What if you need to separate the front end from the back end, think the Activiti designer isn’t working, use BPMN-JS to implement the designer, but the back end is Activiti, and XML isn’t compatible?

To solve

I’m not the only solution. I’m only offering my solution here.

Use BPMN-JS to develop designer

For more information about how to use bPMN-JS, go to Github and search. Here is the official website address: github.com/bpmn-io/bpm…

Official website case address: github.com/bpmn-io/bpm…

When I develop designer, I refer to a series of articles about bPMN-JS developed from 0 by Lin Dai, address: juejin.cn/post/684490…

I believe that after reading the above article, I have been very familiar with BPMN-JS; Let me explain my project:

  • Environment: Windows 10

  • Development tools: VScode, IDEA

  • ** Technology: ** Front-end: Vue + Webpack, back-end SpringBoot + Activiti

1.1 Customizing the right property panel

This is my completely custom properties panel

Part of the code is as follows:

<template> <div> <el-container style="height: 700px"> <el-aside width="80%" style="border: 1px solid #DCDFE6" > <div ref="canvas" style="width: 100%; height: 100%"></div> </el-aside> <el-main style="border: 1px solid #DCDFE6; background-color:#FAFAFA "> <el-form label-width="auto" size="mini" label-position="top"> <! < Component :is= "propsComponent" : Element = "Element" :key= "key"></ Component ></ el-form> </el-main> </el-container> </div> </template>Copy the code

I use propsComponent properties to display properties of different events, such as user task properties, gateway properties

The propsComponent property changes its value by listening on Modeler, Element, and so on:

AddModelerListener () {// Listen to modeler const BPMNJS = this.bpmnModeler const that = this // 'shap.removed ', 'connect.end', 'connect.move' const events = ['shape.added', 'shape.move.end', 'shape.removed'] events.forEach(function(event) { that.bpmnModeler.on(event, e => { var elementRegistry = bpmnjs.get('elementRegistry') var shape = e.element ? elementRegistry.get(e.element.id) : E.shape // console.log(shape) if (event === 'shap.added ') {console.log(' added shape'); Key = e.lement. Id. Replace ('_label', ''); that.propsComponent = bpmnHelper.getComponentByEleType(shape.type); that.element = e.element; } else if (event === 'shape.move.end') {console.log(' moved shape'); that.propsComponent = bpmnHelper.getComponentByEleType(shape.type); that.element = e.shape; } else if (event === 'shape.removed') {console.log(' Shape.removed ') // Show default propertythat.propsComponent = 'CommonProps'}})} }, AddEventBusListener () {// Listen on element let that = this const eventBus = this.bpmNModeler. get('eventBus') const eventTypes  = ['element.click', 'element.changed', 'selection.changed'] eventTypes.forEach(function(eventType) { eventBus.on(eventType, function(e) { if (eventType === 'element.changed') { that.elementChanged(e) } else if (eventType === 'element.click') { Console. log(' Clicked on element'); if (! e || e.element.type == 'bpmn:Process') { that.key = '1'; that.propsComponent = 'CommonProps' that.element = e.element; } else {// Display the property that. Key = e.lement. Id; that.propsComponent = bpmnHelper.getComponentByEleType(e.element.type); that.element = e.element; }}})})},Copy the code

Due to the special nature of VUE, components need to be introduced before attribute components can be used

components: {
    CommonProps,
    ProcessProps,
    StartEventProps,
    EndEventProps,
    IntermediateThrowEventProps,
    ExclusiveGatewayProps,
    ParallelGatewayProps,
    InclusiveGatewayProps,
    UserTaskProps,
    SequenceFlowProps,
    CallActivityProps
  },
Copy the code

Next comes the page that implements the individual event properties.

1.2 adapter activiti

Since BPMN-JS is officially compatible with Camunda, there are some incompatibables for Activiti. In order to enable BPMN-JS to use Activiti, we need to extend activiti code in BpmnModeler as follows:

import activitiModdleDescriptor from '.. /js/activiti.json'; This. bpmnModeler = new bpmnModeler ({container: canvas, // Add properties panel, add translation module additionalModules: [customTranslateModule, customControlsModule], // moddleExtensions: {activiti: activitiModdleDescriptor } });Copy the code

For the activiti.json file, I recommend you look at the custom metamodel example

1.2.1 How to configure the activiti.json file 🌟

{"name": "Activiti", // identify the Activiti" uri": "http://activiti.org/bpmn", // add the Activiti namespace "prefix": "Activiti", / / attribute prefix "XML" : {" tagAlias ":" lowerCase "}, "associations" : [], "types" : [{" name ": "Process", // < bPMn2 :Process > tag "isAbstract": true, "extends": [" BPMN :Process" // inherits from < bPMn2 :Process >], "properties": [// Attribute {"name": "candidateStarterGroups", // attribute name" isAttr": true, // attribute type" type": "String" // attribute type}, {"name": "candidateStarterUsers", "isAttr": true, "type": "String" }, { "name": "versionTag", "isAttr": true, "type": "String" }, { "name": "historyTimeToLive", "isAttr": true, "type": "String" }, { "name": "isStartableInTasklist", "isAttr": true, "type": "Boolean", "default": }, {"name":"executionListener", // Listener property "isAbstract": }, "emumerations": []}, "emumerations": []}, "emumerations": []}, "emumerations": []}Copy the code

Example: My project needs to add a custom attribute to the user task nodeType

{  "name": "UserTask",  "isAbstract": true,   "extends": [    "bpmn:UserTask"  ],  "properties": [    {      "name": "nodeType",      "isAttr": true,      "type": "String"    },  ] }
Copy the code

1.3 About partial extension and full customization 🌟

For the left side of the toolbar, the front-end project: SRC/edit – modeler/js/customController/CustomPalette js file

Q:

As you can see, I have customized the user task and call activity nodes. For the other nodes, I use bPMN-js.

What if I don’t want to use bPMN-JS?

Answer:

SRC/edit – modeler/js/customController/index. Js file

import CustomContextPad from './CustomContextPad'; import CustomPalette from './CustomPalette'; export default {__init__: [ 'customContextPad', 'customPalette' ],customContextPad: [ 'type', CustomContextPad ],customPalette: [ 'type', CustomPalette ]};Copy the code

Here, we use the customPalette, or paletteProvider if we want to fully customize it;

Likewise: Fully custom contextPad with contextPadProvider, fully custom property panel with propertiesProvider

import CustomContextPad from './CustomContextPad'; import CustomPalette from './CustomPalette'; export default {__init__: [ 'contextPadProvider', 'paletteProvider' ],contextPadProvider: [ 'type', CustomContextPad ],paletteProvider: [ 'type', CustomPalette ]};Copy the code

1.4 About attribute prefix 🌟

Q:

As we all know, bPMN-JS generated XML file attributes prefix is camunda, so how to change the prefix we need?

A:

There are two ways to do this

One is to extend the JSON file, for example we need the activiti prefix to extend activiti.json

The second is to modify the initialization XML file directly. When we open the designer, we importXML of an empty node, which we need to add to the XML.

For example, I need to add a prefix normal to generate the following properties: normal:nodeType; We add this sentence to the XML: XMLNS :normal=”flowable.org/bpmn/normal…

<? The 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:bpmn di="http://www.omg.org/spec/BPMN/20100524/DI"xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"xmlns:di="http://www.omg.o rg/spec/DD/20100524/DI"xmlns:normal="http://flowable.org/bpmn/normal"xsi:schemaLocation="http://www.omg.org/spec/BPMN/20 100524/MODEL BPMN20.xsd"id="sample-diagram" targetNamespace="http://activiti.org/bpmn"><bpmn2:process id="Process_1" isExecutable="true"></bpmn2:process><bpmndi:BPMNDiagram id="BPMNDiagram_1"><bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"></bpmndi:BPMNPlane></bpmndi:BPMNDiagram></bpmn2:definitions>Copy the code

After being added to the XML, how do designer generated attributes be added?

It’s actually quite simple. We can prefix updateProperties with something like:

modeling.updateProperties(element, {'normal:nodeType': 'nodeType'})
Copy the code

1.5 Since the property panel is customized, how to synchronize the property value of the property panel to XML after modifying it; And how do I synchronize the properties panel 🌟 if I change the properties graphically

The project is a VUE architecture, so take advantage of vUE’s strengths: listening

Part of the code is as follows:

watch: {id (newVal, oldVal) {const bpmnModeler = this.bpmnModeler(); const modeling = bpmnModeler.get('modeling'); modeling.updateProperties(this.element,{'id':newVal}); },name(newVal, oldVal){const bpmnModeler = this.bpmnModeler(); const modeling = bpmnModeler.get('modeling'); modeling.updateProperties(this.element,{'name':newVal}); },// Monitor the value of element and get the response attribute when it changes. Element: {deep: true,immediate: true,handler(newVal, oldVal) {if(newVal) {const bpmnModeler = this.bpmnModeler(); / / I'm here because of the project is the method to get bpmnModelerthis. Id = newVal. BusinessObject. Get (" id "); this.name = newVal.businessObject.get('name'); // Initialize assignment const modeling = bpmnModeler.get('modeling'); modeling.updateProperties(this.element,{'name':this.name}); modeling.updateProperties(this.element,{'process_namespace':this.process_namespace}); modeling.updateProperties(this.element,{'process_id':this.id}); }}}}Copy the code

Because Element is a complex type, depth listening must be turned on.

Synchronous XML: using modeling. UpdateProperties method, can also use newVal. BusinessObject. $attrs = [‘ name ‘] this. The name change

Modify graphic properties Sync properties panel: Since depth is listening for Element, modifying graphic properties is equivalent to modifying Element, so this is listening for

1.6 How Can I Add a Listener 🌟

See SRC \edit-modeler\ Components \CommonProps. Vue

1.7 How Can I Add a User-defined label 🌟

I suggest you read:! [Custom metamodel example](github.com/bpmn-io/bpm…)

1.8 How Can I Add multiple Instances 🌟

Here is a way to add code to set up multiple instances by clicking the wrench directly in the graph

const moddle = bpmnModeler.get('moddle'); loopCharacteristics = moddle.create('bpmn:MultiInstanceLoopCharacteristics'); loopCharacteristics['collection'] = 'flow_assignee'; loopCharacteristics['elementVariable'] = 'flow_assignee'; let completionCondition = elementsHelper.createElement('bpmn:FormalExpression', { body: '${mulitiInstance.completeTask(execution,passResult,mulitiActivityId)}' }, loopCharacteristics, bpmnFactory); loopCharacteristics['completionCondition'] = completionCondition; modeling.updateProperties(element, { loopCharacteristics: loopCharacteristics });Copy the code

1.9 Obtaining All Nodes and the root node 🌟

// BpmNModeler._definitions. RootElements [0] // BpmnModeler.get ('canvas').getrootelement ()Copy the code

1.10 How can I Add a node 🌟 to its peer node

Such as:

Add BoundaryEvent to the sibling of SequenceFlow, just get all the nodes under the root node and push into the node you added

bpmnModeler._definitions.rootElements[0].flowElements.push(boundaryEvent);
Copy the code

1.11 Default imported empty XML, assign dynamic value to tag ID: no flow to show/collaboration 🌟

My default empty XML is as follows:

The resulting XML is as follows:

As you can see in the figure above, the ID starts with a number, which is what causes 😂😂😂

As long as it starts with a letter, for example: id = ‘T-${uuidV4 ()}’;

There should be applause 👏👏👏

1.12 BpmnViewer process trace shows the flow chart, but the flow chart is obscured 🌟

Add the following code can be solved

const currentViewbox = this.bpmnViewer.get('canvas').viewbox()      const widthWindow = window.outerWidth;      const heightWindow = window.outerHeight;      const elementMid = {        x: widthWindow / 2,        y: heightWindow / 2      }      this.bpmnViewer.get('canvas').viewbox({        x: elementMid.x - currentViewbox.width / 2,        y: elementMid.y - currentViewbox.height / 2,        width: currentViewbox.width,        height: currentViewbox.height      })      const width = document.getElementById('canvas').offsetWidth      this.bpmnViewer.get('canvas').zoom(width / this.width)
Copy the code

There are two identical attributes in 1.13 XML 🌟

If you are in an extended xxx.json file, such as an activiti.json file; You configure the attributes flowable:assignee; for user tasks in the JSON file. This attribute is added under businessObject, so if we were to modify businessobject.attrs [‘flowable:assignee’\], the attribute would be added under Businessobject.attrs, So when you generate XML, you generate two

1.14 Clean the canvas

bpmnModeler.clear()

1.15 Setting the Default Stream

const newDefaultFlow = elementRegistry.get(element.id).businessObject;

modeling.updateProperties(targetElement, { default: newDefaultFlow });

1.16 Invocation of the master subprocess

Active node attributes in the flowable: calledElementType = “id” can be id can also be a key, id represents a process definition in the table id, the key is to define the key fields in the table, too

1.17 Disable some canvas operations

const bpmnModeler = new BpmnModeler({ container: '#canvas', additionalModules:[BpmnModeler, {paletteProvider:['value',' "], // Disable left panel labelEditingProvider:['value', "], // Disable editing contextPadProvider: ['value', "], // disable contextPad bendPoints: [' value '{}], / / ban process line transformation waypoints zoomScroll: [' value', ' '], / / the ban on canvas rolling moveCanvas: [' value ', ' '], / / drag}] is forbidden, height: '400px' });Copy the code

If the back end passes to the front end a JSON file instead of XML; Please go back to 🤔️🤔️ ️

1.18 Hide the BPMNJS icon

.bjs-powered-by { display:none ! important; }Copy the code

Or:

// Delete BPMN logo bpmn. IO const bjsIoLogo = document.querySelector('.js-powered-by '); while (bjsIoLogo.firstChild) { bjsIoLogo.removeChild(bjsIoLogo.firstChild); }Copy the code

1.19 Timeout is automatically completed

XML is as follows:

<sequenceFlow id="Flow_1hu7yoy" sourceRef="Activity_1ig8oe5" targetRef="Event_1xmxdxy" /> <serviceTask id="Activity_1ig8oe5"> <incoming>Flow_1tvddwv</incoming> <outgoing>Flow_1hu7yoy</outgoing> </serviceTask> <boundaryEvent  id="Event_1bi4wq0" attachedToRef="Activity_1ig8oe5"> <timerEventDefinition id="TimerEventDefinition_0wsqmm3" /> </boundaryEvent>Copy the code

The time attribute in timerEventDefinition should be added

2 back-end Activiti implementation

How to build activiti environment, I believe everyone can baidu to, I only introduce how to bPMN-JS and Activiti compatible

3.1 Parsing BPMN Files

Figure shows how a process file in XML format can be deployed to the engine in several large steps

3.2 Start by transferring XML from the front-end to the back-end

The HTTP request will carry two main parameters, bPMN_XML and svG_XML

Since activiti is stored in the database as a JSON file, we need to convert the BPMn_xml file to JSON

Activiti’s conversion methods are not enough for me. I have custom conversion methods and parsers. Activiti also allows you to customize parsers

First method:

Public static JsonNode converterXmlToJson(String bpmnXml) {// Create a conversion object BpmnXMLConverter = new BpmnXMLConverter(); / / XMLStreamReader reads the XML resource XMLInputFactory XMLInputFactory = XMLInputFactory. NewInstance (); StringReader stringReader = new StringReader(bpmnXml); XMLStreamReader xmlStreamReader = null; try { xmlStreamReader = xmlInputFactory.createXMLStreamReader(stringReader); } catch (XMLStreamException e) { e.printStackTrace(); } / / UserTaskXMLConverter class is my custom BpmnXMLConverter addConverter (new UserTaskXMLConverter ()); / / object to transform XML into BpmnModel BpmnModel BpmnModel = bpmnXMLConverter. ConvertToBpmnModel (xmlStreamReader); BpmnJsonConverter = new BpmnJsonConverter(); / / BpmnModel object into json JsonNode jsonNodes = bpmnJsonConverter. ConvertToJson (BpmnModel); // The returned JSON is saved to the database. }Copy the code

The above code uses the BpmnModel object and XML interoperability provided by Activiti’s Activiti-BPMN-Converter module. By creating org. Activiti. BPMN. Converter. BpmnXMLConverter class object call the corresponding method BpmnModel object can be realized with XML conversion between operations.

First, THE custom class UserTaskXMLConverter is because I have a custom attribute in my user task event; When converting XML to BpmnModel, if it is a user task event it will go through my custom UserTaskXMLConverter class

The next step is to convert the BpmnModel to JSON, noting that each bPMNModel. attributes holds all the attributes

3.3 Custom BpmnJsonConverter file

The BpmnJsonConverter class is available in the Activiti-JSON-Converter module provided by Activiti. Let’s compare my custom with the official one

Static class (Custom); static class (Custom); static class (Custom);

Q: Why customize these classes?

A:

1. Because front-end custom attributes (e.g., multi-instance attributes, default process attributes) are lost using the official toBpmnModel transformation, our custom class mainly puts the custom attributes in the attribute, and transforms the multi-instance attributes into Activiti's BPMN specification accepts. 2. Add custom attribute key values to convertElementToJsonCopy the code

User task custom attribute conversion code:

// Multi-instance type String multiInstanceType = getPropertyValueAsString(PROPERTY_MULTIINSTANCE_TYPE, elementNode); String multiInstanceCondition = getPropertyValueAsString(PROPERTY_MULTIINSTANCE_CONDITION, elementNode); if (StringUtils.isNotEmpty(multiInstanceType) && !" none".equalsIgnoreCase(multiInstanceType)) { String name = getPropertyValueAsString(PROPERTY_NAME, elementNode); MultiInstanceLoopCharacteristics multiInstanceObject = new MultiInstanceLoopCharacteristics(); if ("sequential".equalsIgnoreCase(multiInstanceType)) { multiInstanceObject.setSequential(true); } else { multiInstanceObject.setSequential(false); } if (StringUtils.isNotEmpty(multiInstanceCondition)) { try { Integer.valueOf(multiInstanceCondition); } catch (Exception ex) {throw new WorkflowApiException(name + "configured as a countersign, but not an integer through the weight "); } multiInstanceObject.setCompletionCondition("${nextTaskEvaluator.isComplete(execution," + multiInstanceCondition + ")} "); } else {throw new WorkflowApiException(name + "configured to countersign, but not configured to pass weight "); }}Copy the code

3.4 Bpmn Parsing processor

Activiti supports custom BPMN parsing handlers (BpmnParseHandler) when parsing BPMN resource files. The custom BPMN parsing handler can be called at the beginning of parsing an Element or at the end of parsing. We can change the properties of some BPMN objects.

Adding a BPMN parsing handler configures the attributes “preBpmnParseHandlers” and “postBpmnParseHandlers” in the Activiti engine configuration file. The following code adds a parsing handler for the Pre (front) and Post (Post) types respectively

The above code adds two types of BPMN parsing handlers, the type distinction being made for a more nuanced division of processor types; Pre handlers are always executed first, before the defined elements in all process files, while Post handlers are executed last, after the defined elements in all process files. If parsing processors have specific order requirements, they can be distinguished by Pre and Post types.

summary

Overall, full development is a bit of a work in progress and requires some knowledge and patience with BPMN-JS and Activiti.

La la la ~~, finished writing finished writing, I am a happy little fairy.