bpmn.js~流程配置自定义开发记录

4,591 阅读7分钟

流程引擎在业务流转过程中应用比较广泛,bpmn.js能够很方便将BPMN流程图用到你的应用中。定制化的开发,比如自定义工具栏、汉化等等个性化需求在需求研发过程中比较常见,怎么实现呢?有一些经验分享供参考。

文字2500左右,预计5分钟阅读完

一.需求来源

xxx1.3版本,在场景解决方案-方案配置处需要新增流程配置。这一块功能之前在xx运维平台-模板管理-流程模板处已经做过一版了。但是就产品所说,那一版交互性不是很好,对内推广受到严重阻碍,具体体现在:

  1. 左侧工具栏图标没有按钮说明,且存在多余的操作按钮

  2. 点击画布内的元素出现的context-pad,出现多余的操作按钮,混淆视听

  3. 右侧属性操作面板是英文的 总结就是说就想一目了然,满足基本操作,多余的东西全部去掉。对照着新的需求,需要实现的几点可以概括为:

  4. 自定义左侧工具栏:a、新增全屏【后因时间较紧,未实现】,并行,会签等操作按钮;b、所有操作按钮底下都新增汉字说明

  5. 自定义context-pad:去除多余的按扭

  6. 去除默认的右侧属性栏,自定义右侧属性操作面板,样式和交互大改 实现图对比:
    图1image.png 图2**:** image.png

二. 左侧工具栏改造

2.1 增加并行和会签元素

根据源码:
github.com/bpmn-io/bpm…
定义的PaletteProvider,在项目里新建customPalette.js,基本上构造函数,注入变量都不需要修改,只需要在getPaletteEntries中新增actions就行。actions类型新增可分为以下几种情况进行处理:

bpmn已经实现的:
github.com/bpmn-io/bpm… 根据节点类型,查找适合的节点元素。如并行属于网关元素,在GATEWAY数组中找到Parallel Gateway,直接添加即可

bpmn未实现:可根据其他actions的格式,增加对应的属性, 属性说明如下:

    "global-connect-tool": {      // 元素唯一标识,在其他地方以它为准
      group: "tools",            // 分组名
      className: "bpmn-icon-connection-multi",    // 样式类名
      title: "连线",        // 元素悬浮框显示内容
      action: {            // 操作
        click: function (event) {       // 点击时调用的事件
          globalConnect.toggle(event);  
        },
        dragstart: () => {}, // 开始拖拽时调用的事件
      }
    },

bpmn已经实现,但是自己想换的:重写PaletteProvider类,覆盖原型上的getPaletteEntries方法
第一种和第三种的区别还在于在导出的时候__init__变量名定义。第一种可以自定义,第三种必须是paletteProvider。

import CustomPalette from "./customPalette";

export default {
  __init__: ["paletteProvider"],
  paletteProvider: ["type", CustomPalette],
};

注:即便你用bpmn实现了自定义效果,也得先问后端支不支持解析。每一个元素都需要有对应的解析方法。前端自定义,后端也得进行相应拓展

2.2 增加元素底部说明,新增'工具栏'头部

在global.less里修改样式,通过after增加说明,通过before增加头部。通过样式的方式,在提升美观度的同时,也增强了可读性

.djs-palette {
 ...
  .djs-palette-entries {
   ...
    &:before {
      position: absolute;
      content: '工具栏';
      text-align: center;
      font-size: 14px;
      line-height: 40px;
      left: -1px;
      width: calc(100% + 2px);
      height: 40px;
      top: -40px;
      color: #000;
      border: 1px solid #ccc;
      background: #fff;
      box-shadow: -1px 0px 8px #aaaaaa;
    }
    // 给图标加字
    .addFont {
      position: absolute;
      top: 30px;
      left: 12px;
      font-size: 12px;
    }
    .bpmn-icon-hand-tool:after {
      content: '移动';
      .addFont;
    }
    .bpmn-icon-lasso-tool:after {
      content: '选择';
      .addFont;
    }
   ...
  }
}
// 图标
.bjs-powered-by {
  bottom: 0 !important;
}

注:bpmn官网说了不能对图标进行任何改造,也不能隐藏。所以这也只是稍微往下调整了点

三. 自定义context-pad

跟左侧栏差不多,需要自定义ContextPadProvider的getContextPadEntries函数【github.com/bpmn-io/bpm… 根据选中节点的不同,动态设置显示的context-pad。主要是ContextPadProvider原型上createAction函数的改写【方法名可自定义,在调用时注意统一就行】。

function appendAction(type, className, title, options) {
    function appendStart(event, element) {
      var shape = elementFactory.createShape(assign({ type: type }, options));
      create.start(event, shape, {
        source: element
      });
    }

    var append = autoPlace
      ? function (event, element) {
        var shape = elementFactory.createShape(
          assign({ type: type }, options)
        );
        autoPlace.append(element, shape);
      }
      : appendStart;

    return {
      group: "model",
      className: className,
      title: title,
      action: {
        dragstart: appendStart,
        click: append,
      }
    };
  }
var actions = {};

  if (
    element.type === "bpmn:UserTask" ||
    element.type === "bpmn:ServiceTask" ||
    element.type === "bpmn:ScriptTask" ||
    element.type === "bpmn:StartEvent" ||
    element.type === "bpmn:ExclusiveGateway" ||
    element.type === "bpmn:ParallelGateway" ||
    element.type === "bpmn:ComplexGateway"
  ) {
    actions = {
      "append.user-task": appendAction(
        "bpmn:UserTask",
        "bpmn-icon-user-task",
        "节点"
      ),
      ...
      connect: {
        group: "edit",
        className: "bpmn-icon-connection-multi",
        title: "连线",
        action: {
          click: startConnect,
          dragstart: startConnect
        }
      }
    };
  }
  assign(actions, {
    delete: {
      group: "edit",
      className: "bpmn-icon-trash",
      title: translate("Remove"),
      action: {
        click: removeElement
      }
    }
  });

  return actions;
}

导出customModeler

import CustomPalette from "./customPalette";
import CustomContextPadProvider from "./customContextPadProvider";

export default {
  __init__: ["paletteProvider", "contextPadProvider"],
  paletteProvider: ["type", CustomPalette],
  contextPadProvider: ["type", CustomContextPadProvider]
};

四. 定义Modeler

要用改写过的ContextPadProvider,PaletteProvider类,customModeler需要继承bpmn核心类,这样就可以在具体页面进行调用

import inherits from "inherits";

import Modeler from "bpmn-js/lib/Modeler";

import CustomModule from "./custom";

function CustomModeler(options) {
  Modeler.call(this, options);

  this._customElements = [];
}

inherits(CustomModeler, Modeler);

CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [
  CustomModule
]);

export { CustomModeler };

在页面里建模调用如下:

  function createModel() {
    const viewer = new CustomModeler({
      container: '#canvas',
      propertiesPanel: {
      },
    ...
  }

五. 右侧属性栏

bpmn其实提供了一些表单组件来进行搭配操作,考虑到需求上表单项比较少【是否回退,通过条件、任务选择、名称、ID【采用默认】】,所以选用antd4的表单,通过bpmn的操作方法,在表单保存时,实时操作bpmn展示,比上一版增强了联动性,提升用户体验。实现思路:

  1. 定义变量currentElement, 存储当前操作的元素,可能会存在框选信息,selectedElements用来存储被选中区域里的元素信息
  2. 初始化右侧面板,达成选中元素变化,则右侧工具栏显示信息联动变化效果
 // 初始化右侧属性面板
 function initPropertiesPanel() {
   bpmnModeler.on('selection.changed', e => {
     // 如果是第一个节点,则oldSelection有,newSelection没有
     let selection = e.newSelection;
     if (!e.newSelection.length && e.oldSelection.length) {
       selection = e.oldSelection;
     }
     setSelectedElements(selection);
     setCurrentElement(selection[0])
    })
    bpmnModeler.on('element.changed', e => {
     const { element } = e
     if (!currentElement) {
       return
     }
     // 如果选择的节点变化,则更新现在的节点以便更新属性面板
     if (element.id === currentElement.id) {
       setCurrentElement(element)
     }
   })
 }
  1. 在表单提交回调函数种,调用updateProperties函数将右侧属性更新到xml上【可直接写入bpmn的属性:名称等属性】
  function updateProperties(type, value) {
    let properties = {}
    properties[type] = value
    const modeling = bpmnModeler.get('modeling')
    modeling.updateProperties(currentElement, properties)
    currentElement[type] = value
    setCurrentElement(currentElement)
  }
  1. 通过条件是加在非开始节点连出的线上,可以通过新增节点,实现区分显示【加了通过条件的,会在线上出现菱形】。在还不知道操作方法的时候,在最后提交时强行通过正则匹配将bpmn形成的xml文件插入判断条件,后面使用内部方法,简化了这部分操作。如下:
  // 添加新的节点:通过条件在xml上显示为新的节点
  function updateCondictionProperties(condition) {
    const modeling = bpmnModeler.get('modeling');
    const model = bpmnModeler.get('moddle');
    const newCondition = model.create('bpmn:FormalExpression', {
      body: condition
    });
    modeling.updateProperties(currentElement, {
      conditionExpression: newCondition
    })
    currentElement.conditionExpression = newCondition
    setCurrentElement(currentElement)
  }
  1. 是否回退,任务节点等自定义属性,bpmn提供了自定义属性,但因为后端解析器未支持,会直接报错。前端需要自己存储,在回显的时候,进行匹配显示。通过条件因为bpmn里存储的属性名与页面显示的属性名不对应,也需要做处理。在建模完成时,会绑定操作【bindEvent】,匹配显示逻辑写在element.click的回调里
  function bindEvent() {
    const eventBus = bpmnModeler.get('eventBus');
    const events = [
      'element.hover',
      'element.out',
      'element.click',
      'element.dblclick',
      'element.mousedown',
      'element.mouseup',
    ];
    events.forEach(event => {
      eventBus.on(event, e => {
        event === 'element.click' && add(e);
      });
    });

    function add(e) {
      const {
        element: {id, businessObject: { name, $type, sourceRef, conditionExpression }},
      } = e;
      // 找到表单中对应项
      const clickElement = flowNode.find(el => el.flowNodeKey === id);
      let otherOptions = {
        flowNodeName: name
      };
      // 设置节点条件
      if (clickElement) {
        otherOptions = clickElement;
        otherOptions.nodeTask = clickElement?.taskFlowRelationReqDtoList[0]?.taskTemplateId || '';
      }
      // 设置通过条件
      if (conditionExpression) {
        otherOptions.passCondition = conditionExpression.body;
      }
      // 记录类型
      setNodeType($type);
      // 节点的name也是节点,但是不需要显示节点设置弹窗
      setIsLabel(id.indexOf('_label') >= 0);
      // 从开始节点出来的线不需要设置通过条件
      setIsFirstLine((sourceRef?.$type || '') === "bpmn:StartEvent")
      form.setFieldsValue({
        // 判断id,除节点外的不能显示。新增会有process,编辑的时候是flowDefKey
        flowNodeKey: ((id.indexOf('Process')) >= 0) || ((id.indexOf(baseInfo.flowDefKey)) >= 0) ? '' : id,
        flowNodeName: otherOptions.flowNodeName || undefined,
        isReturnable: otherOptions.isReturnable,
        passCondition: otherOptions.passCondition || undefined,
        nodeTask: otherOptions.nodeTask || undefined
      })
    }
  }
  1. 替换processId

在上一版中,提供了输入框改写processId【个人认为交互也不是很好,proccessId为选中跳转进来的流程ID,原则上是默认且不被用户察觉的】。这一版因没有这个信息的输入项,同时也不存在子流程。所以选择在提交时,直接用正则替换掉processid,同时增加isExecutable属性

  function saveXML() {
    bpmnModeler.saveXML({format: true}, (err, xml) => {
      // 流程id和isExecutable需要直接写成processId和true
      xml = xml.replace('<bpmn:process id="Process_1" isExecutable="false">',
      `<bpmn:process id="${baseInfo.flowDefKey}" isExecutable="true">`)
      setXmlContent(xml);
     ...
    });
  }

六. 汉化

汉化主要是为了处理context-pad元素的悬浮框显示文字,增强可读性
1.定义汉化对应文件translationsGerman.js
2.汉化处理函数customTranslate

import translations from './translationsGerman';
export default function customTranslate(template, replacements) {
  replacements = replacements || {};
  template = translations[template] || template;
  return template.replace(/{([^}]+)}/g, function(_, key) {

	 var str=replacements[key];
	  if(translations[replacements[key]]!=null&&translations[replacements[key]]!='undefined'){
		  str=translations[replacements[key]];
	  }
    return  str || '{' + key + '}';
  
  });
}
  1. 作为CustomModeler函数additionalModules进行传入
  // 汉化
  const customTranslateModule = {
    translate: ['value', customTranslate]
  };
  // 建模
  function createModel() {
    const viewer = new CustomModeler({
      container: '#canvas',
      propertiesPanel: {
      },
      additionalModules: [
        customTranslateModule
      ],
      moddleExtensions: {
        camunda: camundaModdleDescriptor,
      },
    });
    setBpmnModeler(viewer);
  }

七. 总结

交互是一大重点,用户体验不好,后期必然会被替换或重构。时间有限,交互还有很多可以改进的地方。共勉~