手把手教你玩转 BpmnJS 元素属性更新~

简介: 手把手教你玩转 BpmnJS 元素属性更新~

前言


最近我加入的几个 BpmnJS 的讨论群里面,很多同学都在研究 自定义显示自定义属性面板 两个方面的东西,关于自定义显示相关的内容之前我在 Bpmnjs进阶指南 中也有大致讲过,当然具体的代码编写也得结合实际的UI界面。但是 自定义属性面板 这块,不管采用哪种方式,最终都会使用一样的更新手段去更新元素,所以这里结合 camunda 流程引擎单独讲一讲更新。


1 更新操作的API


关于2k Star项目BpmnProcessDesigner的开发过程与扩展说明 一文的 第四小节 中我讲了官方提供的元素属性更新方式 有两种:


  • updateProperties


  • updateModdleProperties


当时没有放出两个 API 的 types,这里补一下:


declare module 'bpmn-js/lib/features/modeling/Modeling' 
  type Properties = Record<string, string | number | boolean | ModdleElement | null | undefined>
  export default class Modeling extends BaseModeling {
    constructor(
      eventBus: EventBus,
      elementFactory: ElementFactory,
      commandStack: CommandStack,
      bpmnRules: Rules
    )
    updateModdleProperties(
      element: Base,
      moddleElement: ModdleElement,
      properties: Properties
    ): void
    updateProperties(element: Base, properties: Properties): void
  }
}


解释一下上面的几个类型声明:


  1. Base:画布 可见元素 的根类型,包括 Shape、Connection、Root 等


  1. ModdleElement: 可以说是所有 BpmnJS 元素、属性实例 的根类型,后面我放个截图大家应该就知道了


  1. BaseModeling:diagram.js 提供的基础模块


这两个 API 有联系也有区别,但是除了某些特定属性之外都建议使用 updateModdleProperies 来更新元素属性


1.1 updateProperties 说明与执行过程


该方法依赖UpdatePropertiesHandler 模块执行,可以看做是继承的commandHandler(之所以是看做,是因为 CommandHandler 是一个 抽象类,而 updatePropertiesHandler 实现了 CommandHanlder 类定义的所有重要方法,但是没有明显的 extends 关系


该模块依赖 elementRegistry, moddle, translate, modeling, textRenderer 几大模块。


在属性更新的执行(execute)过程中,会校验 element 参数是否传递,并且获取到element 原来的 “所有” “与传入的新 properties 对象 key 一致的” Properties 属性组成的对象


然后,会进行以下 updateModdleProperties 无法实现的处理


  1. 如果新的 properties 对象中有 id 属性且与原 id 不一致,会更新 id


  1. 如果新的 properties 对象中有 default 字段,则会更新相关的连线(也就是“流”)的显示样式和对应属性


  1. 如有新的 properties 对象中有 DI 字段,也会更新元素的图形元素相关属性


  1. 最后更新元素的 businessObject 上与 新 properties 对象中的同名属性


需要注意的是,这两个 API 的更新方式都类似 Object.assign(oldProperties, newProperties) ,即 只要第一层属性名相同,就会以新的属性配置覆盖原始的属性配置,所以像更新某个数组属性的某一项时,记得保留其他的属性元素。


1.2 updateModdleProperties 说明与执行过程


该方法依赖UpdateModdlePropertiesHandler 模块执行,该模块与UpdatePropertiesHandler 一样也可以看做是commandHandler 的实现类


该模块只依赖 elementRegistry 模块。


当然这个方法是 专门用来更新元素中指定的某项业务属性,所以执行过程十分简单:


  • 首先获取 元素的数据对象绑定,当然这个内容暂时我还没有碰到有同学使用哈


  • 然后是获取 指定的 ModdleElement 中与传入的 Properties 对象 key 一致的属性组成的对象(updateProperties 方法也有这一步,主要是为了放在 EventBus 的上下文中给同名订阅事件使用)


  • 然后就是更新指定 ModdleElement 中的 properties 指定属性,这里的属性更新十分粗暴,可以给大家看看源码。


function setModdleProperties(moddleElement, properties) {
  forEach(properties, function(value, key) {
    moddleElement.set(key, value);
  });
}


2. 判断属性更新方式


通过上面的两个方法的更新过程来看,除了以下情况外都可以使用 updateModdleProperties:


  1. 更新元素 Id 和 name


  1. 更新默认默认流转路径(也就是 shape 的 default 属性)


  1. 更新元素 DI


那么更新元素属性的 API 确定了,怎么确定如何去更新呢?就像更新更新元素的监听器、扩展属性的时候怎么判断,这里我分成这些情况来说明吧:


  • 使用 camunda、flowable、activiti 这些流程引擎且没有做其他自定义属性的时候,可以通过后端或者官方的设计器设计好相应的 XML 之后导入到我们的项目中打印相关元素、查看属性结构


  • 只有对应的 JSON Schema 文件,需要自己解析的时候


  • 需要根据已有的 xml 和后端流程引擎的需求来定义我们的 JSON Schema 文件的时候


3. 通过导入XML打印节点来分析属性关系


大部分的需求其实都是实现自定义的属性面板和自定义显示,对官方的流程元素属性没有改动,此时就建议使用这种方式,也算是最简单的一种方式。


假设我们以一个 具有带注入字段的执行监听器的用户任务节点 为例。


上面的这个需求使用官方的 camunda 设计器来编辑的话,右侧编辑后的属性面板内容如下:


网络异常,图片无法展示
|


此时的流程图 XML 如下,其中蓝色部分就是我们的任务节点:


网络异常,图片无法展示
|


那么打印一下该节点的businessObject 对象如下:


网络异常,图片无法展示
|


大家此时也可以发现,控制台中打印的对象如果是通过构造函数生成的,后面会跟着类型名称,所以这里属于 ModdleElement 类的就有以下部分:


  1. 本身的打印对象businessObject


  1. 扩展属性extensionElements


  1. 扩展属性内部的每一个values 数组元素


  1. 我们配置的EventType 是 start 的执行监听器实例


  1. 执行监听器中的每一个注入字段 field


  1. 执行监听器的使用 脚本配置 script


拆解一下整体的层级关系:


const businessObject = {
  $type: "bpmn:UserTask",
  extensionElements: {
    $type: "bpmn:ExtensionElements",
    values: [
      {},
      {
        $type: "camunda:ExecutionListener",
        event: "start",
        fields: [{ $type: "camunda:Field", name: "field1", value: "value1" }],
        script: {
          $type: "camunda:Script",
          resource: "123123312",
          scriptFormat: "123123"
        }
      }
    ]
  }
};


可以发现,每一个 ModdleElement 实例都有一个 $type 属性,来标识该实例的类型;并且都有get 和 set 两个方法,用来获取和更新该实例的属性


4 从 0 开始创建执行监听器


上面已经分析出来了配置好一个执行监听器之后的元素的业务对象属性格式,那么现在开始从 0 配置吧。


这里的创建步骤就是根据分析出来的整体的层级关系来确定的


第一步:判断最外层 extensionElemenets


因为监听器是作为 extensionElements 中的一个实例的,所以在更新之前监听器之前要 先保证 extensionElements 存在。如果没有的话,则需要我们创建一个 extensionElements 实例并更新到 businessObject 中。


let extensionElements = businessObject.get("extensionElements");
if (!extensionElements) {
  extensionElements = createModdleElement("bpmn:ExtensionElements", { values: [] }, businessObject);
  modeling.updateModdleProperties(element, businessObject, { extensionElements });
}


这里的 createModdleElement 是用来创建一个 ModdleElement 实例的。


export function createModdleElement(elementType, properties, parent) {
  const element = moddle.create(elementType, properties);
  parent && (element.$parent = parent);
  return element;
}


因为要保证所有 ModdleElement 实例的正确级联关系,所以需要配置每个实例的parent属性,手动指定生成实例的父级


这里的 values 默认配置了一个空数组,也是为了后面用到了扩展运算符来复制数组元素,如果这里是 undefined 的话,后面的代码可能不能正确执行。


第二步:创建一个监听器实例


创建监听器的过程就很简单了,只要调用上面定义的createModdleElement方法创建就行了,注意此时该方法接收的 parent 参数是上面获取的 extensionElements


但是需要注意创建时的 properties,因为 script 是一个 ModdleElement 实例,这里最好不要添加该参数;或者先创建一个 script 的实例,然后再创建监听器实例。


当然,建议是先创建 script 实例


const script = createModdleElement("camunda:Script", { scriptFormat: 'xxx', resource: 'sss' })
const listener = createModdleElement("camunda:ExecutionListener", { event: 'start', script, fileds: [] })


第三步:创建注入字段并更新监听器


这一步其实与上面的 Script 实例的创建和更新到 listener 上的步骤可以互换,也可以先创建 field 再创建 listener。


const field1 = createModdleElement("camunda:Field", { name: 'field1', value: 'value1' }, listener)
const field2 = createModdleElement("camunda:Field", { name: 'field2', value: 'value2' }, listener)


第四步:更新到元素上


因为第一步已经处理了元素的 extensionElements,此时元素的业务属性 businessObject 中肯定是有一个 extensionElements 对象的,所以可以直接通updateModdleProperties更新该对象。


modeling.updateModdleProperties(element, extensionElements, {
  values: [...extensionElements.get("values"), listener]
});
// 此时需要重新更新监听器的 field 字段
modeling.updateModdleProperties(element, listener, {
  fields: [...listener.get("fields"), field1, field2]
});


但是如果我们是在创建了 listener 之后再创建的 field 字段的话,建议调整一下顺序,在创建了监听器之后就更新元素 extensionElements;或者将 field 创建提到与 script 的创建阶段一致,最后再创建 listener 和更新 extensionElements


const script = createModdleElement("camunda:Script", { scriptFormat: 'xxx', resource: 'sss' })
const field1 = createModdleElement("camunda:Field", { name: 'field1', value: 'value1' }, listener)
const field2 = createModdleElement("camunda:Field", { name: 'field2', value: 'value2' }, listener)
const listener = createModdleElement("camunda:ExecutionListener", { event: 'start', script, fileds: [field1, field2] })
modeling.updateModdleProperties(element, extensionElements, {
  values: [...extensionElements.get("values"), listener]
});


5 编辑某个监听器


这部分其实算是最简单的一部分了吧。


首先我们只需要拿到 当前编辑的这个监听器 listener,然后调用updateModdleProperties 去更新相关的实例属性即可


当然需要注意的,切换监听器类型的时候需要对内部的原有字段 fields、script 之类的数据进行清除,可以将原来的字段值设置 undefined 即可(因为看起来这些是有一定冲突的)。


6 移除某个监听器


在移除某个监听器的时候,其实是 需要更新整个 extensionElements 属性的


这里可以借用数组的 filter 筛选方法来处理


const extensionElements = businessObject.get('extensionElements')
const values = extensionElements.get('values').filter(value => value === listener)
modeling.updateModdleProperties(element, extensionElements, { values });


7 简单总结一下


通过官方的编辑器配置一个需要的 xml 在导入本地打印节点来分析属性关系,算是个人认为最快的一种方式了,虽然也可以通过阅读官方侧边栏源码的方式来学习,但是那样确实很慢,而且对从未接触过 Bpmn.js 的同学也不友好。


分析元素打印出来的属性结构,只需要注意以下几点即可:


  1. 控制台显示的ModdleElement类型的实例都需要通过moddle.create的方式创建,其中实例的**$type** 属性就是我们创建实例时需要指定的类型


  1. ModdleElement 数组形式的属性,在后面更新某一个的时候需要注意 保留原有的其他数组元素


最后


文章到这里为止,基本上已经把这种最快解决元素属性更新的问题说清楚了,但是这里还是留下了一个问题:多次调用 updateModdleProperties 将插入多个操作记录,影响正常的撤销恢复操作。当然这个问题官方的属性面板其实已经给出了答案,希望大家可以多看看源码来解决这个问题。


另外文中没有对参数进行校验,大家在实际项目中需要注意最好给表单增加一个必填校验,在创建 ModdleElement 实例的时候也要根据参数情况判断是否需要创建新的实例。


因为时间有限,后面的文章我也会再详细的说一下如何从 JSON Schema 或者 xml 中分析属性的层级关系。


目录
相关文章
|
2天前
|
XML JSON 移动开发
BpmnJS 元素属性的updateProperties 和updateModdleProperties的属性更新区别
BpmnJS 元素属性的updateProperties 和updateModdleProperties的属性更新区别
16 1
|
2天前
|
C#
C#学习相关系列之自定义遍历器
C#学习相关系列之自定义遍历器
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序4
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序4
32 0
|
2天前
|
JavaScript
【sgDrag】自定义组件:基于Vue开发支持批量声明拖拽元素、被碰撞元素,拖拽全过程监听元素碰撞检测并返回拖拽原始元素、克隆元素及其getBoundingClientRect对象和碰撞接触元素数组。
【sgDrag】自定义组件:基于Vue开发支持批量声明拖拽元素、被碰撞元素,拖拽全过程监听元素碰撞检测并返回拖拽原始元素、克隆元素及其getBoundingClientRect对象和碰撞接触元素数组。
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序3
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序3
35 0
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序1
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序1
31 0
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序2
前端学习笔记202305学习笔记第二十八天-数组结构之列表拖拽改变顺序2
36 0
|
前端开发
前端学习案例7-数组的填充项1
前端学习案例7-数组的填充项1
56 0
前端学习案例7-数组的填充项1
|
前端开发
前端项目实战243-利用splice改变原数组
前端项目实战243-利用splice改变原数组
60 0
|
前端开发
前端学习案例11-数组遍历方法3-修改this指向
前端学习案例11-数组遍历方法3-修改this指向
59 0
前端学习案例11-数组遍历方法3-修改this指向