掘金2023年度人气创作者打榜中,快来帮我打榜吧~ https://activity.juejin.cn/rank/2023/writer/747323639208391?utm_campaign=annual_2023&utm_medium=self_web_share&utm_source=MiyueFE

前言

首先需要明确的一点是,本文的出发点 纯粹是针对工作流开发 的场景的选型对比,其他业务场景下建议重新调研。

什么是工作流?

工作流,即 Workflow,是对工作流程及其各操作步骤之间业务规则的抽象、概括描述。工作流建模,即将工作流程中的工作如何前后组织在一起的逻辑和规则,在计算机中以恰当的模型表达并对其实施计算。工作流要解决的主要问题是:为实现某个业务目标,利用计算机在多个参与者之间按某种预定规则自动传递文档、信息或者任务。

以上是维基百科对 工作流技术 的定义。之所以称为 技术,是因为其中包含了 业务规则抽象与规则模型的创建,并可以将其用来实现一系列自动化操作。通常来说,一个工作流的定义,包含至少一个 触发点 与一个 终止点,当被触发后会由触发点沿着定义的路径和条件自动流转直到进入终止点。

所以工作流常常会与 规则引擎 结合使用。

而为了使得业务人员也能快速理解和创建一套符合业务规范的工作流,最后由 OMG 发布了 BPMN(全称 Business Process Model and Notation,业务流程模型和标记法)规范,用来标准化和约束对工作流的定义,并实现业务人员与实际程序的解耦,两者由 XML 进行信息交互。

既然有了工作流,那么就需要后端的对应的 工作流引擎 了。

什么是工作流引擎?

既然定义了一套工作流,那么就需要对应的后台服务来解析和运行这一套工作流定义。所以工作流引擎其实就是 一套用来解析工作流定义并根据根据定义驱动工作流正常执行和流转的系统

就目前来说,常用的一般有 Camunda、Activiti、Flowable、Jbpm,也有一些国内自研的工作流平台。不过目前仍然以最前面的三个使用更为广泛。

以这三个平台为例,它们在接收传入工作流定义时,都是接收 符合 BPMN2 规范的 XML 字符串,当然好像也可以接收 json 对象,但是并不是都支持 json。

那么在了解了工作流的相关内容之后,再进入正题。

bpmn.js

首先,bpmn.js 目前 没有 官方的中英文文档,TypeScript 支持也依然不良好,讨论区当前只有官方提供的一个全英文社区,如果你是一个才入前端不久又需要搞这个的话,建议你赶!!紧!!!跑!!!!!

开个玩笑,这里先介绍一下 bpmn.js 的基本情况~

在 Camunda 平台发展一段时间之后,由 camunda 团队内部以 nikko 为首组织了一个小团队 bpmn.io ,开发了 bpmn.js

在进入 bpmn.js 仓库就可以看到:bpmn-js – BPMN 2.0 for the web,也侧面反应了这个库就是 纯粹的为了在 web 页面上处理 BPMN 2.0 规范工作流的。它的介绍也是:View and edit BPMN 2.0 diagrams in the browser,即在浏览器查看和编辑 BPMN2.0 图。

bpmn.js 基于 SVG 实现网页工作流的可视化编辑,通过内部逻辑实现 SVG 与 XML 之间的相互转换,所以要使用这个库起码你的浏览器需要支持 SVG(死去的 IE 突然攻击我~)

内部,bpmn.js 依赖 bpmn-moddlemoddle 两个库实现 JS 对象与 xml 的转换,再由 bpmn-moddlediagram.js 实现 JS 对象和 SVG 之间绑定与切换。

diagram.js 提供了最基础的绘图能力与 js 操控,并提供了基础的工具能力模块。

当然,由于 bpmn.js 的团队本身就属于 camunda,所以在对 camunda 的支持上是十分优秀的。

LogicFlow

由于笔者没有深度使用过 LogicFlow,所以如果有片面之处希望大家能及时指出。

对于 LogicFlow(后面简称 LF),是由 滴滴 的团队开源的一款图形编辑工具,官方自己的定义是:一款流程图编辑框架,提供了一系列流程图交互、编辑所必需的功能和简单灵活的节点自定义、插件等拓展机制。

与 bpmn.js 一样的是,LF 一样也选择了使用 SVG 来进行页面交互的。但是与 bpmn.js 不同的是,LF 则是 专注于提供可以快捷扩展的图形绘制能力,用来取代 bpmn.js、jsplumb 扩展能力不足、自定义成本高的问题。

当然我个人觉得这里说 bpmn.js 扩展能力不足也只是因为对其核心实现不够了解

这里截取一段滴滴团队在掘金发布的文章 滴滴开源 LogicFlow:专注流程可视化的前端框架 中关于 LF 定位的说明:

  • activiti 作为工作流引擎提供了前后端的解决方案,简单二次开发就可以部署一套业务流程的管理平台
  • Bpmn.js:基于 BPMN2.0 规范,设计的流程图编辑器
  • G6:antv 旗下专注图形可视化,各类分析类图表。比如生态树、脑图、辐射图、缩进图等等
  • X6:图编辑引擎,核心能力是节点、连线和画布。不仅支持了流程图,还有 Dag 图、ER 图

LogicFlow 的定位在上图的 Bpmn.js 和 X6 之间,填补中间的空白。核心提供了流程图的编辑器,并且通过拓展能力来支持 BPMN 等规范所需的流程节点和数据格式,以满足当前业务下的现状。

可以看到 LF 在中间进行了一些平衡,在 保留可以高度扩展绘图能力等情况下,通过扩展来实现与 BPMN 的适配

本身按照 LF 的这种设计和计划来说,是能够给开发者带来很大便利的,但是目前 LF 在工作流业务场景下仍然缺乏很大的完成度

工作流业务下的选型

上面提到了,LF 这个库的结构设计是可以给开发者带来遍历的,只是目前在工作流业务中依然存在很大不足;另外 bpmn.js 的扩展性也是十分强大的,会让人觉得难用的最大原因就是缺乏相关文档——甚至是英文文档(至于全量引入那个是有误解的,已经更换为 esm 的模式了)。

言外之意就是,指定工作流场景下依然建议使用 bpmn.js;当然如果需求特别简单,也可以尝试 LF。

这里比较一下他们的一些常用功能,关于 bpmn.js 的基础原理可以查看我的这篇文章 Bpmn.js 进阶指南之原理分析与模块改造

常见扩展需求/功能

1. Render

LogicFlow

虽然 BPMN 2.0 规范中对每一种节点图标都进行了详细的标注和说明,但是那个黑白分明的突然肯定不符合现在老板们的审美,所以最常见的业务需求之一就是对显示效果进行修改,这里的显示效果自然指的就是 render 部分。

在 LF 中,它默认提供了以下 7 种基础节点:

  • RectNode:矩形
  • CircleNode:圆形
  • EllipseNode:椭圆
  • PolygonNode:多边形
  • DiamondNode:菱形
  • TextNode:文本
  • HtmlNode:HTML

每种基础节点都有对应的 Model 类型,分别管理 View 显示控制与 Model 属性控制,开发者分别可以继承同一种节点的 ViewModel 类来实现自定义节点,例如:

import { RectNode, RectNodeModel } from "@logicflow/core";class UserTaskView 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}),'']);}}class UserTaskModel extends RectNodeModel {getNodeStyle() {const style = super.getNodeStyle();style.stroke = "blue";style.strokeDasharray = "3 3";return style;}}export default {type: "UserTask",view: UserTaskView,model: UserTaskModel,};
bpmn.js

而在 bpmn.js 中,则是按照基础流程节点类型来绘制元素的,但是与 LF 不同的是,它没有节点继承关系。也就是说,一个类型的节点有且只有一个对应的渲染方法,只是 如果这个节点属于某一种基础节点的话,会在它的渲染方法内部去调用对应基础节点的绘制方法

默认的 Render 方法在 bpmn-js/lib/draw/BpmnRednerer.js 中,可以通过 this.handlers 找到。

其中 handlers 属性是一个对象,包含了每个 页面可见的 BPMN 节点对应的 方法

当然,一些公共的 SVG 绘制方法还是抽离出来了。

例如我们以 StartEvent 开始节点为例,它依赖与 Event 节点,所以:

this.handlers = {'bpmn:Event': function(parentGfx, element, attrs) {if (!('fillOpacity' in attrs)) {attrs.fillOpacity = DEFAULT_FILL_OPACITY;}return drawCircle(parentGfx, element.width, element.height, attrs);},'bpmn:StartEvent': function(parentGfx, element) {var attrs = {fill: getFillColor(element, defaultFillColor),stroke: getStrokeColor(element, defaultStrokeColor)};var semantic = getSemantic(element);if (!semantic.isInterrupting) {attrs = {strokeDasharray: '6',strokeLinecap: 'round',fill: getFillColor(element, defaultFillColor),stroke: getStrokeColor(element, defaultStrokeColor)};}var circle = renderer('bpmn:Event')(parentGfx, element, attrs);renderEventContent(element, parentGfx);return circle;}// ...}

可见,handler 方法就是对每种节点进行渲染并返回创建的 SVG 节点,所以我们在自定义的时候只需要修改 handlers 对象即可,例如新增一个 SqlTask 节点:

this.handlers['miyue:SqlTask'] = function (parentGfx, element, attr) {// 渲染外层边框const attrs = {fill: getFillColor(element, defaultFillColor),fillOpacity: defaultTaskOpacity,stroke: getStrokeColor(element, defaultTaskColor)};renderer("bpmn:Activity")(parentGfx, element, attrs);// 自定义节点const customIcon = svgCreate("image");svgAttr(customIcon, {...(attr || {}),width: element.width,height: element.height,href: mysqlIcon});svgAppend(parentGfx, customIcon);return customIcon;}

因为 SqlTask 节点是需要继承 Task 类型节点的,而 Task 最终又继承 Activity 节点,所以这里直接调用 Activity 即可。然后在 parentGfx 中插入我们的自定义内容(parentGfx 即是这个节点对应的 SVG 节点)。

当然,这是在我们需要显示原来的 Activity 类型节点的一些元素时才需要在内部再次调用 renderer('bpmn:Activity'),如果是其他一些情况下(例如使用 foreignObject 绑定div元素、直接插入图片等)也可以直接在父元素下创建一个新的 svg 节点并返回。这里其实 bpmn.js 并没有对其做限制,因为它 只负责在对应的时候调用对应的渲染方法

2. 交互效果

可能是 web 编辑器的效果都类似,在默认情况下节点对 hover 的响应一般都是显示一个边框,在选中后增加一个高亮效果,这一点通常产品都不会对其进行太大的改动,但是也不排除一些特殊情况:

  • 查看流程图时特殊显示已流转节点
  • hover 时显示办理信息
  • 显示动画边框或者连线动画

由于我对 LF 不是很熟悉,所以这几个场景我了解的也只有这样的方案:

  1. 重写 render 渲染部分
  2. 监听事件显示其他内容
  3. 通过 graphModel 的 api 来动态修改节点样式

而 bpmn.js 也可以使用类似的方式实现,一样可以重写 render 渲染方法、监听事件等、通过 API 动态修改等。

所以针对这个方面,两者之间的差距并不大,解决方案也都大同小异。

3. BPMN 2.0 规范支持

上面也提了,bpmn.js 是 camunda 团队的成员进行开发的,本身的出发点就是为了支持 BPMN 2.0 规范的文件在浏览器上的编辑和显示,所以对 BPMN 2.0 的规范提供了一套完整的处理方法。

而 LF 则是一个具有绘图能力的库,通过扩展去支持的少部分 BPMN 2.0 规范,所以从这个层面来说 LF 肯定是不如 bpmn.js 的。

虽然在可视图形的编辑和操作层面,LF 可以通过自定义来实现与 bpmn.js 一样对 BPMN 2.0 规范节点的完整支持。但是 LF 在深层的属性处理与解析上,均是通过自定义属性来处理,缺少约束;虽然这样方便了开发者最初开发,但是在生成 xml 阶段则不太友好。

bpmn.js 是通过 bpmn-moddle 与 moddle 两个库来实现 js 对象与 xml 之间的转换的,可以通过 JSON Schema 来约束整个工作流中可以使用的节点与属性,以及他们互相之间的关联关系,对不符合的属性或者条件,可以很快的找到问题来源。

一点儿总结

总的来说,大家对 bpmn.js 如此排斥的最大原因估计就是因为 没有文档,除了霖呆呆和我写过一些相关的文章,基本上剩下的都是大同小异的基础使用文章,对于需要深度定制化开发的小伙伴并没有太多帮助。

也是因为这个原因, bpmn.js 在国外也饱受诟病,甚至有团队愿意花钱请 bpmn-io 团队完善文档都不了了之。而之所以不提供文档,也是因为 bpmn-io 团队对他们编写的测试代码有足够的自信(看了一下确实很全面)。

bpmn.js 本身也提供了非常强大的自定义,通过依赖注入模式,可以很轻松的覆盖或者扩展原有内容。

而 Logicflow 虽然在开发体验上对没有接触过工作流的同学来说会很友好,但是目前对 BPMN 的支持依然还很欠缺,深度使用流程引擎依然需要进行大量的自定义工作以及 XML 转换工作,对于层级较深的一些定义还容易出现意想不到的bug。

所以在纯工作流业务方向,还是推荐使用 bpmn.js 进行前端页面的开发。