换掉bpmn-js,让前端更熟悉工作流业务

前言

看到这个标题可能有些同学会奇怪,bpmn.js不是工作流系统前端最普遍的方案吗?为什么我们需要换掉bpmn.js。这里我们站在前端的角度先明确一下工作流、工作流管理系统、工作流引擎、BPMN规范、bpmn-js的关系。

工作流

工作流的概念在百度百科和wiki上描述的都比较抽象,我们可以大致理解为做一些事情的时候,我们把做事情抽象为几个步骤,然后再合理的组织这些步骤,最终通过组织好的步骤来做这些事情。例如我们早上起来做的事情看做一项工作流,可以拆分为上厕所、洗手、刷牙、吃饭这些步骤。有的人“上厕所->洗手->吃饭->刷牙”;有的人“刷牙->上厕所->吃饭->洗手”。这里可以看到“组织步骤”的不同,最后表现的效果也不同。我们把事情的拆分、组织、执行、最后到管理都搬到计算机上面来,就可以叫做工作流

工作流管理系统

这个就是我们平时开发的系统,这些系统大多是以流程为中心,用图形化的方式来表示工作流。既然是图形化,那就需要画图,现在大多数系统都把画图搬到网页上来,所以才需要我们前端参与进来。

工作流引擎

这个一般是后端用来执行我们组织的步骤的引擎,决定着我们上厕所后是吃饭还是洗手。工作流引擎有很多,最常见的有ActivitiCamunda BPMFlowable

BPMN规范

翻译成中文是业务流程建模符号,是一个组织定的行业标准。前面提到可以通过画图的方式来描述一个事情怎么做,但是不同的人画的图不一样,不同的工作流引擎对图产出的数据也不一样。为了让图更好的被人和机器理解,就有一个组织就提出了一个标准,从图上节点、连线的具体含义到提交给工作流引擎的数据格式,都有给出了具体的定义。国内的资料大家可以看这个文档:docs.awspaas.com/reference-g… 。目前大部分开源的工作流引擎都支持BPMN规范,下面截的是wiki上的资料: bpmn.png

bpmn-js

这个很好理解,就是一个开源的实现BPMN2.0规范的web建模器。需要注意一下,bpmn-js虽然名字中有bpmn,但是并不是提出BPMN规范的官方组织开发的。所以不代表工作流引擎支持BPMN规范,前端就一定要用bpmn-js。 我们也可以基于BPMN规范重新开发一套符合自己业务的流程设计器。

为什么我选择换掉bpmn-js

从上面的介绍中,我们可以发现bpmn-js天然满足BPMN规范,而绝大多数工作流引擎也是支持BPMN规范,我们前端只需要把bpmn-js引入到项目中来,直接开箱即用。为什么还要想换掉bpmn-js呢?主要是因为bpmn-js给我们带来了下面几个痛点。

业务逻辑黑盒

在工作流相关的项目上,随着项目维护时间的增长,前端往往在这类项目中作为资源型角色,不会关心整个工作流业务逻辑。由于bpmn-js将所有的业务逻辑都在其内部实现,前端无法直接从代码层面对业务逻辑有直观的感受。这个时候如果出现一些在前端层面的修改,往往会因为缺乏一个约束,导致代码被修改的十分混乱。

假设后端需要在某个节点产生的数据上加一个字段:有的人选择在数据导出的时候直接正则暴力替换,有的人选择到bpmn-js源码中去写死,有的人强制获取所有的节点,然后给对应的节点updateProperties。随着这样的随意写代码的地方越来越多,项目的前端开发人员的更换,最终会让项目难以维护。

目前网上有各种各样对bpmn-js进行自定义的教程,也有着各种”黑科技“。归根结底是因为bpmn-js对自定义并不是完全开放,虽然有很多预设场景的示例,但是并不能满足实际项目中所有的场景

另外目前bpmn-js文档比较缺乏,大多是网友基于自己实践经验总结出来的,目前我看到的最好的总结系列是全网最详bpmn.js教材-基础篇,大家有需要可以去看看。

源码的可读性

由于bpmn-js的自定义机制不够开放,当某些产品需要做更多深度扩展的时候,其自定义效果往往不满足。例如我之前看到的要给流程图来个展开收起的功能,用bpmn-js基本不可能实现。有的项目可能会把bpmn-js的源码给引入到项目中来进行深度定制修改,到后来会发现因为源码的可读性问题,导致前端并不能理解bpmn-js的思路。为了实现需求,硬着头皮乱改最终的结果往往是前端跑路。我们部门之前有个给教育部门做审批的项目,哪个前端接手,哪个前端没多久就离职。最终出现了铁打的工作流后端,流水的前端这种让人蛋疼的现象。

职业的发展

对前端来说,花费大量的精力去学习bpmn-js源码意义不大,我们作为一个铁打的bpmn-js前端对自己的择业和成长不利。单就bpmn-js来说,它的形态、规范都是固定的,并不能发挥我们前端在UI层面的专业性。如果任何需求,产品问能不能实现,我们都说”不能,bpmn-js没这个功能”,会让我们自身的专业性被挑战,不利于我们在这个领域深入发展。

如何替换bpmn-js

技术选型

既然要替换bpmn-js,那么我们需要找一种对自定义支持更好,可维护性更高的流程图编辑工具。在网上找了一圈后选择了LogicFlow。用它的原因有这些:

  1. 自定义机制完善,它支持你在流程图上自定义任何svg能实现的效果。这个机制基本保证了我们能完全实现任何产品的需求,剩下的只是工作量的问题了。
  2. 工作量可控,官方插件中就有对bpmn自定义的实现,这个插件代码也还是比较简单,可以直接copy到项目中,让我继续自定义。
  3. 源码更清晰前端更易理解,基于mobx和preact技术栈,采用继承的自定义机制,基本上能写react就能读懂源码。

定义流程图中的基本元素

虽然在BPMN2.0中,将流程执行语义分为Events(事件)Gateways(网关)Activities(活动) 这三类要素,但是在实际项目中,我们并不需要按照BPMN规范来定义流程图的中内容,而是需要按照我们项目业务来定义流程图的内容。这里还是以我之前做的教育类审批为例:在流程图上有一个节点是审批,在bpmn规范中它实际是一个属于UserTaskActivities。我们之间定义一个ApprovalNode节点就行,然后再导出数据的时候,把数据转换为后端引擎需要的UserTask即可。这样的好处是不论产品说的“审批”还是后端说的“UserTask”在我们的代码中都有体现,我们将不再游离在业务之外了。

LogicFlow完全自定义节点主要代码如下:

import { h, RectNode, RectNodeModel } from '@logicflow/core';
class ApprovalModel extends RectNodeModel {
  initNodeData(data) {
    super.initNodeData(data);
    this.text = {
      value: data.text || "",
      x: data.x,
      y: data.y + 40,
    };
    this.width = 100;
    this.height = 80;
  }
}


class ApprovalView extends RectNode {
  /**
   * 完全自定义节点外观方法
   */
  getShape() {
    const { model, graphModel } = this.props;
    const { x, y, width, height, radius } = model;
    const style = model.getNodeStyle();
    return h("g", {}, [
      h("rect", {
        ...style,
        x: x - width / 2,
        y: y - height / 2,
        rx: radius,
        ry: radius,
        width,
        height
      })
    ]);
  }
}


export default {
  type: 'approval',
  view: ApprovalView,
  model: ApprovalModel,
复制代码

LogicFlow通过定义节点的model和view来实现节点的自定义机制。我们可以继承内置的或者其它已经定义好的节点,在它们的基础上进行扩展。这里不对LogicFlow的用法做过多的介绍,LogicFlow支持各种节点、连线、插件的自定义,基本上完全能满足我们产品的各种奇葩需求。

对于LogicFlow的自定义机制大家可以看这些资料:

logic-flow.org/guide/basic…

juejin.cn/post/693831…

bpmn-js完全自定义节点主要代码如下:

import inherits from 'inherits'
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
import { append as svgAppend, create as svgCreate } from 'tiny-svg'
import { customElements, customConfig } from '../../utils/util'
export default function CustomRenderer(eventBus, styles) {
    BaseRenderer.call(this, eventBus, 2000)
    var computeStyle = styles.computeStyle
    this.drawCustomElements = function(parentNode, element) {
        const shape = this.bpmnRenderer.drawShape(parentNode, element)
        return shape
    }
}
inherits(CustomRenderer, BaseRenderer)
CustomRenderer.$inject = ['eventBus', 'styles']
CustomRenderer.prototype.canRender = function(element) {
    return !element.labelTarget;
}
/**
 * 完全自定义节点外观方法
 */
CustomRenderer.prototype.drawShape = function(p, element) {
  const type = element.type// 获取到类型
  // 对需要自定义的类型进行定义
  if (customElements.includes(type)) {
    const { url, attr } = customConfig[type]
    const customIcon = svgCreate('image', {
      ...attr,
      href: url
    })
    element['width'] = attr.width
    element['height'] = attr.height
    svgAppend(parentNode, customIcon)
    return customIcon
  }
  const shape = this.bpmnRenderer.drawShape(parentNode, element)
  return this.drawCustomElements(p, element)
}
CustomRenderer.prototype.getShapePath = function(shape) {
    console.log(shape)
}
复制代码

看起来这里和LogicFlow一样,在drawShape可以使用svg完全自定义节点的外观,但是实际还是由很多不一样的地方:首先是drawShape时所有元素通用的,所以需要先在自定义之前识别元素类型。没有logicflow那样给每个元素分别定义一个类直观,而且LogicFlow的model-view模型也更符合前端开发的习惯;然后bpmn对文案位置、选择状态、轮廓等各种细节的定义是不支持的,我们实际开发中只能去修改源码或者用全局css强制覆盖

对于bpmn-js元素的定义,大家可以看这些资料:

github.com/bpmn-io/bpm…

juejin.cn/post/684490…

自定义业务属性

在前面我们提到过,如果后续需要我们给某类节点加个属性,我们来看看LogicFlow是怎么做的。首先是LogicFlow在数据上预留了一个properties属性,支持我们在里面设置任何自定义属性。LogicFlow在数据层面将业务数据和图数据是分开的,这比较直观。

这里假设我们有一个属性叫做rollback, 用来标识节点是否支持回滚。支持回滚节点颜色就是绿色,不支持就是蓝色。对于后端服务来说,他不需要关系颜色,只需要给他的节点信息中有rollback字段即可。

{
  id: "userTask",
  type: "rect",
  x: 100,
  y: 100,
  text: { x: 100, y: 100, value: '节点文本' },
  properties: { // 这里可以放任何业务属性
    rollback: true
  }
}
复制代码

在自定义节点的时候可以这样写:

class ApprovalModel extends RectNodeModel {
  getNodeStyle() {
    const style = super.getNodeStyle()
    const { properties } = this
    if (properties.rollback) {
      style.fill = 'green'
    } else {
      style.fill = 'blue'
    }
    return style
  }
}
复制代码

转换为流程引擎可识别的xml

从上面的代码可以看到,我们代码中更多的是对业务和UI的定义,并没有过多的去涉及BPMN规范。那么我们如何把这类业务的代码产生的数据转换为流程引擎可识别的内容呢?

最方便的方法当前是让后端自己去转换,比如后端用Java就让他写一个filter。当然,很多时候后端不愿意做,我们也可以前端来转。写转换的过程也是前端对BPMN规范了解的过程。

LogicFlow本身提供了一个插件bpmnAdapter,来实现LogicFlow数据与bpmn数据互转(json格式和xml格式都支持)。在实际项目中,我建议负责LogicFlow这个插件的源码到我们本地代码中,避免这一块逻辑成为黑盒。我们的产品可能会有更多自己的特殊逻辑,随着业务的发展,甚至产出的数据格式都不一定完全遵守BPMN规范。

总结

在工作流项目中,一开始为了快速出成果,直接用bpmn-js也没有问题。 而且很多项目可能只是给研发使用B端管理类项目,对UI也没有啥要求,甚至出现后端同学自己一套搞完,都不需要前端介入。

但是随着项目的发展,特别是项目中开始出现产品角色,打算拿出去做商业化应用的时候。这个时候如果还是保留bpmn-js作为流程设计器,会给前端研发带来巨大的压力。在必要的时候,建议前端同学激进一点,选择更易维护的流程设计器来开发。

LogicFlow确实是一个比较好的选择,文档全、源码也易理解,使用后会将你从bpmn-js的痛苦中解脱出来。当你的代码变得更易维护,也更贴近业务后,也方便其他同学快速接手项目,避免陷入谁做谁跑路的窘境。

猜你喜欢

转载自juejin.im/post/7108628315593768974