bpmn-js 阅读指南:
- bpmn.js一个基于Bpmn 2.0的前端工作流展示和绘制工具
- 使用bpmn-js 配置颜色
- Bpmn-js 属性控制
- Bpmn-js自定义Palette
- bpmn-js 事件总线处理
- 聊一聊bpmn-js中的Viewer和Modeler
- 聊一聊bpmn-js中的Palette
- 聊一聊bpmn-js中的elementFactory模块
- bpmn-js通过moddle插件实现自定义元素和自定义属性
- 聊一聊bpmn-js中的contextpad
- bpmn-js中实现shape的内置属性、节点的默认配置
bpmn-js
中使用elementfactory
模块来构建一个元素的结构,其构建构成和元素属性的组成可参考:聊一聊bpmn-js中的elementFactory模块。构建元素的属性会自动帮我们生成一个对应类型的shape
的Id
,其余属性均为空,需要我们后续手动添加。
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
事件总线的方式进行事件的监听和传递,我们可以借助事件总线中的内置事件选择合适的时机对元素进行属性配置操作。这种方式的好处就是不需要额外去修改palette
和contextPad
插件即可实现,侵入性小,操作比较独立。
通过eventbus
实现功能前,先来了解下几个比较重要的内置事件:
element.changed
:选中元素发生改变【所有类型元素】shape.added
:shape
类型新增元素事件,当首次导入的时候,每个shape
的新增也都会触发shape.removed
:shape
类型图形移除事件import.parse.start
:xml
开始导入事件import.done
:xml
导入结束事件
如下是bpmn-js
中这个几个事件的执行顺序图示:
需要注意的几个点:
shape.added
的时机有两个地方:一个是导入加载已有数据,一个是新增shape
的时候shape
的属性写入必须要在element.changed之后操作才可生效,shape.added
监听后不可直接对element
进行操作
几个写入的方法
根据上述了解,我们需要实现如下几个功能:
- 写入
shape
属性 - 写入
shape
扩展属性 - 写入
shape
执行任务监听器:此功能为flowable
扩展功能,想要此功能实现,还需提前添加flowable
的moddle
适配声明插件,详见:bpmn-js通过moddle插件实现自定义元素和自定义属性
如下为我封装的几个写入操作的函数,代码如下:
// 创建一个元素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)
}
})
...
结果
使用palette
和contextpad
追加两种方式测试新增节点,获取xml
文件如下:
结果成功!
自定义Palette和ContextPad
若是深度定制可以通过在shape
创建的时候配置shape
属性实现,在开始添加内置属性之前,我们先来了解下shape
在bpmn-js
中直接创建的场景,以及内置属性创建的具体格式。
创建场景
就bpmn-js
提供的建模器来说内置创建元素模块主要分为两个地方:Palette
和contextPad
,其具体代码部分如下:
1、PaletteProvider.js
2、ContextPadProvider.js
需要注意的是追加操作有两次需要添加,一个是appendStart方法,一个是append方法,这两个方法均需要实现。
内置对象属性
上传我们可以发现其实这两个地方的实现是一样的,都是通过elementFactory.createShape
来创建一个shape
对象,然后通过create.start
进行创建。由于createShape
方法只是生成了一个id,所以为了创建内置属性配置,我们就需要自己新增属性,在开始实现之前,我们首先需要了解到shape
元素的描述属性都是在shape.businessObject
对象下的,如下是一个shape
元素的数据结构:
由于palette
和contextpad
的实现本质一致,我这里就在自定义的palette
(palette
自定义参考:聊一聊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的绘制次数,但代码侵入性较高,不利于不同场景的适配共用。 具体可根据需求选择使用何种方式进行实现。