前言
bpmn.js是什么
bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成.简单来说,就是能让前端来画流程图,且可以在工作流引擎flowable跑起来。
本文档主要记录如何使用以及如何设计出好看的流程图
先简单画个原版的流程图,你也可以亲自试试画一个:在线绘制bpmn流程图

那么问题来了,原版的感觉并没有那么好看,作为合格的前端切图仔,你能忍受自己辛苦画出的流程图那么单调吗,所以坚决不能委屈自己的作品!
接下来废话不多说,先简单上个图看我这边实现的效果

这不是设计稿!
这不是设计稿!!
这不是设计稿!!!
重要的事情先说他三遍!!!
这样UI效果的流程图可还满意?
接下来先聊聊背景,由于公司业务需要,需要基于工作流引擎flowable设计一套可灵活配置的营销任务系统,对不同人群通过配置的工作流进行精准营销推送,其中不乏普通推送、条件判断、分流、定时等节点类型
接下来开始正文咯 😄
节点类型
- StartEvent - 开始节点
 - EndEvent - 结束节点
 - ServiceTask - 服务节点
 - ExclusiveGateway - 网关节点
 - IntermediateCatchEvent - 定时节点
 - UserTask - 用户节点
 - BoundaryEvent - 边界节点
 
几个常用的节点大致就是上面的几个了,如果特殊场景需要其他节点,原则上用法都大差不差,举一反三即可,每个节点的具体用法,留在下次文章 🐶🐶🐶🐶
首先要做的就是初始化实例了
            
            
              js
              
              
            
          
          import BpmnModeler from 'bpmn-js/lib/Modeler'
import customModule from './custom'
import flowableModdleDescriptors from './moddle-extensions/flowable.json'
const bpmnModeler = new BpmnModeler({
    container: canvas,
    additionalModules: [
      customModule,
      {
        paletteProvider: ['value', ''], // 禁用/清空左侧工具栏
        move: ['value', ''], // 禁用单个图形拖动
        bendpoints: ['value', {}], // 禁用连线拖动
        labelEditingProvider: ['value', ''], // 禁用节点编辑
        contextPadProvider: ['value', ''], // 禁用图形菜单
      }
    ],
    bpmnRenderer: {
      defaultStrokeColor: '#acaec0', // 线条 文字颜色
      defaultFillColor: '#FFF' // 图形填充颜色
    },
    moddleExtensions: {
      flowable: flowableModdleDescriptors
    }
})
        如上初始化代码,customModule就是我们自定义样式的部分了
            
            
              js
              
              
            
          
          // CustomRenderer.js
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
export default class CustomRenderer extends BaseRenderer {
    constructor(eventBus, bpmnRenderer, modeling) {
        super(eventBus, HIGH_PRIORITY)
        this.bpmnRenderer = bpmnRenderer
        this.modeling = modeling
    }
    drawShape(parentNode, element) {
        const attrProperty = getExtensionsValue(element)
        switch (attrProperty.type) {
          case NODE_TYPE.start:
            return drawStartNode(parentNode, element)
          case NODE_TYPE.end:
            return drawEndNode(parentNode, element)
          case NODE_TYPE.push:
          case NODE_TYPE.disturbPush:
            return drawPushNode(parentNode, element)
          case NODE_TYPE.process:
            return drawProcessNode(parentNode, element)
          case NODE_TYPE.trigger:
            return drawTriggerNode(parentNode, element)
          case NODE_TYPE.abTest:
            return drawAbTestNode(parentNode, element)
          case NODE_TYPE.triggerMulti:
            return drawTriggerMultiNode(parentNode, element)
          case NODE_TYPE.timingPush:
            return drawTimingPushNode(parentNode, element)
          default:
            break
        }
    }
}
CustomRenderer.$inject = ['eventBus', 'bpmnRenderer', 'modeling']
    
// custom/index.js
import CustomRenderer from './CustomRenderer'
export default {
  __init__: ['customRenderer'],
  customRenderer: ['type', CustomRenderer]
}
        其中核心就是继承 BaseRenderer 然后重写drawShape方法了,当然还需要依赖tiny-svg进行绘制svg内容,以开始节点为例(即上面的drawStartNode方法)
            
            
              js
              
              
            
          
              import {
      append as svgAppend,
      create as svgCreate
    } from 'tiny-svg'
    
    const drawStartNode = (parentNode) => {
      // 绘制圆形
      const customEllipse = svgCreate('ellipse', {
        rx: 18,
        ry: 18,
        cx: 18,
        cy: 18,
        fill: '#36B37E'
      })
      svgAppend(parentNode, customEllipse)
      // 插入文案
      const nameText = svgCreate('text', {
        x: 6,
        y: 22,
        'font-size': '12',
        fill: '#fff'
      })
      nameText.innerHTML = '开始'
      svgAppend(parentNode, nameText)
      return customEllipse
    }
        如上就可以得到一个自定义的开始节点了

当然,你也可以根据创建foreignObject,然后插入自定义的html元素,设计更复杂的你喜欢的样式,来个最简单的例子:
            
            
              js
              
              
            
          
          const foreignObject = svgCreate('foreignObject', {
    x: 20,
    y: 35,
    width: 170,
    height: 65
  })
  const foreignObjectDiv = const foreignObjectDiv = `<div style="color: red; font-weight: bold">
        哈哈哈哈哈哈
      </div>`  
  foreignObject.innerHTML = foreignObjectDiv
  svgAppend(parentNode, foreignObject)
        svgCreate 还有更多支持的api,可自行探索 tiny-svg,到这里我们自定义样式基本就搞定了
接下来就是绘制并连线各个节点了
- 维护一个数组,存储我创建的所有节点及每个节点配置的数据
 - 更新某个节点的时候,需要清除画布内容,重新遍历,重新计算各个节点的位置并连线
 
还记得初始化时候的bpmnModeler吗?创建节点和连线都需要依赖他了
            
            
              js
              
              
            
          
          createStartShape() {
  const taskBusinessObject = this.bpmnBase.bpmnFactory?.create('bpmn:StartEvent')
  const startEvent = this.bpmnBase.elementFactory.createShape({
    type: 'bpmn:StartEvent',
    businessObject: taskBusinessObject,
    width: 36,
    height: 36
  })
  this.setFlowableProperties(startEvent, {
    type: NODE_TYPE.start
  })
  return startEvent
}
const element = createStartShape()
// position 节点位置,根据当前节点在维护节点数组中的位置而定,如果是在多分支节点的子节点,情况较为复杂,不仅需要根据上下位置,还需要根据左右位置来定最终位置
// process 引入的基础xml容器
// 创建成功后,返回一个完整的节点实例
const createdElement = bpmnModeler.get('modeling').createShape(element, position, process)
// lastElement 上一个节点
// waypoints 连线的起始坐标,根据实际情况计算,如果是直线,则包含2个坐标,如果有拐点,则需要加上拐点坐标,如包含一个拐点: 
const demoWaypoints = [
 { x: 100, y: 100 },
 { x: 150, y: 100 },
 { x: 150, y: 200 },
]
const sequenceFlowElement = bpmnModeler.get('modeling').connect(lastElement, createdElement, null, { waypoints } )
// sequenceFlowElement 连线成功后返回连线实例,可在线上添加元素(overlays),如 + icon
const position = { left: -8, top: 35 }
const html = `<div id="${element.id}_add" class="bpmn_add_icon">+</div>`
this.bpmnBase.overlays?.add(element, element.id, {
    html,
    position
})
        到这里就完成了支持创建,自定义绘制,连线,渲染画布且支持增加、删除等更新节点重绘画布的最终效果了,实现方案还有优化的空间,继续努力~~~