React Code Demo 和本介绍相匹配的 demo。
读者在看这篇文档的时候不要专注于阶段性的问题,有疑问往下看。
在自定义属性面板这功能的实现上主要看
前言
bpmnjs 英文官网
bpmnjs GitHub 官方案例
bpmn 行业规范 (bpmnjs 是按照该规范进行的)
第五和第六点了解一下当中的 API,不要去关心 bpmn-js-properties-panel 导出的任何东西。
所有的 API 都在官方的 demo 文档中:当前官方的包版本和以前的包版本对比有差别,所以很多导出方式可能会变化
1 2 3 4 5 6 7 8 9 "dependencies" : { "bpmn-js" : "^12.0.0" , "@bpmn-io/properties-panel" : "^1.7.0" , "bpmn-js-properties-panel" : "^1.20.3" , "camunda-bpmn-moddle" : "^7.0.1" , "zeebe-bpmn-moddle" : "^0.18.0" } ,
安装
核心包
npm i bpmn-js
官方提供的属性面板(在真实的开发中不需要用到,因为我们自己需要完全的实现整个属性面板)
npm install --save bpmn-js-properties-panel @bpmn-io/properties-panel
一、创建一个流程设计器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import BpmnModeler from "bpmn-js/lib/Modeler" ;import "bpmn-js/dist/assets/bpmn-font/css/bpmn.css" ;import "bpmn-js/dist/assets/diagram-js.css" ;useEffect (() => { console .log ("useeffect enter" ); const bpmnModeler = new BpmnModeler ({ container : canvasRef.current as HTMLDivElement , }); bpmnModelerRef.current = bpmnModeler; return () => { console .log ("useeffect out" ); bpmnModeler.clear (); bpmnModeler.destroy (); }; }, []); return <div className ={styles.canvas} ref ={canvasRef} /> ;
二、导入已有的流程
官方案例:https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n
1 2 3 4 5 6 7 8 getMockBpmnData () .then ((data ) => { return bpmnModeler.importXML(data); }) .then ((ImportXMLResult ) => { console .log (ImportXMLResult , "ImportXMLResult" ); });
Mock data 数据展示:
三、i18n 国际化
官方案例:https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n
translations.js 1 2 3 4 5 6 7 8 9 10 11 export default { "Change element" : "改变元素" , "Activate the create/remove space tool" : "启动创建/删除空间工具" , };
customTranslate.js 1 2 3 4 5 6 7 8 9 10 11 12 13 import translations from "./translations" ;export default function customTranslate (template, replacements ) { replacements = replacements || {}; template = translations[template] || template; return template.replace (/{([^}]+)}/g , function (_, key ) { return replacements[key] || "{" + key + "}" ; }); }
index.ts 1 2 3 4 5 6 7 const customTranslateModule = { translate : ["value" , customTranslate], }; const bpmnModeler = new BpmnModeler ({ container : canvasRef.current as HTMLDivElement , additionalModules : [customTranslateModule], });
四、保存数据(XML 和 SVG 数据)
关键代码在加粗段
saveData 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 const downloadFile = (url: string , fileName: string ) => { const a = document .createElement ("a" ); a.style .display = "none" ; document .body .appendChild (a); a.href = url; a.download = fileName; a.click (); window .URL .revokeObjectURL (url); document .body .removeChild (a); }; const getXML = async ( ) => { const bpmnModeler = bpmnModelerRef.current ; if (!bpmnModeler) return "" ; const { xml = "" } = await bpmnModeler.saveXML ({ format : true }); return xml; }; const getSVG = async ( ) => { const bpmnModeler = bpmnModelerRef.current ; if (!bpmnModeler) return "" ; const { svg = "" } = await bpmnModeler.saveSVG (); return svg; }; const handleSave = async ( ) => { const xml = await getXML (); message.success ("保存成功" ); }; const handleSvaeBpmnFile = async ( ) => { const data = await getXML (); var encodedData = encodeURIComponent (data); downloadFile ( "data:application/bpmn20-xml;charset=UTF-8," + encodedData, "diagram.bpmn" ); }; const handleSvaeSvgFile = async ( ) => { const svg = await getSVG (); var encodedData = encodeURIComponent (svg); downloadFile ( "data:application/bpmn20-xml;charset=UTF-8," + encodedData, "diagram.svg" ); };
五、接入属性面板-基础
官方案例:https://github.com/bpmn-io/bpmn-js-examples/tree/master/properties-panel
(实际工作中不要使用 官方提供的bpmn-js-properties-panel
包)
npm install --save bpmn-js-properties-panel @bpmn-io/properties-panel
1 2 3 4 5 6 7 8 9 .properties-panel { width : 400px ; position : absolute; right : 0 ; top : 0 ; height : 100vh ; border-left : 1px solid #ccc ; background-color : #fcfcfc ; }
1 2 <div className="{styles.canvas}" ref="{canvasRef}" /> <div className ={styles[ "properties-panel "]} ref ={propertiesPanelRef} />
1 2 3 4 5 6 7 8 9 10 11 12 13 const { BpmnPropertiesPanelModule , BpmnPropertiesProviderModule , } = require ("bpmn-js-properties-panel" ); import "bpmn-js-properties-panel/dist/assets/properties-panel.css" ;const bpmnModeler = new BpmnModeler ({ container : canvasRef.current as HTMLDivElement , additionalModules : [BpmnPropertiesPanelModule , BpmnPropertiesProviderModule ], propertiesPanel : { parent : propertiesPanelRef.current , }, });
六、自定义扩展属性面板(基于 bpmn-js-properties-panel 工作中不要使用)
就是给某一个流程节点添加表单项
官方案例:https://github.com/bpmn-io/bpmn-js-examples/tree/master/properties-panel-extension
效果:
自定义扩展属性面板包含了以下几个步骤:
创建一个名为 Magic
的组: provider/MagicPropertiesProvider.ts
provider/MagicPropertiesProvider.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import { TextFieldEntry , isTextFieldEntryEdited, } from "@bpmn-io/properties-panel" ; import { useService } from "bpmn-js-properties-panel" ;import { is } from "bpmn-js/lib/util/ModelUtil" ;const LOW_PRIORITY = 500 ;export default function PropertiesProvider (propertiesPanel, translate ) { propertiesPanel.registerProvider (LOW_PRIORITY , { getGroups : (element: React.ReactDOM ) => { return function (groups: Array <any > ) { if (is (element, "bpmn:StartEvent" )) { groups.push ({ id : "magic" , label : translate ("Magic properties" ), entries : [ { id : "spell" , element, component : Spell , isEdited : isTextFieldEntryEdited, }, ], }); } return groups; }; }, }); } function Spell ({ element, id } ) { const modeling = useService ("modeling" ); const translate = useService ("translate" ); const debounce = useService ("debounceInput" ); const getValue = ( ) => { return element.businessObject .spell || "" ; }; const setValue = (value ) => { modeling.updateProperties (element, { spell : value, }); }; return TextFieldEntry ({ id, element, debounce, description : translate ("Apply a black magic spell" ), label : translate ("Spell" ), getValue, setValue, }); }
导出程序模块: provider/index.ts
provider/index.ts 1 2 3 4 5 6 7 8 import MagicPropertiesProvider from "./MagicPropertiesProvider" ;export const magicPropertiesProviderModule = {**init**: ["magicPropertiesProvider1" ], magicPropertiesProvider1 : ["type" , MagicPropertiesProvider ],};
创建模组扩展: magic.json
它定义了一个名为"Magic"的自定义属性面板插件,其中包含一个类型扩展"BewitchedStartEvent"。该类型扩展继承自"BPMN:StartEvent",并添加了一个名为"spell"的属性,该属性是一个字符串类型,并使用"isAttr"标志将其标记为属性而不是元素。此外,该插件未指定任何关联或其他类型扩展。
magic.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "name" : "Magic" , "prefix" : "magic" , "uri" : "https://magic" , "xml" : { "tagAlias" : "lowerCase" } , "associations" : [ ] , "types" : [ { "name" : "BewitchedStartEvent" , "extends" : [ "bpmn:StartEvent" ] , "properties" : [ { "name" : "spell" , "isAttr" : true , "type" : "String" } ] } ] }
问答:
Q:“isAttr”: true,什么意思?
“isAttr”: true 表示将属性标记为 XML 属性,而不是 XML 元素。在 XML 文档中,属性是与元素相关联的名称-值对,而元素是由开始标记、内容和结束标记组成的结构。将属性标记为 XML 属性可以使它们作为元素的属性而不是子元素出现在 XML 文档中。在上面的代码中,“spell” 属性被标记为 XML 属性,这意味着它会作为 “BewitchedStartEvent” 元素的一个属性出现在 XML 文档中,而不是作为子元素嵌套在 “BewitchedStartEvent” 元素中。
Q:“xml”: {“tagAlias”: “lowerCase”}?
“xml” 对象定义了用于该扩展的 XML 映射选项。“tagAlias” 是其中的一个选项,它指定 XML 元素名称在生成 XML 时应该使用的别名。在这里,“lowerCase” 是该别名,这意味着所有生成的 XML 元素名称都将以小写字母表示。例如,在上面的代码中,“BewitchedStartEvent” 类型扩展将生成一个 “bewitchedstartevent” 的元素名称,而不是 “BewitchedStartEvent”。这个选项可以使生成的 XML 更加规范化和易于处理。
Q: “prefix”: “magic”,“uri”: “https://magic ”?
“prefix” 和 “uri” 属性用于指定 XML 命名空间的前缀和 URI。在 XML 中,命名空间用于标识 XML 元素和属性的来源和所属。在 BPMN 中,命名空间用于将自定义元素和属性与标准 BPMN 元素和属性区分开来。在这里,“magic” 是命名空间的前缀,“https://magic ” 是命名空间的 URI。在 XML 中,命名空间前缀通常与命名空间 URI 相关联,以便标识元素和属性所属的命名空间。例如,在上面的代码中,“BewitchedStartEvent” 类型扩展将被认为属于 “https://magic ” 命名空间,因为它使用了 “magic” 前缀。这样,可以确保自定义的 BPMN 元素和属性不会与标准的 BPMN 元素和属性冲突,从而实现更好的互操作性和扩展性。
注册 bpmn 的时候添加该模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import magicModdleDescriptor from "./descriptors/magic.json" ;import { magicPropertiesProviderModule } from "./provider/index" ;const bpmnModeler = new BpmnModeler ({ container : canvasRef.current as HTMLDivElement , propertiesPanel : { parent : propertiesPanelRef.current , }, additionalModules : [ customTranslateModule, BpmnPropertiesPanelModule , BpmnPropertiesProviderModule , + magicPropertiesProviderModule, ], + moddleExtensions : { + magic : magicModdleDescriptor, + }, });
上述方式是官方实现方式,并不是理想的实现方式。接入三方组件实现往下看
七、三方组件实现属性面板-原理
在写这一点的时候,本来打算是按照官方的案例依次写下来的,但是看了网上各作者的实现方式,豁然开朗。
在 1-6 的文档说明中,我们使用了部分 API,其中,包括:创建流程设计器、导入、保存、设置字段值、自定义扩展等。
由于参考了官方实现和三方开发者的开源贡献,得出以下结论:
在流程设计器中最复杂最繁琐的莫非是属性面板和操作栏的自定义,属性面板设置的值是直接挂载在该元素 DOM 对应的 XML 标签上,那就可以理解为 XML 数据和前端的 DOM 不是强关联,我们只需要告诉 bpmn 该 dom 包含哪些字段和使用 modeling.updateProperties
API 给 XML 属性赋值,以至于前端的 DOM 怎么实现就完全脱离了束缚。
在 6-2 和 6-3 中,我们导出了一个 UI 实现和 JSON,在实现的过程中我也有一个疑问:
Q: 为什么导出了官方的 UI 的实现,为什么还要导出一个 JSON?UI 组件的实现过程中已经告诉了 bpmn 字段名,为什么 JSON 还要告诉一遍?使用三方的 UI 是否还需要描述文件?
A:使用官方的 UI 实现是需要 JSON 描述文件的,但自己实现熟悉面板是不需要描述文件的,因为 UI 的实现 bpmn 是不感知的,字段与字段之间的关系是由““6-3.创建模组扩展””的 json 告诉 bpmn 的,可以选择不告诉它,只要保证不使用官方的 UI 实现。也就是说 UI 的实现是完全自由的,只需要在 UI 实现的过程中使用 updateProperties API 设置对应的键值对,而当中的键是必须要在 json 定义的。
这么讲可能有点干,画个图:
结论:属性面板的实现可以完全独立于 bpmn,其中的业务逻辑按照我们熟悉的 UI 库写。只要在关键的节点上调用 API 设置表单的值和 bpmnXML 文件的值。
八、三方组件实现属性面板-实现
具体实现见Demo ,提供基本的 API:
modeling?.updateProperties 更新属性
modeler.on(“selection.changed”,function) 元素变化时触发
实现效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // 上一张图片的生成结果 <?xml ...> <bpmn2:process id="Process_1" isExecutable="false" title="bpmn:Process" status="open" > <bpmn2:startEvent id="Event_0eceo9i" name="开始" title="bpmn:StartEvent" status="open" startElemen="我是开始节点11111" > <bpmn2:outgoing>Flow_1e1yd6i</bpmn2:outgoing> </bpmn2:startEvent> <bpmn2:sequenceFlow id="Flow_1e1yd6i" sourceRef="Event_0eceo9i" targetRef="Gateway_0gyrpxi" /> <bpmn2:parallelGateway id="Gateway_0gyrpxi" title="bpmn:ParallelGateway" status="open" > <bpmn2:incoming>Flow_1e1yd6i</bpmn2:incoming> <bpmn2:outgoing>Flow_1s4ahir</bpmn2:outgoing> <bpmn2:outgoing>Flow_1prys0p</bpmn2:outgoing> </bpmn2:parallelGateway> <bpmn2:task id="Activity_1h7dpk5" name="网关1" title="bpmn:Task" status="open" > <bpmn2:incoming>Flow_1s4ahir</bpmn2:incoming> <bpmn2:outgoing>Flow_0w5kua8</bpmn2:outgoing> </bpmn2:task> <bpmn2:sequenceFlow id="Flow_1s4ahir" sourceRef="Gateway_0gyrpxi" targetRef="Activity_1h7dpk5" /> <bpmn2:task id="Activity_03zx4qv网关1" name="网关1" title="bpmn:Task" status="open" > <bpmn2:incoming>Flow_1prys0p</bpmn2:incoming> <bpmn2:outgoing>Flow_01j42kt</bpmn2:outgoing> </bpmn2:task> <bpmn2:sequenceFlow id="Flow_1prys0p" sourceRef="Gateway_0gyrpxi" targetRef="Activity_03zx4qv网关1" /> <bpmn2:endEvent id="Event_0k0pq9f" name="结束" title="bpmn:EndEvent" status="open" > <bpmn2:incoming>Flow_0w5kua8</bpmn2:incoming> <bpmn2:incoming>Flow_01j42kt</bpmn2:incoming> </bpmn2:endEvent> <bpmn2:sequenceFlow id="Flow_0w5kua8" sourceRef="Activity_1h7dpk5" targetRef="Event_0k0pq9f" /> <bpmn2:sequenceFlow id="Flow_01j42kt" sourceRef="Activity_03zx4qv网关1" targetRef="Event_0k0pq9f" title="bpmn:SequenceFlow" status="open" /> </bpmn2:process> <bpmndi:BPMNDiagram id="BPMNDiagram_1" > ...这里是描述位置信息的 </bpmndi:BPMNDiagram> </bpmn2:definitions>
九、属性扩展描述文件规则
1.bpmn 行业规范
在 bpmn 中主要分为 4 类元素节点:事件、活动、网关和流
Events (事件)
Start Event (开始事件)
Intermediate Event (中间事件)
End Event (结束事件)
Activities (活动)
Task (任务)
Sub Process (子流程)
Call Activity (调用活动)
Gateways (网关)
Exclusive Gateway (排他网关)
Inclusive Gateway (包容网关)
Parallel Gateway (并行网关)
Complex Gateway (复杂网关)
Event-Based Gateway (基于事件的网关)
Flow
Sequence Flow (顺序流)
Message Flow (消息流)
Association (关联)
Data Association (数据通讯)
数据来源:https://cloud.trisotech.com/bpmnquickguide/bpmn-quick-guide/bpmn-basics.html
举例:bpmn:Task
是 BPMN 规范中的一个任务类型,它代表一个需要被执行的工作或活动。下面是bpmn:Task
的一些常用属性和行为(数据来源 Chat GPT 可能不准确):
属性:
id
:唯一标识符,用于在流程图中引用任务。
name
:任务的名称。
assignee
:指定任务的执行者。
candidateUsers
:可以候选的任务执行者列表。
candidateGroups
:可以候选的任务执行者所在的组。
dueDate
:任务的截止日期。
priority
:任务的优先级。
formKey
:任务的表单关键字,用于引用任务的表单定义。
行为:
complete
:完成任务,将任务标记为已完成。
delegate
:委托任务,将任务交给另一个执行者处理。
claim
:声明任务,将任务标记为已声明并指定执行者。
reassign
:重新指定任务的执行者。
escalate
:升级任务,将任务交给更高级别的执行者处理。
需要注意的是,BPMN 规范定义了一组标准的属性和行为,但每个 BPMN 实现可能会根据自己的需求进行扩展或修改。因此,实际使用中可能会存在一些差异。
2.案例
在上述内容中,是 bpmn 规范中的基础节点,以下会讲述扩展文件的配置,也就是说我们自己添加节点或者属性:
案例一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "name" : "MyBPMN" , "prefix" : "mybpmn" , "uri" : "https://MyBPMN" , "xml" : { "tagAlias" : "lowerCase" } , "associations" : [ ] , "types" : [ { "name" : "CustomServiceTask" , "extends" : [ "bpmn:ServiceTask" ] , "properties" : [ { "name" : "customProperty" , "type" : "String" } ] } ] }
prefix
xml 前缀
isAttr
代表customProperty
是以属性的形式存在
"tagAlias": "lowerCase"
代表 xml 属性和标签全部使用小写
"extends": ["bpmn:ServiceTask"]
,代表bpmn:ServiceTask
可以使用CustomServiceTask类型
(不注册也可以使用,但是存储位置不同)
当属性不具有子元素时,可以将其表示为 XML 属性,而不是 XML 元素
案例二 superClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "name" : "MyBPMN" , "prefix" : "my" , "uri" : "https://MyBPMN" , "xml" : { "tagAlias" : "lowerCase" } , "associations" : [ ] , "types" : [ { "name" : "CustomTask" , "superClass" : [ "bpmn:Task" ] , "properties" : [ { "name" : "customProperty" , "type" : "String" , "isAttr" : true } ] } ] }
superClass
:CustomTask 继承了Task
的所有属性
案例三 列表属性扩展
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "name" : "MyBPMN" , "prefix" : "my" , "uri" : "https://MyBPMN" , "xml" : { "tagAlias" : "lowerCase" } , "associations" : [ ] , "types" : [ { "name" : "Extensions" , "superClass" : [ "Element" ] , "properties" : [ { "name" : "extensions" , "isMany" : true , "type" : "Extension" } ] } , { "name" : "Extension" , "properties" : [ { "name" : "key" , "isAttr" : true , "type" : "String" , "default" : "默认值" } ] } ] }
该描述文件定义了一个Extension
的类型:{key:string}
。
又定义了一个Extensions
类型:{"extensions":[{key:string}]}
isMany
是一个数组
default
:默认值
3.重点
在 bpmnjs 中如果不使用官方提供的 UI 组件,可以不用注册描述文件,也就是说不用提前在 json 文件中声明类型和字段
需要注意的是如果不在文件中注册字段,数据保存在Element?.businessObject.$attrs
,注册了的或者原生节点内置的字段存在于Element?.businessObject
4.总结和最优解
在实际的开发中,如果不是新的 UI 节点,只是简单的属性那么就不要在描述文件中注册。
只保证在描述文件中注册新的 UI 节点,并且也不要注册属性。
初始值的设置和数据同步
也就是说在定义节点属性的时候你不用照着“2.案例
”写
在节点变化的时候,将节点的值设置进表单。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 useEffect (() => { if (!currentElement) return ; formRef.current ?.resetFields (); const data = currentElement?.businessObject .$attrs ; formRef.current ?.setFieldsValue ({ id : currentElement?.id , ...data, }); synchronousXMLData (); }, [currentElement]); const synchronousXMLData = ( ) => { const formData = formRef.current ?.getFieldsValue (); modeling?.updateProperties (currentElement, formData); }; onFieldsChange={() => { synchronousXMLData (); }}
属性面板示例,完整的 react 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import { BetaSchemaForm , ProFormColumnsType , ProFormInstance , } from "@ant-design/pro-components" ; import { useContext, useEffect, useRef } from "react" ;import { GlobalContext } from ".." ;export default () => { const { bpmnInstance } = useContext (GlobalContext ); const { currentElement, modeling, modeler } = bpmnInstance || {}; const { type : currentElementType } = currentElement || {}; const formRef = useRef<ProFormInstance >(); useEffect (() => { if (!currentElement) return ; formRef.current ?.resetFields (); console .log (currentElement?.businessObject ); const data = currentElement?.businessObject .$attrs ; formRef.current ?.setFieldsValue ({ id : currentElement?.id , ...data, }); synchronousXMLData (); }, [currentElement]); const columns : ProFormColumnsType <BpmnAPI .record >[] = [ { title : "ID" , dataIndex : "id" , formItemProps : { rules : [ { required : true , message : "此项为必填项" , }, ], }, }, { title : "标题" , dataIndex : "title" , initialValue : currentElementType, formItemProps : { rules : [ { required : true , message : "此项为必填项" , }, ], }, }, { title : "状态" , initialValue : "open" , dataIndex : "status" , valueEnum : { open : { text : "开启" }, close : { text : "关闭" }, }, }, { title : "开始节点" , initialValue : "我是开始节点" , dataIndex : "startElemen" , hideInForm : currentElementType !== "bpmn:StartEvent" , }, ]; const synchronousXMLData = ( ) => { const formData = formRef.current ?.getFieldsValue (); modeling?.updateProperties (currentElement, formData); }; return ( <BetaSchemaForm <BpmnAPI .record > formRef={formRef} onFieldsChange={() => { synchronousXMLData (); }} columns={columns} /> ); };