胖蔡说技术
随便扯扯

bpmn-js中实现shape的内置属性、节点的默认配置

bpmn-js 阅读指南:

bpmn-js中使用elementfactory模块来构建一个元素的结构,其构建构成和元素属性的组成可参考:聊一聊bpmn-js中的elementFactory模块。构建元素的属性会自动帮我们生成一个对应类型的shapeId,其余属性均为空,需要我们后续手动添加。

ElementFactory.prototype.create = function(type, attrs) {
  attrs = assign({}, attrs || {});
  if (!attrs.id) {
    attrs.id = type + '_' + (this._uid++); // 自动生成id
  }
  return create(type, attrs);
};

为了方便用户操作和隐藏某些固定配置信息,现在我希望在用户创建的时候就将某些配置信息固定配置进入对应的shape,以节省流程编辑器制作时间,也防止某些敏感配置出现不可预知的错误配置。通过对bpmn-js的结构的了解和分析,针对不同使用常见我总结了两种方式对齐进行配置的注入操作。

期望达到的效果

对于配置注入的最终结果有如下描述:

  • 单个组件创建会自动配置一个默认名称
  • bpmn:UserTask配置默认的任务监听器
  • 配置内置扩展属性

通过EventBus实现

bpmn-js使用内置的eventbus事件总线的方式进行事件的监听和传递,我们可以借助事件总线中的内置事件选择合适的时机对元素进行属性配置操作。这种方式的好处就是不需要额外去修改palettecontextPad插件即可实现,侵入性小,操作比较独立。

通过eventbus实现功能前,先来了解下几个比较重要的内置事件:

  • element.changed:选中元素发生改变【所有类型元素】
  • shape.addedshape类型新增元素事件,当首次导入的时候,每个shape的新增也都会触发
  • shape.removedshape类型图形移除事件
  • import.parse.startxml开始导入事件
  • import.donexml导入结束事件

如下是bpmn-js中这个几个事件的执行顺序图示:

需要注意的几个点:

  • shape.added的时机有两个地方:一个是导入加载已有数据,一个是新增shape的时候
  • shape的属性写入必须要在element.changed之后操作才可生效,shape.added监听后不可直接对element进行操作

几个写入的方法

根据上述了解,我们需要实现如下几个功能:

如下为我封装的几个写入操作的函数,代码如下:


// 创建一个元素id
export function uuid(
  length = 8,
  chars: any = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
) {
  let result = ''
  const charsString = chars
  for (let i = length; i > 0; --i)
    result += charsString[Math.floor(Math.random() * charsString.length)]
  return result
}
/**
 * 更新bpmn-js中元素的属性更新操作
 * @param modeler bpmn-js中的操作对象modeler
 * @param element 当前待操作的元素
 * @param key 需要更新的key
 * @param value 更新后的value
 */
export const updateProperty = (modeler: any, element: any, key: string, value: any) => {
  const modeling = modeler.get('modeling') // 依据didi设计的插件获取方式

  const attrObj = Object.create(null)
  attrObj[key] = value

  if (element && element[key] === attrObj[key]) {
    console.log('属性值未发生改变,请忽略:', element[key])
    return
  }
  if (modeling && element) {
    if (key === 'id') {
      // 更新属性
      modeling.updateProperties(element, {
        id: value,
        di: { id: `${value}_di` },
      })
    }
    else {
      modeling.updateProperties(element, attrObj)
    }
  }
}

/**
 * 添加扩展属性
 * @param modeler bpmn-js中的操作对象modeler
 * @param element 当前待操作的元素
 * @param key 需要更新的key
 * @param value 更新后的value
 */
export const addExtensionProperty = (modeler: any, element: any, key: string, value: any) => {
  const modeling = modeler.get('modeling') // 依据didi设计的插件获取方式
  const elementRegistry = modeler.get('elementRegistry')
  const moddle = modeler.get('moddle')
  const targetElement = elementRegistry.get(element.id)
  if (!targetElement)
    return
  // bpmn:properties
  const otherPropertis: any = []
  const properties = targetElement.businessObject?.extensionElements?.values.filter((ex: any) => {
    const type = ex.$type.split(':')[1] || ''
    if (type !== 'Properties')
      otherPropertis.push(ex)
    return type === 'Properties'
  }) ?? []
  const values: any[] = properties.reduce((last: any[], current: any) => last.concat(current.values), [])

  const current = values.find((item: any) => item.name === key) // 存在当前key

  if (current) {
    // 当前key已存在,需要进行更新
    modeling.updateModdleProperties(targetElement, current, { name: key, value })
  }
  else {
    // 当前key不存在,需要创建一个
    const newPropertyObject = moddle.create('flowable:Property', { name: key, value })
    const propertiesObject = moddle.create(('flowable:Properties'), {
      values: values.concat([newPropertyObject]),
    })

    const extensionElements = moddle.create('bpmn:ExtensionElements', {
      values: otherPropertis.concat([propertiesObject]),
    })
    modeling.updateProperties(targetElement, {
      extensionElements,
    })
  }
}

const createScriptObject = (moddle: any, options: any) => {
  const { scriptType, scriptFormat, value, resource } = options
  const scriptConfig: any = { scriptFormat }
  if (scriptType === 'inlineScript')
    scriptConfig.value = value
  else scriptConfig.resource = resource

  return moddle.create('flowable:Script', scriptConfig)
}

/**
 * 添加任务监听器
 * @param modeler bpmn-js中的操作对象modeler
 * @param element 当前待操作的元素
 * @param model
 */
export const addTaskListenerProperty = (modeler: any, element: any, model: any) => {
  const modeling = modeler.get('modeling') // 依据didi设计的插件获取方式
  const elementRegistry = modeler.get('elementRegistry')
  const moddle = modeler.get('moddle')
  const targetElement = elementRegistry.get(element.id)
  if (!targetElement)
    return
  const otherExtensionList: any[] = []
  const properties = targetElement.businessObject?.extensionElements?.values?.filter(
    (ex: any) => {
      if (ex.$type !== 'flowable:TaskListener')
        otherExtensionList.push(ex)
      return ex.$type === 'flowable:TaskListener'
    },
  ) ?? []

  const listenerObj = Object.create(null)
  listenerObj.event = model.event
  switch (model.listenerType) {
    case 'scriptListener':
      listenerObj.script = createScriptObject(moddle, model)
      break

    case 'expressionListener':
      listenerObj.expression = model.expression
      break

    case 'delegateExpressionListener':
      listenerObj.delegateExpression = model.delegateExpression
      break

    default:
      listenerObj.class = model.class
  }

  if (model.event === 'timeout' && !!model.eventDefinitionType) {
    // 超时定时器
    const timeDefinition = moddle.create('bpmn:FormalExpression', {
      body: model.eventTimeDefinitions,
    })
    const TimerEventDefinition = moddle.create('bpmn:TimerEventDefinition', {
      id: `TimerEventDefinition_${uuid(8)}`,
      [`time${model.eventDefinitionType.replace(/^\S/, (s: string) => s.toUpperCase())}`]:
        timeDefinition,
    })
    listenerObj.eventDefinitions = [TimerEventDefinition]
  }

  const listenerObject = moddle.create('flowable:TaskListener', listenerObj)

  properties.push(listenerObject)

  const extensionElements = moddle.create('bpmn:ExtensionElements', {
    values: otherExtensionList.concat(properties),
  })
  modeling.updateProperties(targetElement, {
    extensionElements,
  })
}

实现

东风具备,接下来就是如何实现扩展属性等的创建插入了,原理就是参考上述的执行事件顺序,通过记录状态在shape.added中添加element.changed方法【原理是元素创建后会自定聚焦当前元素,会主动发起一次element.changed事件】,去除监听【可参考bpmn-js 事件总线处理了解更多事件总线的操作】,以防止重复占有元素聚焦导致逻辑死循环。伪代码实现如下:

const importDone = ref<boolean>(false) // 导入状态

....
// 确认导入是否完成
modeler.on('import.parse.start', () => {
   importDone.value = false
})

modeler.on('import.done', (e: any) => {
   importDone.value = true
})

modeler.on('shape.added', (event: any) => {
 if (importDone.value) {
        // 编辑过程中新增元素
    const listener = ({ element }: any) => {
     if (element.id === event?.element.id) {
          modeler.get('eventBus').off('element.changed', listener)
          updateProperty(modeler, element, 'name', '测试节点名称')
          addExtensionProperty(modeler, element, 'test', 'property') // 添加扩展属性
          addTaskListenerProperty(modeler, element, {
            event: 'create',
            listenerType: 'classListener',
            class: 'cn.enjoytoday.bpmnClassLisener',
          }) // 添加默认执行任务
     }
   }

    modeler.get('eventBus').on('element.changed', listener)
 }
})
...

结果

使用palettecontextpad追加两种方式测试新增节点,获取xml文件如下:

结果成功!

自定义Palette和ContextPad

若是深度定制可以通过在shape创建的时候配置shape属性实现,在开始添加内置属性之前,我们先来了解下shapebpmn-js中直接创建的场景,以及内置属性创建的具体格式。

创建场景

bpmn-js提供的建模器来说内置创建元素模块主要分为两个地方:PalettecontextPad,其具体代码部分如下:

1、PaletteProvider.js

2、ContextPadProvider.js

需要注意的是追加操作有两次需要添加,一个是appendStart方法,一个是append方法,这两个方法均需要实现。

内置对象属性

上传我们可以发现其实这两个地方的实现是一样的,都是通过elementFactory.createShape来创建一个shape对象,然后通过create.start进行创建。由于createShape方法只是生成了一个id,所以为了创建内置属性配置,我们就需要自己新增属性,在开始实现之前,我们首先需要了解到shape元素的描述属性都是在shape.businessObject对象下的,如下是一个shape元素的数据结构:

由于palettecontextpad的实现本质一致,我这里就在自定义的palettepalette自定义参考:聊一聊bpmn-js中的Palette)中实现内置属性挂载。其实现代码:

// PaletteProvider.js 由于创建ModdleElement对象需要用到moddle模块,需要在inject中添加moddle
    function createListener(event) {
      const shape = elementFactory.createShape(assign({ type }, options))

      if (options) {
        !shape.businessObject.di && (shape.businessObject.di = {})
        shape.businessObject.di.isExpanded = options.isExpanded
      }

       // 这里开始是插入内置属性代码
      shape.businessObject.name = '测试节点名称'

      const testProp = moddle.create('flowable:Property', { name: 'test', value: '123' })
      const task = moddle.create('flowable:TaskListener', { event: 'creat', class: 'cn.enjoytoday.bpmnClassLisener' })
      const a = moddle.create('flowable:Properties', {
        values: [testProp],
      })
      const extensionElements = moddle.create('bpmn:ExtensionElements', {
        values: [a, task],
      })

      shape.businessObject.extensionElements = extensionElements
     //将属性插入到extensionElements中


      create.start(event, shape)
    }

结果

测试发现,可正常实现内置属性插入,得出xml文件如下:

总结

两种方式均可实现内置属性节点的配置插入,第一种方式通过适配式的方式实现,尽可能少的影响建模其的独立性和完整性,后一种方式比较直接,一次性完成创建配置,减少shape的绘制次数,但代码侵入性较高,不利于不同场景的适配共用。 具体可根据需求选择使用何种方式进行实现。

赞(1) 打赏
转载请附上原文出处链接:胖蔡说技术 » bpmn-js中实现shape的内置属性、节点的默认配置
分享到: 更多 (0)

请小编喝杯咖啡~

支付宝扫一扫打赏

微信扫一扫打赏