bpmn-js 流程设计器 与 flowable/activiti 拓展的可行性研究

14 篇文章 2 订阅
12 篇文章 7 订阅

bpmn-js 流程设计器 与 flowable/activiti 拓展的可行性研究

前因

最近在准备开源一款流程引擎项目,主要包含 流程设计器 表单设计器 流程引擎,碰见了一个问题 开发过程中 经常需要拓展节点或节点元素,因为bpmn规范可能不满足实际项目需求。记录一下 解决思路。

先上效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

涉及技术

前端使用 bpmn.js

  1. 拓展 flowable.json或者 activiti.json,新增我们 拓展的节点及元素。
{
  "name": "Flowable",
  "uri": "http://flowable.org/bpmn",
  "prefix": "flowable",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "associations": [],
  "types": [
    {
       // 拓展节点名称
      "name": "CustomProperties",
      "superClass": [
        "Element"
      ],
      "meta": {
      	// * 表示所有bpmn节点都可继承该属性
        "allowedIn": [
          "*"
        ]
      },
      "properties": [
       // 拓展属性
         {
          "name": "values",
          "type": "CustomProperty",
          "isMany": true
        },
        {
          "name": "userIdList",
          "isAttr": true,
          "type": "String"
        },
		{
          "name": "userNameList",
          "isAttr": true,
          "type": "String"
        },
		{
          "name": "assigneeField",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "handlerStrategy",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "roleGroupCode",
          "isAttr": true,
          "type": "String"
        },
		{
          "name": "roleCode",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "findUserType",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "combineType",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "relationNodeId",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "actionList",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "taskType",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "nodeType",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "isSequential",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "proportion",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "expression",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "skipExpression",
          "isAttr": true,
          "type": "String"
        },{
          "name": "formName",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "selectFormKey",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "selectPath",
          "isAttr": true,
          "type": "String"
        }
        
      ]
    },
    {
      "name": "CustomProperty",
      "superClass": [
        "Element"
      ],
      "properties": [
        {
          "name": "id",
          "type": "String",
          "isAttr": true
        },
        {
          "name": "name",
          "type": "String",
          "isAttr": true
        },
        {
          "name": "value",
          "type": "String",
          "isAttr": true
        }
      ]
    },
  1. 通过bpmn.js 新增或者修改对应节点元素XML
   // 先判断当前element 是否包含 flowable:CustomProperties ,如果包含 则找出来更新对应属性,如果不存在,则创建后, 在更新整个 extensionElements 即可
 createOrUpdateCustomProperties(property, value) {
      const that = this
      const bpmnModeler = that.bpmnModeler()
      const bpmnFactory = bpmnModeler.get('bpmnFactory')
      let extensionElements = bpmnHelper.getPropertie(that.element, 'extensionElements')
      if (!extensionElements) {
        extensionElements = elementHelper.createElement('bpmn:ExtensionElements', null, this.element, bpmnFactory)
      }
      const length = extensionElements.get('values').length
      let customProperties
      let customPropertiesIndex = -1
      for (let i = 0; i < length; i++) {
        if (extensionElements.get('values')[i] && extensionElements.get('values')[i].$type === 'flowable:CustomProperties') {
          customProperties = extensionElements.get('values')[i]
          customPropertiesIndex = i
        }
      }
      if (!customProperties) {
        customProperties = elementHelper.createElement('flowable:CustomProperties', null, this.element, bpmnFactory)
      }

      const data = {}
      data[property] = value
      customProperties[property] = value

      if (customPropertiesIndex > -1) {
        extensionElements.get('values')[customPropertiesIndex] = customProperties
      } else {
        extensionElements.get('values').push(customProperties)
      }
      const modeling = bpmnModeler.get('modeling')
      // 更新
      modeling.updateProperties(this.element, {
        extensionElements: extensionElements
      })
    }
elementHelper 如下:
'use strict'

var ElementHelper = {}
module.exports = ElementHelper

/**
 * Creates a new element and set the parent to it
 *
 * @method ElementHelper#createElement
 *
 * @param {String} elementType of the new element
 * @param {Object} properties of the new element in key-value pairs
 * @param {moddle.object} parent of the new element
 * @param {BpmnFactory} factory which creates the new element
 *
 * @returns {djs.model.Base} element which is created
 */
ElementHelper.createElement = function(elementType, properties, parent, factory) {
  var element = factory.create(elementType, properties)
  element.$parent = parent
  return element
}

  1. 前端将这个xml 文件 传递给后端,后端可以通过如下代码解析:
  BpmnXMLConverter bpmnXMLConverter = new BpmnXMLConverter();
        byte[] bytes = processXml.getBytes();
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        XMLInputFactory xif = XMLInputFactory.newInstance();
        InputStreamReader in = null;
        try {
            in = new InputStreamReader(inputStream, "UTF-8");
            XMLStreamReader xtr = xif.createXMLStreamReader(in);
            BpmnModel bpmnModel = bpmnXMLConverter.convertToBpmnModel(xtr);
            // 注意 这个 bpmnModel 已经包含了我们刚才定义的属性
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (XMLStreamException e) {
          e.printStackTrace()
        }
  1. 后台解析结果,即可拿到对应拓展属性与业务对接
    在这里插入图片描述
附上后台读取Utils 方法
    public static final String CUSTOME_EXTENSIONELEMENT = "customProperties";

    /**
     * 功能描述: 从 flowElement 获取 指定名称的 拓展元素
     *
     *
     * @param flowElement 元素
     * @param extensionElementName 拓展元素名称
     * @return : org.flowable.bpmn.model.ExtensionElement
     * @author : zhoulin.zhu
     * @date : 2020/6/19 18:28
     */
    public static ExtensionElement getExtensionElementFromFlowElementByName(FlowElement flowElement, String extensionElementName) {

        if (flowElement == null) {
            return null;
        }
        if (StringUtils.isEmpty(extensionElementName)) {
            extensionElementName = CUSTOME_EXTENSIONELEMENT;
        }
        Map<String, List<ExtensionElement>> extensionElements = flowElement.getExtensionElements();
        for (Map.Entry<String, List<ExtensionElement>> stringEntry : extensionElements.entrySet()) {
            if (stringEntry.getKey().equals(extensionElementName)) {
                for (ExtensionElement extensionElement : stringEntry.getValue()) {
                    if (extensionElement.getName().equals(extensionElementName)) {
                        return extensionElement;
                    }
                }
            }
        }

        return null;
    }

    /**
     * 功能描述: 从拓展元素 获取 拓展 属性值
    /**
     * 功能描述: 从拓展元素 获取 拓展 属性值
     *
     *
     * @param extensionElement 拓展元素
     * @param attributesName 属性名称
     * @return : java.lang.String
     * @author : zhoulin.zhu
     * @date : 2020/6/19 18:30
     */
    public static  String getAttributesFromExtensionElementByName(ExtensionElement extensionElement, String attributesName) {

        if (extensionElement == null
                || StringUtils.isEmpty(attributesName)) {
            return null;
        }
        Map<String, List<ExtensionAttribute>> stringListMap = extensionElement.getAttributes();
        for (Map.Entry<String, List<ExtensionAttribute>> listEntry : stringListMap.entrySet()) {
            if (listEntry.getKey().equals(attributesName)) {
                return listEntry.getValue() != null && listEntry.getValue().size() > 0 ? listEntry.getValue().get(0).getValue() : null;
            }
        }
        return null;
    }

源代码地址

  1. 前端项目.
  2. 后端项目

引用

链接: link.
[1]:bpmn.js.
[2]: 表单设计器: 表单设计器.

在 Vue3 中集成 `bpmn-js` 在线设计 `flowable` 流程图的实现代码如下: ```vue <template> <div> <div ref="canvas" class="bpmn-canvas"></div> <div> <button @click="save">保存</button> <button @click="undo">撤销</button> <button @click="redo">重做</button> </div> </div> </template> <script> import BpmnModeler 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-properties-panel/dist/assets/bpmn-js-properties-panel.css'; import 'bpmn-js-properties-panel/dist/assets/properties.css'; import 'bpmn-js-properties-panel/dist/assets/properties-panel.css'; export default { mounted() { this.bpmnModeler = new BpmnModeler({ container: this.$refs.canvas, propertiesPanel: { parent: '#properties-panel', }, additionalModules: [ // properties panel module require('bpmn-js-properties-panel'), // properties provider module require('bpmn-js-properties-panel/lib/provider/camunda'), // custom module require('./CustomModule'), ], moddleExtensions: { camunda: require('camunda-bpmn-moddle/resources/camunda'), }, }); this.bpmnModeler.createDiagram(() => { this.bpmnModeler.get('canvas').zoom('fit-viewport'); }); }, methods: { save() { this.bpmnModeler.saveXML((err, xml) => { if (err) { console.error('保存失败', err); } else { console.log('保存成功', xml); // 发送给后台进行保存 } }); }, undo() { this.bpmnModeler.get('commandStack').undo(); }, redo() { this.bpmnModeler.get('commandStack').redo(); }, }, }; </script> ``` 首先,我们需要引入 `bpmn-js` 和 `bpmn-js-properties-panel` 两个依赖。然后,在 `additionalModules` 中添加我们自定义的 `CustomModule` 模块,该模块将在后面实现。 在 `mounted` 生命周期中,我们创建了一个 `BpmnModeler` 实例,并使用 `createDiagram` 方法创建一个新的 BPMN 图表。最后,我们将 BPMN 图表缩放以适应视口大小。 在 `methods` 中,我们实现了保存、撤销和重做功能。`save` 方法将当前 BPMN 图表保存为 XML 字符串,你可以将该字符串发送给后台进行保存。`undo` 和 `redo` 方法分别实现了撤销和重做操作。 接下来我们需要实现 `CustomModule` 模块。该模块可以添加自定义的工具栏按钮和事件监听。下面是实现代码: ```js const CustomModule = { __init__: [ 'customRenderer', 'customPalette', 'customContextPad', ], customRenderer: ['type', require('./CustomRenderer')], customPalette: ['type', require('./CustomPalette')], customContextPad: ['type', require('./CustomContextPad')], }; module.exports = CustomModule; ``` 这里我们定义了三个模块:`customRenderer`、`customPalette` 和 `customContextPad`。分别用来实现自定义的渲染、自定义的工具栏和自定义的上下文菜单。 下面是 `CustomRenderer` 的实现代码: ```js function CustomRenderer(eventBus, bpmnRenderer) { bpmnRenderer.drawShape = function (p, element) { const shape = bpmnRenderer.drawShape(p, element); if (element.type === 'bpmn:ServiceTask') { const rect = drawRect(p, 2, shape.width - 4, shape.height - 4, 10, '#000000'); const path = drawPath(p, `M ${shape.width / 2} ${shape.height / 2 + 10} L ${shape.width / 2} ${shape.height - 10}`, { strokeWidth: 2, stroke: '#ffffff' }); return [shape, rect, path]; } return shape; }; } CustomRenderer.$inject = ['eventBus', 'bpmnRenderer']; module.exports = CustomRenderer; // 封装绘制矩形方法 function drawRect(p, x, y, width, height, color) { return p.rect(x, y, width, height, 10).attr({ fill: color }); } // 封装绘制线段方法 function drawPath(p, path, attrs) { return p.path(path).attr(attrs); } ``` 这里我们实现了自定义的渲染,通过修改 `bpmnRenderer` 来实现。我们在 `bpmn:ServiceTask` 上添加了一个矩形和一条竖线,用来表示该节点需要进行某个操作。 下面是 `CustomPalette` 的实现代码: ```js function CustomPalette(palette, create) { this._create = create; palette.registerProvider(this); } CustomPalette.$inject = ['palette', 'create']; module.exports = CustomPalette; CustomPalette.prototype.getPaletteEntries = function () { const create = this._create; function createAction(type) { return function (event) { const shape = create.start(event, type); create.end(event, shape); }; } return { 'create.service-task': { group: 'activity', className: 'bpmn-icon-service-task', title: 'Service Task', action: { dragstart: createAction('bpmn:ServiceTask'), click: createAction('bpmn:ServiceTask'), }, }, }; }; ``` 这里我们实现了自定义的工具栏,通过 `getPaletteEntries` 方法返回一个工具栏项,并在 `createAction` 方法中实现具体的操作。 最后是 `CustomContextPad` 的实现代码: ```js function CustomContextPad(eventBus, contextPad, create, elementFactory) { this._eventBus = eventBus; this._contextPad = contextPad; this._create = create; this._elementFactory = elementFactory; eventBus.on('shape.added', 2000, (event) => { const shape = event.element; if (shape.type === 'bpmn:ServiceTask') { this.appendAction(shape, 'Edit', 'bpmn-icon-service-task', 'custom-action', { action: 'edit', icon: 'bpmn-icon-service-task', }); } }); } CustomContextPad.$inject = ['eventBus', 'contextPad', 'create', 'elementFactory']; module.exports = CustomContextPad; CustomContextPad.prototype.appendAction = function (element, name, cssClass, title, options = {}) { const { action, icon, } = options; const self = this; const click = options.click || function (event, element) { self.triggerAction(action, element); }; const button = document.createElement('div'); button.title = title; button.className = `entry ${cssClass}`; button.innerHTML = `<i class="${icon}"></i>`; this._contextPad.registerProvider(element, { getEntries: () => { const entries = {}; entries[name] = { action: click, className: cssClass, id: action, title, }; return entries; }, }); return button; }; CustomContextPad.prototype.triggerAction = function (action, element) { switch (action) { case 'edit': this._eventBus.fire('serviceTask.edit', { element }); break; default: console.warn(`未知操作: ${action}`); break; } }; ``` 这里我们实现了自定义的上下文菜单,通过在 `shape.added` 事件中添加自定义的菜单项。 至此,我们实现了在 Vue3 中集成 `bpmn-js` 在线设计 `flowable` 流程图的完整代码。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风中思絮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值