从0到1实现BPMN边界事件节点

什么是边界事件

在BPMN规范中,事件可以分为有3种,分别为只能发到流程开头的开始事件、只能放到结尾的结束事件和可以放到中间的中间事件。在中间事件中,有一种可以附加到任务或者子流程边缘的的事件,叫做边界事件。虽然边界事件还可以细分为中断和非中断事件,但是对于我们前端来说,其交互体验都一致,所以只需要实现一种就行。这篇文章记录如何使用LogicFlow从0到1实现一个边界事件,希望能帮到有类似需求的同学。至于为什么要自己实现而不是使用bpmn-js, 请参考我之前的文章换掉bpmn-js,让前端更熟悉工作流业务。

下图为bpmn-js中边界事件的效果图和我实现的demo示例效果图:

从0到1实现BPMN边界事件节点_第1张图片

bpmn-js中的效果

从0到1实现BPMN边界事件节点_第2张图片

最终实现的效果

实现边界事件的思路

边界事件的功能分析

参考bpmn-js中边界事件的交互,结合我们项目中实际的需求,这里先分析边界事件需要的一些通用的交互功能点。

交互1: 目标节点的高亮提示

当拖动边界事件到可以附加的节点上时,这个节点的变高亮,给出智能提示。

交互2: 边界事件跟随目标节点

边界事件节点附加到目标节点上后,拖动目标节点边界事件可以跟随目标节点一起移动。

交互3: 边界事件脱离目标节点

在bpmn-js交互中,如果一个已经附件到目标节点上的边界事件节点是"原始的"(未设置边界事件的具体功能), 则可以再拖动边界事件节点离开附加节点,然后作为流程中独立的中间事件或者附加到其他节点上。

交互4:保留其他节点交互

边界事件节点仍然具有节点菜单,锚点,选中状态等功能。

交互5:边界事件节点永远在附加节点上方

由于我们项目是使用LogicFlow开发,下面的实现代码也是使用LogicFlow提供的API来实现。如果大家使用的其他框架,可以参考这个思路来自己实现。通过边界事件的功能分析和LogicFlow本身提供的API,我们可以大致把这些交互分配到不同的模块实现。我们可以大致把边界事件这个功能分为3个模块:

  1. 边界事件节点:实现交互4、交互5
  2. 目标节点:实现交互1
  3. LogicFlow插件: 实现交互2、交互3

以上思路参考了LogicFlow Group插件源码,整个边界事件封装为一个插件整个代码结构确实更加清晰了。

模块1:定义边界事件节点

交互4中提到,边界事件节点仍然具有节点的通用功能,所有这里我们新建一个文件boundary-events-node.js,通过自定义节点来实现边界事件节点。

import { CircleNode, CircleNodeModel, h } from "@logicflow/core"


class BoundaryEventModel extends CircleNodeModel {
  getNodeStyle () {
    const style = super.getNodeStyle()
    style.strokeWidth = 1
    style.stroke = '#EFEA9A'
    return style
  }
  getOutlineStyle() {
    const style = super.getOutlineStyle()
    style.stroke = 'transparent'
    style.hover.stroke = 'transparent'
    return style
  }
  initNodeData (data) {
    super.initNodeData(data)
    this.r = 20
    this.zIndex = 9999 // 保证边界事件节点用于在最上方
  }
}


class BoundaryEvent extends CircleNode {
  // 自定义边界事件的外观,后面细分的边界事件节点可以重写该方法来实现自定义图标
  getIcon () {
    const { model } = this.props;
    const style = model.getNodeStyle();
    const { x, y, width, height } = model;
    return h(
      'svg',
      {
        x: x - width / 2 + 2,
        y: y - height / 2 + 2,
      },
      h('path', {
        stroke: style.stroke,
        fill: style.fill,
        d: 'M 10.5,8.5 l 14.5,0 l 0,18 l -14.5,0 Z M 12.5,11.5 l 10.5,0 M 12.5,14.5 l 10.5,0 M 12.5,17.5 l 10.5,0 M 12.5,20.5 l 10.5,0 M 12.5,23.5 l 10.5,0 M 12.5,26.5 l 10.5,0',
      }),
    )
  }
  getShape() {
    const { model } = this.props;
    const style = model.getNodeStyle();
    const { x, y, r } = model;
    const outCircle = super.getShape();
    return h(
      'g',
      {},
      outCircle,
      h('circle', {
        ...style,
        cx: x,
        cy: y,
        r: r - 5,
      }),
      this.getIcon()
    );
  }
  // 重写toFront为空来防止边界事件层级因为交互而改变
  // 改变节点在失去选中状态后被zIndex重置为0的问题
  toFront () {}
}


export default {
  type: "boundary-event",
  view: BoundaryEvent,
  model: BoundaryEventModel,
}

从上面的代码中可以看到,利用LogicFlow的继承重写思路,重写toFront来强制改变节点失去选中状态后被重置为0的问题,来实现交互5

自定义节点主要是定义边界事件节点的外观,这里就不详细解释了。值得注意的一点是,边界事件节点本身会有很多种类型,不同的类型图标是不一样的,所以这里我们把生成图标的功能单独提出来做成一个getIcon函数,后面实际应用时只需要重此方法即可。

模块2: 定义可被附加的目标节点

为了让使用体验更好,我们需要对目标节点进行一定的改造,使其在边界节点被拖拽靠近的时候,节点自身做出高亮的动作来提示用户。同样利用LogicFlow的自定义节点,给节点增加增加方法setIsCloseToBoundary,这个方法传入参数true就表现为高亮,false就表现为默认外观。

import { RectNode, RectNodeModel, h } from '@logicflow/core'


class TaskModel extends RectNodeModel {
  initNodeData (data) {
    super.initNodeData(data)
    this.width = 100
    this.height = 60
    this.isTaskNode = true // 标识此节点是任务节点,可以被附件边界事件
    this.boundaryEvents = [] // 记录自己附加的边界事件
  }
  getNodeStyle() {
    const style = super.getNodeStyle()
    // isCloseToBoundary属性用于标识拖动边界节点是否靠近此节点
    // 如果靠近,则高亮提示
    const { isCloseToBoundary } = this.properties
    if (isCloseToBoundary) {
      style.stroke = '#FF0000'
      style.strokeWidth = 2
    } else {
      style.stroke = '#EFEA9A'
      style.strokeWidth = 1
    }
    return style
  }
  getOutlineStyle() {
    const style = super.getOutlineStyle()
    style.stroke = 'transparent'
    style.hover.stroke = 'transparent'
    return style
  }
  /**
   * 提供方法给插件在判断此节点被拖动边界事件节点靠近时调用,从而触发高亮
   */
  setIsCloseToBoundary (flag) {
    this.setProperty('isCloseToBoundary', flag)
  }
  /**
   * 附加后记录被附加的边界事件节点Id
   */
  addBoundaryEvent (nodeId) {
    if (this.boundaryEvents.find(item => item === nodeId)) {
      return false
    }
    this.boundaryEvents.push(nodeId)
    return true
  }
  /**
   * 被附加的边界事件节点被删除时,移除记录
   */
  deleteBoundaryEvent (nodeId) {
    this.boundaryEvents = this.boundaryEvents.filter(item => item !== nodeId)
  }
}


class Task extends RectNode {
  getShape() {
    const { model } = this.props;
    const style = model.getNodeStyle();
    const { x, y, width, height } = model;
    const outCircle = super.getShape();
    return h(
      'g',
      {},
      outCircle,
      h('rect', {
        ...style,
        x: x - width / 2,
        y: y - height / 2,
        width,
        height,
      }),
    );
  }
}


export default {
  type: 'task-node',
  view: Task,
  model: TaskModel,
}

模块3:LogicFlow边界事件插件

有了边界事件节点和目标节点后,下面我们用一个插件来组合边界事件节点和目标节点,实现剩余的交互效果。

  1. 首先是目标节点移动的同时移动边界事件节点, 利用LogicFlow全局的addNodeMoveRules api, 判断移动的是taskNode,就同时移动taskNode上面的boundaryEvents node。
  2. 监听节点的拖动事件,判断节点当前坐标是否处于其它目标节点边所在的位置。如果在某个节点的边所在位置,就触发这个节点的高亮方法。
import BoundaryEvent from './boundary-events-node'
import TaskNode from './task-node';


class BoundaryEventPlugin {
  constructor({ lf }) {
    this.lf = lf;
    this.nodeBoundaryMap = new Map()
    lf.register(BoundaryEvent);
    lf.register(TaskNode);
    lf.on('node:drag,node:dnd-drag', this.checkAppendBoundaryEvent);
    lf.on('node:drop,node:dnd-add', this.appendBoundaryEvent);
    lf.graphModel.addNodeMoveRules((model, deltaX, deltaY) => {
      if (model.isTaskNode) { // 如果移动的是分组,那么分组的子节点也跟着移动。
        const nodeIds = model.boundaryEvents;
        lf.graphModel.moveNodes(nodeIds, deltaX, deltaY, true);
        return true;
      }
      return true
    });
  }
  private appendBoundaryEvent = ({ data }) => {
    const preBoundaryNodeId = this.nodeBoundaryMap.get(data.id);
    const closeNodeId = this.checkAppendBoundaryEvent({ data })
    if (closeNodeId) {
      const nodeModel = this.lf.graphModel.getNodeModelById(closeNodeId)
      nodeModel.setIsCloseToBoundary(false)
      nodeModel.addBoundaryEvent(data.id)
      this.nodeBoundaryMap.set(data.id, closeNodeId)
    }
    if (preBoundaryNodeId !== closeNodeId) {
      const preNodeModel = this.lf.graphModel.getNodeModelById(preBoundaryNodeId)
      if (preNodeModel) {
        preNodeModel.deleteBoundaryEvent(data.id)
      }
    }
  }
  // 判断此节点是否在某个节点的边界上
  // 如果在,且这个节点model存在属性isTaskNode,则调用这个方法
  private checkAppendBoundaryEvent = ({ data }) => {
    const { x, y, id } = data;
    const { nodes } = this.lf.graphModel;
    let closeNodeId = '';
    for (let i = 0; i < nodes.length; i++) {
      const nodeModel = nodes[i];
      if (nodeModel.isTaskNode && nodeModel.id !== id) {
        if (this.isCloseNodeEdge(nodeModel, x, y) && !closeNodeId) { // 同时只允许在一个节点的边界上
          nodeModel.setIsCloseToBoundary(true);
          closeNodeId = nodeModel.id;
        } else {
          nodeModel.setIsCloseToBoundary(false);
        }
      }
    }
    return closeNodeId;
  }
  private isCloseNodeEdge (nodeModel, x, y) {
    if (Math.abs(Math.abs(nodeModel.x - x) - nodeModel.width / 2) < 10 &&
    y >= nodeModel.y - nodeModel.height / 2 - 10 &&
    y <= nodeModel.y + nodeModel.height / 2 + 10) {
      return true
    }
    if (Math.abs(Math.abs(nodeModel.y - y) - nodeModel.height / 2) < 10 &&
    x >= nodeModel.x - nodeModel.width / 2 - 10 &&
    x <= nodeModel.x + nodeModel.width / 2 + 10) {
      return true
    }
    return false
  }
}
export {
  BoundaryEventPlugin,
};

效果演示

github地址:https://github.com/hsole/BoundaryEvents

在线演示地址:自定义边界事件

从0到1实现BPMN边界事件节点_第3张图片

最后

说一下我对LogicFlow的看法。在我们最开始调研的时候,选型用LogicFlow的原因主要是因为其源码“简单”,相比于其他流程图编辑框架,遇到问题我们改源码成本会低很多。后来在实际项目发现,LogicFlow的插件机制和基于继承重写的自定义方式具有非常高的可扩展性,基本上svg能实现的LogicFlow都能实现。不需要去改源码来实现我们项目各种特殊的需求,最多是不用LogicFlow内置的插件,而是参考其插件重新实现一个自己的插件罢了。目前我们用了LogicFlow基本上不会出现产品有需求我们做不了的情况了。

虽然流程图是一个相对小众的方向,LogicFlow在社区也没啥人知道,用不到的人基本上都不会去看。但是LogicFlow本身对svg的应用挺好,在加上其源码是用preact和mobx写的,基本上懂react的人就很容易看懂,大家有空可以去尝试一下,就当学SVG了。

LogicFlow github地址:https://github.com/didi/LogicFlow

你可能感兴趣的:(前端,javascript,工作流)