2022年12月26日
By: Chase

bpmn学习笔记

学习笔记1

项目初始化, 对官方提供的一些demo进行学习, 整理, 微调

前期准备

  1. 本blog代码地址: https://github.com/chase-si/learn-bpmn

  2. 该装的npm包都装上, 核心的包就是bpmn-js, 如果需要编辑流程参数等需求的话, 酌情根据官方demo引入别的包 官方demo: https://github.com/bpmn-io/bpmn-js-examples

  3. 该引入的对应css都引入 我是在全局总css内, 用css的引用方法引入的, 参考如下

@import url("../node_modules/bpmn-js/dist/assets/diagram-js.css");
@import url("../node_modules/bpmn-js/dist/assets/bpmn-js.css");
@import url("../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css");
@import url("../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css");
@import url("../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css");
/* 属性模板css */
@import url("../node_modules/bpmn-js-properties-panel/dist/assets/properties-panel.css");

基本显示

file: src/demos/basicBpmn

核心代码:

import BpmnJs from 'bpmn-js';
import { diagramXML } from './constants'; // xml的字符串

const bpmnViewer = new BpmnJs({
    container: ${绑定的dom} // 比如 "#container" 或者 react/vue里ref绑定的dom指向
})

bpmnViewer.importXML(diagramXML);

添加import监听:

bpmnViewer.current.on('import.done', event => {
    const { error, warnings } = event
    if (error) {
        return console.log('加载失败:', error)
    }
    return console.log('加载成功:', warnings);
})

带左边栏和可编辑

file: src/demos/modeler

核心代码:

import BpmnModeler from 'bpmn-js/lib/Modeler';
import { diagramXML } from './constants'; // xml的字符串

const = new BpmnModeler({
    container: ${绑定的dom} // 比如 "#container" 或者 react/vue里ref绑定的dom指向
})

bpmnViewer.importXML(diagramXML);

基本同基本显示demo, 只不过BpmnJS换成了BpmnModeler

打印输出编辑后的xml的function:

// 把function绑定到click事件上触发即可, 注意`saveXML`是个promise异步
const prnNewXml = async () => {
    const res = await bpmnViewer.saveXML()
    console.log('new xml', res)
}

下载为svg, 和打印输出xml差不多

const prnNewSVG = async () => {
    const res = await bpmnModeler.current.saveSVG()
    const { svg } = res

    const aLink = document.createElement('a')
    aLink.href = `data:application/bpmn20-xml;charset=UTF-8,${encodeURIComponent(svg)}`
    aLink.download = 'test.svg'
    aLink.click()
}

带参数编辑的组件

file:src/demos/modelerPanel

核心代码, 加载之后每个小模块点中后可编辑其默认的属性, 有name,id, document之类的.

import BpmnModeler from 'bpmn-js/lib/Modeler';
import {
    BpmnPropertiesPanelModule,
    BpmnPropertiesProviderModule,
    ZeebePropertiesProviderModule
} from 'bpmn-js-properties-panel';
import ZeebeBpmnModdle from 'zeebe-bpmn-moddle/resources/zeebe.json'

import { diagramXML } from './constants'; // xml的字符串

const bpmnViewer = new BpmnModeler({
    container: ${绑定的xml渲染显示的dom},
    propertiesPanel: {
        parent: `${绑定的编辑面板显示的dom}`, // 这个dom的布局位置自己记得写css控制一下, 此处略
    },
    additionalModules: [
        BpmnPropertiesPanelModule,
        BpmnPropertiesProviderModule
    ]
})
bpmnViewer.importXML(diagramXML);

带拓展:

const bpmnViewer = new BpmnModeler({
    container: ${绑定的xml渲染显示的dom},
    propertiesPanel: {
        parent: `${绑定的编辑面板显示的dom}`, // 这个dom的布局位置自己记得写css控制一下, 此处略
    },
    additionalModules: [
        BpmnPropertiesPanelModule,
        BpmnPropertiesProviderModule, // 只引入到此就可带基本的编辑功能, 默认都带id和document属性
        ZeebePropertiesProviderModule // 此扩展增加了output和extension Properties
    ],
    moddleExtensions: {
        zeebe: ZeebeBpmnModdle
    }
})

学习笔记2

改造, 覆写官方提供的demos甚至库代码, 实现可定制化的流程组件编辑 实现:

  1. 去掉logo
  2. 左侧边栏palette自定义初始化组件 / 增加自定义组件
  3. 组件弹窗contextPad自定义可连接初始化组件 / 增加自定义组件
  4. contextPad的添加方向改为向下

file: src/demos/modelerCustomOverWrite

官方自带logo, 查看dom发现挂在一个a元素上, 我们在import xml完成后, 直接找到那个元素remove即可

bpmnModeler.on('import.done', (event) => {
    if (event?.error) {
        return console.error(event.error)
    }
    // 移除logo元素
    const bpmnLogo = document.querySelector('a.bjs-powered-by')
    if (bpmnLogo) {
        bpmnLogo.remove()
    }
})

自定义左边栏流程组件

官方提供了demo讲解如何自定义化组件: https://github.com/bpmn-io/bpmn-js-example-custom-controls 引入实现了一下: file: src/demos/modelerCustom

同时有一个bpmn中文学习文档里讲这个也比较透彻: bpmn LinDaiDai中文学习文档

但是, 这两个讲的都是新增自定义化组件, 但是我连初始化自带的组件都想删除, 研究了一番决定覆写他左侧边栏的加载代码.

覆盖原来的Modeler代码(how)

怎么做先写前面, 为什么这么做见下一个小标题

  1. /node_modules/bpmn-js/lib/features/palette文件夹复制出来, 自定义个文件夹名字, 比如newPalette, 复制出的过程中, 记得palette文件内的相对路径引用要改成绝对路径的
  2. 覆盖替换原库中代码模块
import newPalette from './newPalette'

const newModelingModules = [...BpmnModeler.prototype._modelingModules]
newModelingModules.splice(19, 1, newPalette) // 替换原来的palette

const bpmnModeler = new BpmnModeler({
    container: canvasDom.current,
    modules: [
        ...Viewer.prototype._modules,
        ...BpmnModeler.prototype._interactionModules,
        ...newModelingModules
    ]
})
  1. 对newPalette里的一些代码做操作, 会发现左侧边栏会有变化, 在此我们可以多尝试调试帮助理解代码, 具体做哪些操作请看以下

阅读我们的Modeler代码(why)

点击我们的BpmnModeler直接跳进node_modules/bpmn-js/lib/Modeler.js, 发现其有三部分组成

Modeler.prototype._modules = [].concat(
	Viewer.prototype._modules,
	Modeler.prototype._interactionModules,
	Modeler.prototype._modelingModules
);

而其中的第三部分_modelingModules中, paletteModule就是负责左边边栏渲染的

Modeler.prototype._modelingModules = [
	// modeling components
	AlignElementsModule,
	AutoPlaceModule,
	AutoScrollModule,
	AutoResizeModule,
	BendpointsModule,
	ConnectModule,
	ConnectionPreviewModule,
	ContextPadModule,
	CopyPasteModule,
	CreateModule,
	DistributeElementsModule,
	EditorActionsModule,
	GridSnappingModule,
	InteractionEventsModule,
	KeyboardModule,
	KeyboardMoveSelectionModule,
	LabelEditingModule,
	ModelingModule,
	MoveModule,
	PaletteModule, // <============================ 这里
	ReplacePreviewModule,
	ResizeModule,
	SnappingModule,
	SearchModule
];

paletteModule进到node_modules/bpmn-js/lib/features/palette/PaletteProvider.js, 看到有如下代码:

assign(actions, {
    'hand-tool': {
      group: 'tools',
      className: 'bpmn-icon-hand-tool',
      title: translate('Activate the hand tool'),
      action: {
        click: function(event) {
          handTool.activateHand(event);
        }
      }
    },
    'lasso-tool': {
      group: 'tools',
      className: 'bpmn-icon-lasso-tool',
      title: translate('Activate the lasso tool'),
      action: {
        click: function(event) {
          lassoTool.activateSelection(event);
        }
      }
    },
    ...
    'create.start-event': createAction(
      'bpmn:StartEvent', 'event', 'bpmn-icon-start-event-none',
      translate('Create StartEvent')
    ),
    ...
}

拿第一个工具图标来看

'hand-tool': {
    group: 'tools',
    className: 'bpmn-icon-hand-tool',
    title: translate('Activate the hand tool'), // 这里是悬浮在icon上显示的文本, 需要汉化的直接改中文的就可
    action: {
    click: function(event) {
        handTool.activateHand(event);
      }
    }
},

hand-tool显然就是左侧边栏的第一个小手图标, 对其各个属性进行猜测与测试验证:

className 代表图标的icon, bpmn是用style实现icon样式, 同时我发现node_modules/diagram-js/lib/features/palette/Palette.js中, 他有的对imageUrl属性的支持的, 所以如果传入图片路径给imageUrl, 可以实现icon替换

title就是hover时显示的文本, 需要汉化的直接这里改了拉倒

action代表触发事件, 此处未对他原来支持的action深究, 现阶段直接套用即可

拿后面的任务图标来看

'create.task': createAction(
  'bpmn:Task', 'activity', 'bpmn-icon-task',
  translate('Create Task')
),

这里就是我们大展手脚添加自定义组件的地方了, 只要对keycreate.task动动手脚, 我可以创出多个类似task图标的流程组件, 比如添加

'create.task2': createAction(
  'bpmn:Task', 'activity', 'bpmn-icon-task',
  translate('Create Task 2号')
),

会发现左边栏多了一个长得一样又不完全一致的task icon

目前代码中对组件type的定义比较全的可看/bpmn-js/lib/features/replace/ReplaceOptions.js

至于删除初始组件, 直接把初始列表里不要的注释掉就行.

写到这里, 接下来就可以把自定义组件的参数JSON化, 作为入参在初始化Modeler加载, 过程不表了. 最终代码可看file: src/demos/modelerCustomOverWrite

自定义menu菜单(点中xml渲染图中组件后, 显示的可接着添加的组件列表)

核心思想和替换PaletteModule类似

Modeler.prototype._modelingModules = [
	// modeling components
	AlignElementsModule,
	AutoPlaceModule,
	AutoScrollModule,
	AutoResizeModule,
	BendpointsModule,
	ConnectModule,
	ConnectionPreviewModule,
	ContextPadModule,  // <============================ 管事的在这里
	CopyPasteModule,
	CreateModule,
	DistributeElementsModule,
	EditorActionsModule,
	GridSnappingModule,
	InteractionEventsModule,
	KeyboardModule,
	KeyboardMoveSelectionModule,
	LabelEditingModule,
	ModelingModule,
	MoveModule,
	PaletteModule,
	ReplacePreviewModule,
	ResizeModule,
	SnappingModule,
	SearchModule
];

node_modules/diagram-js/lib/features/centext-pad复制代码出来替换, 过程略了

默认的添加方向是在右侧, 源代码还考虑到了添加多个组件做布局适配, 不建议修改. 如果要修改, 相关代码在: /node_modules/bpmn-js/lib/features/auto-place/BpmnAutoPlaceUtil.js

+  // var position = {
+  //   x: sourceTrbl.right + horizontalDistance + element.width / 2,
+  //   y: sourceMid.y + getVerticalDistance(orientation, minDistance)
+  // };
+  // chase change: add to bottom position
   var position = {
-    x: sourceTrbl.right + horizontalDistance + element.width / 2,
-    y: sourceMid.y + getVerticalDistance(orientation, minDistance)
+    x: sourceMid.x + getVerticalDistance(orientation, minDistance),
+    y: sourceTrbl.bottom + horizontalDistance + element.height / 2,
   };

这个文件的在原来的代码库里相关引用比较多, 没法像上面的PaletteModuleContextPadModule两个模块一样拖出来重写, 所以用了patch-package直接覆盖修改了node_modules包里的几行代码.

patch-package使用简介

  1. 安装
yarn add patch-package
  1. 修改bpmn-js为锁定版本, 重新安装一个固定版本
yarn remove bpmn-js
yarn add bpmn-js@10.3.0
  1. . 到node_modules里修改想要改的代码
  2. 改完之后yarn patch-package ${对应修改的代码在的包}, 此处如果是修改自动布局位置, 应该跑
yarn patch-package bpmn-js
  1. 跑完之后发现会生成一个/patches文件夹, 里面记录了你修改的代码, 这样别人在安装你的项目时, 会自动读这个修改, 同步覆盖到他的node_modules

学习笔记3

自定义组件属性栏的编辑

官方demo

带编辑栏的官方demo已经在学习笔记(1)里讲过了, 下面实现一下自己封装的编辑栏组件

整体设计

  • 编辑流程属性设计, 斜波代表一个自定义流程组件
  +---------------+     load(1)                     +------------------------------+                        +---------------------+
  |               |          |                      |                              |                        |                     |
  |   left-bar    |          |                      |       xml-render             |                        |     property-edit   |
  |               |          |                      |                              |                        |                     |
  |               |          |                      |                              |                        |                     |
  |               |          |                      |                              |                        |                     |
  |               |          |                      |                              |                        |                     |
  |  +---------+  |          |                      |                              |                        |                     |
  |  |  xiebo  |  |<---------+                      |       +----------+           |                        |                     |
  |  |         |  |        click to add shape(2)    |       |     xiebo|           |  click to edit props(3)|                     |
  |  |         +--+------------------------------>  |       |          |-----------+--------------------->  |                     |
  |  |         |  |                                 |       |          |           |                        |                     |
  |  +---+-----+  |                                 |       |          |           |                        |                     |
  |               |                                 |       +----------+           |                        |                     |
  |               |                                 |             ↑                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  |               |                                 |             |                |                        |                     |
  +------+--------+                                 +-------------+----------------+                        +-----------+---------+
                                                                  ↑                                                    |
                                                                  |                                                    |
                                                                  |save to xml(5)                                      |
                                                                  |                                                    |
                                                                  |                                                    |
                                                                  |                                                    |
                                                                  |               save(4)                              |
                                                                 -+----------------------------------------------------+
  1. 加载数据
  • 接受新palett格式的数据, 约定RcProperties内为可编辑参数 [{key: "label名称斜波", value: '斜波描述内容'}]
  • 调用init内的函数load到left-bar
  1. 点击加载斜波task
  • 通过监听shape.added的方式, 监听加载斜波
  • 监听事件内通过model.updatePropertites将RcProperties更新到xml内
  1. click聚焦xml-render面板内的斜波Task
  • 如果有如果有RcProperties, 就在property-edit内渲染显示可编辑的参数
  • 设置当前聚焦的task.element为当前编辑element(改变焦点或聚焦其他element时触发(4)update使用)
  1. 当前聚焦的斜波Task blur判定
  • 判断新click的element的ID === 当前element.id
  1. 编辑完的数据save
  • 更新xml内的(3)聚焦的task.element的xml(再次通过model.updatePropertites)

代码示例:file: src/demos/modelerPanelOverWrite

学习笔记4

自定义添加组合组件: 添加类似一个自定义条件组件, 自动连着俩subProcess框

学会调用自身扩展包

首先还是这个modeler

const bpmnModeler = new BpmnModeler({
    container: `#${domID}`
    modules: 
})

通过bpmnModeler.injector._providers能获取所有能用的扩展包, 非常多, 当前版本(10.3.0)是157个, 其中有2个应该是我自己注入的.

所有这里能看到的扩展包都能用bpmnModeler.get(${扩展包名})获取调用

目前我了解的就几个, 其他无说明的, 等后面接触到了慢慢补充吧.

扩展名说明
"adaptiveLabelPositioningBehavior",
"alignElements",
"alignElementsContextPadProvider",
"alignElementsMenuProvider",
"appendBehavior",
"associationBehavior",
"attachEventBehavior",
"attachSupport",
"autoPlace",contextPad的自动添加有用到, 下文会提及
"autoPlaceSelectionBehavior",
"autoScroll",
"bendpointMove",
"bendpointMovePreview",
"bendpointSnapping",
"bendpoints",
"boundaryEventBehavior",
"bpmnAlignElements",
"bpmnAutoPlace",
"bpmnAutoResize",
"bpmnAutoResizeProvider",
"bpmnCopyPaste",
"bpmnDiOrdering",
"bpmnDistributeElements",
"bpmnFactory",
"bpmnGridSnapping",
"bpmnImporter",
"bpmnInteractionEvents",
"bpmnOrderingProvider",
"bpmnRenderer",
"bpmnReplace",
"bpmnReplacePreview",
"bpmnRules",
"bpmnSearch",可根据xml的中的元素id模糊搜索
"bpmnUpdater",
"bpmnjs",
"canvas",
"changeSupport",
"clipboard",
"commandStack",
"config",
"connect",
"connectPreview",
"connectSnapping",
"connectionDocking",
"connectionPreview",
"connectionSegmentMove",
"contextPad",
"contextPadProvider",
"copyPaste",
"create",
"createBehavior",
"createDataObjectBehavior",
"createMoveSnapping",
"createParticipantBehavior",
"createPreview",
"dataInputAssociationBehavior",
"dataStoreBehavior",
"defaultRenderer",
"deleteLaneBehavior",
"detachEventBehavior",
"directEditing",
"distributeElements",
"distributeElementsMenuProvider",
"dragging",
"drilldownBreadcrumbs",
"drilldownCentering",
"drilldownOverlayBehavior",
"dropOnFlowBehavior",
"editorActions",
"elementFactory",内含创建新的组件的方法, 下文会提及
"elementRegistry",可用于找出所有渲染出来的组件, 下文会提及
"eventBasedGatewayBehavior",
"eventBus",他下面的._listeners列出了所有可以监听的事件
"fixHoverBehavior",
"globalConnect",
"graphicsFactory",
"gridSnapping",
"gridSnappingAutoPlaceBehavior",
"gridSnappingLayoutConnectionBehavior",
"gridSnappingParticipantBehavior",
"gridSnappingResizeBehavior",
"gridSnappingSpaceToolBehavior",
"groupBehavior",
"handTool",
"hoverFix",
"importDockingFix",
"interactionEvents",
"isHorizontalFix",
"keyboard",
"keyboardBindings",
"keyboardMove",
"keyboardMoveSelection",
"labelBehavior",
"labelEditingPreview",
"labelEditingProvider",
"labelSupport",
"lassoTool",
"layoutConnectionBehavior",
"layouter",
"messageFlowBehavior",
"moddle",
"moddleCopy",
"modeling",
"modelingFeedback",
"mouse",
"move",
"moveCanvas",
"movePreview",
"outline",
"overlays",
"padEntries",blog2中传入contextPad自定义组件的参数, 这里可获取
"palette",
"paletteEntries",blog2中传入palette自定义组件的参数, 这里可获取
"paletteProvider",左侧的palette模块
"pathMap",
"popupMenu",
"previewSupport",
"removeElementBehavior",
"removeEmbeddedLabelBoundsBehavior",
"removeParticipantBehavior",
"replace",contextPad的替换功能有用到
"replaceConnectionBehavior",
"replaceElementBehaviour",
"replaceMenuProvider",
"resize",
"resizeBehavior",
"resizeHandles",
"resizeLaneBehavior",
"resizePreview",
"resizeSnapping",
"rootElementReferenceBehavior",
"rootElementsBehavior",
"rules",
"searchPad",
"selection",
"selectionBehavior",
"selectionVisuals",
"snapping",
"spaceTool",
"spaceToolBehavior",
"spaceToolPreview",
"styles",
"subProcessPlaneBehavior",
"subProcessStartEventBehavior",
"subprocessCompatibility",
"textRenderer",
"toggleCollapseConnectionBehaviour",
"toggleElementCollapseBehaviour",
"toolManager",
"tooltips",
"touchFix",
"touchInteractionEvents",
"translate",翻译模块
"unclaimIdBehavior",
"unsetDefaultFlowBehavior",
"updateFlowNodeRefsBehavior",
"zoomScroll"

代码实现contextPad添加组件demo

  1. 首先拖出一个开始节点 这一步很关键, 因为我还不会代码原地生成开始节点...

  2. 获取我们要用到的扩展

// 定义bpmnModeler, 略
...

const elementFactory = bpmnModeler.get('elementFactory')
const elementRegistry = bpmnModeler.get('elementRegistry')
const autoPlace = bpmnModeler.get('autoPlace')

  1. 创建一个新的task流程组件, 找到我们1里的开始节点, 自动连接上

const newTaskShape = elementFactory.createShape({ type: 'bpmn:ServiceTask' })
const [rootShape, startShape] = elementRegistry.getAll()

autoPlace.append(startShape, newTaskShape)

以上阅读的相关代码可见/bpmn-js/lib/features/context-pad/ContextPadProvider.js demo的代码例子file: /src/demos/modelerAutoAdd

自定义条件组件实现demo

在学习笔记(2)里讲了如何创建一个自定义组件, 这里我们基于bpmn:ParallelGateway这个基础组件, 创建一个自定义的条件组件

export const ConditionPalett = {
  key: 'create.condition',
  type: 'bpmn:ParallelGateway',
  group: 'gateway',
  iconClassName: 'bpmn-icon-gateway-parallel',
  title: '条件组件',
  show: true
}

之后把我们的自定义条件组件丢到左侧菜单栏, 监听shape.added的事件

...
// 自定义的条件组件, 自动带一个subProcess出现
    if (target.type === 'bpmn:ParallelGateway') {
        const elementFactory = modeler.get('elementFactory')
        const elementRegistry = modeler.get('elementRegistry')
        const autoPlace = modeler.get('autoPlace')
        
        const newTaskShape = elementFactory.createShape({
            type: 'bpmn:SubProcess',
            isExpanded: true,
            rcProperties: [{
                key: 'name',
                value: 'true',
            }]
        })
        autoPlace.append(target, newTaskShape)
    }
...

demo的代码例子file: /src/demos/modelerAutoAdd

Tags: 前端 bpmn