Bpmn.js 进阶指南之原理分析与模块改造(上)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: Bpmn.js 进阶指南之原理分析与模块改造

前言


由于 bpmn.js 内部各个模块相互独立,很难编写出全面且流畅的使用教程,之前写的文章也常常是写到一半便没了头绪,所以看起来和没看没什么区别。


现在在了解了 bpmn.js 与 diagram.js 的源码,并对相关模块和插件进行了 dts (typescript declare) 的编写之后,心里大致明白如何在原来的基础上进行扩展与重置,所以希望这篇文章能写的尽量全面和清晰,减少大家入坑时消耗的时间和精力。


上节 Bpmn.js简介与基础使用 - 掘金 中,讲述了 bpmn.js 的简介和相关底层依赖,以及在 Vue 2.x 项目中的基础使用。本篇将在该基础上介绍几种常见 additionalModule 的扩展和自定义重写。


本篇示例代码将采用 Vue 3.0 结合 PiniaTsx 来展示,并且 bpmn.js 版本为 9.2,具体项目Demo见 Vite Vue Process Designer


因为作者很少写文章,所以排版和描述可能有些不够清晰,希望大家多多包涵。如果您觉得有地方可以改进或者描述有误差,希望您能及时指出,让我可以加以改正,谢谢😋😋


源码地址(github): vite-vue-bpmn-process:基于 Vite + TypeScript+ Vue3 + NaiveUI + Bpmn.js 的流程编辑器(前端部分)


1. 创建基础页面


首先,我们需要创建一个“容器”,用来显示 Designer 流程设计器实例 与 PropertiesPanel 属性配置边栏。根据 bpmn-js-properties-Panel 仓库的说明,只需要在页面放置一个 Div 并设置对应的 id 即可,在后续初始化设计器实例时将边栏元素 id 传递给 Modeler 构造函数。


当然,一个“设计器”不可能没有工具栏,所以我们也需要实现一个 Toolbar 组件,用来提供放大缩小、撤销恢复等相关功能。


import { defineComponent, computed, ref } from 'vue'
import Designer from '@/components/Designer'
import Toolbar from '@/components/Toolbar'
const App = defineComponent({
    setup() {
        return () => (
            <div class="main-content">
                <Toolbar></Toolbar>
                <Designer v-model={[processXml.value, 'xml']}></Designer>
                <div class="camunda-Panel" id="camunda-Panel"></div>
            </div>
        )
    }
})
export default App


2. 创建 Modeler 组件


当前步骤主要是初始化一个基础的 BpmnModeler 实例,包含默认的功能模块;并且使用 Pinia 来缓存当前的 Modeler 实例。


// Designer/index.tsx
import { defineComponent, ref, onMounted } from 'vue'
import modulesAndModdle from '@/components/Designer/modulesAndModdle'
import initModeler from '@/components/Designer/initModeler'
import { createNewDiagram } from '@/utils'
const Designer = defineComponent({
    name: 'Designer',
    emits: ['update:xml', 'command-stack-changed'],
    setup(props, { emit }) {
        const designer = ref<HTMLDivElement | null>(null)
        onMounted(() => {
            const modelerModules = modulesAndModdle()
            initModeler(designer, modelerModules, emit)
            createNewDiagram()
        })
        return () => <div ref={designer} class="designer"></div>
    }
})
export default Designer


// store/modeler.ts
import { defineStore } from 'pinia'
type ModelerStore = {
    activeElement: Base | undefined
    activeElementId: string | undefined
    modeler: Modeler | undefined
    moddle: Moddle | undefined
    modeling: Modeling | undefined
    canvas: Canvas | undefined
    elementRegistry: ElementRegistry | undefined
}
const defaultState: ModelerStore = {
    activeElement: undefined,
    activeElementId: undefined,
    modeler: undefined,
    moddle: undefined,
    modeling: undefined,
    canvas: undefined,
    elementRegistry: undefined
}
export default defineStore('modeler', {
    state: () => defaultState,
    getters: {
        getActive: (state) => state.activeElement,
        getActiveId: (state) => state.activeElementId,
        getModeler: (state) => state.modeler,
        // 这里的后续步骤也可以改写成 getXxx = (state) => state.modeler?.get('xxx')
        getModdle: (state) => state.moddle,
        getModeling: (state) => state.modeling,
        getCanvas: (state) => state.canvas,
        getElRegistry: (state) => state.elementRegistry
    },
    actions: {
        setModeler(modeler) {
            this.modeler = modeler
        },
        setModules<K extends keyof ModelerStore>(key: K, module) {
            this[key] = module
        },
        setElement(element: Base, id: string) {
            this.activeElement = element
            this.activeElementId = id
        }
    }
})


这一步相信大多数人都能理解


  1. 通过 modulesAndModdle 获取到对应的配置项


  1. 调用 initModeler() 来实例化 bpmn.jsModeler 构造函数


  1. 最后调用 createNewDiagram() 来创建一个基础的流程图。


store/modeler.ts 内部则是创建了一个数据状态缓存,用来保存 Modeler 实例,以及提供基础功能模块的 getter 方法。


其中 modulesAndModdle 部分为本篇核心部分,这里先跳过,后续进行讲解。


以下是 initModelercreateNewDiagram 方法的具体代码:


// 1. initModeler.ts
import modeler from '@/store/modeler'
import { markRaw, Ref } from 'vue'
export default function (designer: Ref<HTMLElement | null>, modelerModules: ViewerOptions<Element>, emit) {
    const modelerStore = modeler()
    const options: ViewerOptions<Element> = {
        container: designer!.value as HTMLElement,
        additionalModules: modelerModules[0] || [],
        moddleExtensions: modelerModules[1] || {},
        propertiesPanel: {
            parent: '#camunda-panel'
        },
        ...modelerModules[2]
    }
    const modeler: Modeler = new Modeler(options)
    // 更新 store 缓存数据,这里使用 markRaw 定义非响应式处理,避免 proxy 代理影响原始状态和方法
    store.setModeler(markRaw(modeler))
    store.setModules('moddle', markRaw(modeler.get<Moddle>('moddle')))
    store.setModules('modeling', markRaw(modeler.get<Modeling>('modeling')))
    store.setModules('canvas', markRaw(modeler.get<Canvas>('canvas')))
    store.setModules('elementRegistry', markRaw(modeler.get<ElementRegistry>('elementRegistry')))
}


// createNewDiagram.ts
import modeler from '@/store/modeler'
export const createNewDiagram = async function (newXml?: string) {
    try {
        const modelerStore = modeler()
        const timestamp = Date.now()
        const newId: string = `Process_${timestamp}`
        const newName: string = `业务流程_${timestamp}`
        const processEngine: string = 'camunda'
        const xmlString = newXml || EmptyXML(newId, newName, processEngine)
        const modeler = store.getModeler
        const { warnings } = await modeler!.importXML(xmlString)
        if (warnings && warnings.length) {
            warnings.forEach((warn) => console.warn(warn))
        }
    } catch (e) {
        console.error(`[Process Designer Warn]: ${typeof e === 'string' ? e : (e as Error)?.message}`)
    }
}


经过一点点美化之后,我们就能得到这样一个编辑器界面:


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


下面我们详细讲讲 new Modeler 的整个过程。


3. Bpmn.js 的“实例化过程”


initModeler 时,我们传递进 Modeler 构造函数的参数主要包含四个部分:


  1. container :画布挂载的 Div,可以直接传递这个 Div 的元素实例,也可以传递该元素对应的 id 字符串


  1. additionalModules :Bpmn.js 所使用的相关插件,是一个对象数组


  1. moddleExtensions :用来进行 xml 字符串解析以及元素、属性实例定义的声明,是一个对象格式参数,通常 key 是声明的属性前缀,对应的属性值则是一个模块的所有扩展属性定义声明,通常为外部引入的一个json文件或者js对象


  1. options :其他配置项,包括上文提到的 propertiesPanel,这些配置项一般以插件实例的名称作为 key,用来给对应插件提供特殊的实例化配置参数


在进行 new Modeler() 时,首先会与 bpmn.js 的 Modeler 默认配置进行合并,之后创建一个 BpmnModdle(moddleExtensions) 实例作为 modeler._moddle 的属性值,该模块主要用来进行 xml 字符串的解析和属性转换,也可以用来注册新的解析规则创建对应的元素实例


之后创建一个 DOM 节点作为画布区域,挂载到 modeler._container 上,并添加 bpmn-io 的 logo。


然后,会根据 additionalModules 和默认的 { bpmnjs: [ 'value', this ], moddle: [ 'value', moddle ] } 合并,再合并 canvas 配置,调用 Diagram 进行后续逻辑,结束后再将 _container 挂载到传入的 container 对应的 DOM 节点上。


new Modeler()new Diagram() 主要过程如下:


function Modeler(options) {
    BaseModeler.call(this, options);
}
function BaseModeler(options) {
    BaseViewer.call(this, options);
    // 添加 导入解析完成事件 的监听,在解析正常时处理和保存元素id
    this.on('import.parse.complete', function(event) {
        if (!event.error) {
            this._collectIds(event.definitions, event.elementsById);
        }
    }, this);
    // 添加 销毁事件 的监听,在画布销毁时清空保存的元素ids
    this.on('diagram.destroy', function() {
        this.get('moddle').ids.clear();
    }, this);
}
function BaseViewer(options) {
    options = assign({}, DEFAULT_OPTIONS, options);
    this._moddle = this._createModdle(options);
    this._container = this._createContainer(options);
    addProjectLogo(this._container);
    this._init(this._container, this._moddle, options);
}
BaseViewer.prototype._init = function(container, moddle, options) {
    // getModules() 返回 Modeler.prototype._modules,包含官方默认引入的插件
    var baseModules = options.modules || this.getModules(),
        additionalModules = options.additionalModules || [],
        staticModules = [{ bpmnjs: [ 'value', this ], moddle: [ 'value', moddle ] }];
    var diagramModules = [].concat(staticModules, baseModules, additionalModules);
    var diagramOptions = assign(omit(options, [ 'additionalModules' ]), {
        canvas: assign({}, options.canvas, { container: container }),
        modules: diagramModules
    });
    // invoke diagram constructor
    Diagram.call(this, diagramOptions);
    if (options && options.container) {
        this.attachTo(options.container);
    }
};
function Diagram(options, injector) {
    this.injector = injector = injector || createInjector(options);
    this.get = injector.get;
    this.invoke = injector.invoke;
    this.get('eventBus').fire('diagram.init');
}


new Diagram(diagramOptions) 的过程中,主要是通过 createInjector(options) 实例化 InjectoradditionalModules 中配置的插件实例,并触发 diagram.init 事件表示画布实例化结束。


createInjector(options) 过程中会将 diagramOptions 全部作为 { config: [ 'value', diagramOptions ] } 保存在一个 configModule 模块中,并添加 Diagram 的基础插件包 CoreModule,之后执行 injector = new Injector(modules)injector.init()


3.1 Injector


上一章我们讲过,Bpmn.js 继承自 Diagram.js,采用依赖注入的形式来链接各个插件之间的引用关系。


这个进行依赖注入的注入器 Injector(源码见 didi), 在进行 new Modeler(options) 时,便会进行一次实例化,对 options 内部的属性进行解析与实例化(部分),并挂载到 Injector 实例下的 _instances 上。并且在 Modeler 的实例上创建两个属性:getinvoke


get 方法指向 Injector 实例的 get 方法,可以通过 modeler.get('xxx') 来获取对应的插件实例。


invoke 方法指向 Injector 实例的 invoke(func, context, locals) 方法,作用向插件系统中注入新插件和依赖的方法,会根据 locals 或者 func.$inject 来声明该函数对应的依赖关系。


📌所以源码中很多需要调用其他模块实例的构造函数,末尾都会有一个 $inject 静态属性。


首先,Injector 是一个构造函数


Injector 接收两个参数:modules, parent。 其中 parent 是可选参数,如果为空,会默认生成一个带有 get() 方法的对象参与后面的逻辑。


new Injector(modules, parent) 时,首先执行:


// 省略了 parent 判断部分
const providers = this._providers = Object.create(parent._providers || null);
const instances = this._instances = Object.create(null);
const self = instances.injector = this;


这里会在 Injector 的实例上挂载 _providers 属性,保存各个 additionalModule 的配置; 挂载 _instances 属性,保存各个 additionalModule 对应配置项生成的函数、实例、或者配置常量;挂载 injector 属性指向当前实例本身,用来提供给 additionalMudole 的配置实例化时调用。


随后执行:


this.get = get;
this.invoke = invoke;
this.instantiate = instantiate;
this.createChild = createChild;
// setup
this.init = bootstrap(modules);


这里执行 bootstrap(modules) 方法,遍历传入的 modules 插件模块配置项,并进行扁平化处理 resolveDependencies;然后遍历扁平化结果,执行模块的加载和初始化 loadModule;最后返回一个闭包函数,用来进行模块实例初始化。


function bootstrap(moduleDefinitions) {
    var initializers = moduleDefinitions
        .reduce(resolveDependencies, [])
        .map(loadModule);
    var initialized = false;
    return function() {
        if (initialized) return;
        initialized = true;
        initializers.forEach(function(initializer) {
            return initializer();
        });
    };
}


moduleDefinitions.reduce(resolveDependencies, []) 过程中,如果某一遍历项存在 __depends__ , 则会对 __depends__ 数组再次进行遍历操作。如果当前项已经存在新的数组中,则直接返回。


function resolveDependencies(moduleDefinitions: ModuleDefinition[], moduleDefinition: ModuleDefinition): ModuleDefinition[] {
    if (moduleDefinitions.indexOf(moduleDefinition) !== -1) {
        return moduleDefinitions;
    }
    moduleDefinitions = (moduleDefinition.__depends__ || []).reduce(resolveDependencies, moduleDefinitions);
    if (moduleDefinitions.indexOf(moduleDefinition) !== -1) {
        return moduleDefinitions;
    }
    return moduleDefinitions.concat(moduleDefinition);
}


loadModule 时,会区分两种情况处理:private modulenormal module,但是最终返回的都是一个 函数,用来获取 module 插件实例或者函数等(这里主要处理每个插件模块中配置的 __init__ 属性,保存到闭包函数的遍历 initializers 中,供后面 injector.init() 调用)。


private module 私有模块通过某个模块的 moduleDefinition.__exports__ 是否有值来区分,目前 diagram.jsbpmn.js 都没有私有模块。所以这里暂时不做讲解。


type ProviderType = 'value' | 'factory' | 'type'
type FactoryMap<T> = {
    factory<T>(func: (...args: unknown[]) => T, context: InjectionContext, locals: LocalsMap): T
    type<T>(Type: T): T
    value(T): T
}
type ProviderType<T> = [Function, T | Function, ProviderType]
function loadModule(moduleDefinition: ModuleDefinition): Function {
    Object.keys(moduleDefinition).forEach(function(key: string) {
        // 区分模块依赖定义字段
        if (key === '__init__' || key === '__depends__') return;
        if (moduleDefinition[key][2] === 'private') {
            providers[key] = moduleDefinition[key];
            return;
        }
        const type: string = moduleDefinition[key][0];
        const value: Object | Function = moduleDefinition[key][1];
        // arrayUnwrap 主要是判断模块定义类型,如果是 'value' 或者 'factory',则直接返回对应函数
        // 否则判断第二个参数类型,如果是数组格式,则对其按照模块标准定义格式重新进行格式化再返回格式化后的函数
        providers[key] = [ factoryMap[type], arrayUnwrap(type, value), type ];
    });
    // self 在 Injector() 已经定义,指向 injector 实例
    return createInitializer(moduleDefinition, self);
}
// 这里是根据模块定义,来定义初始化时需要执行实例化的模块,以及该模块的实例获取方式
function createInitializer(moduleDefinition: ModuleDefinition, injector: Injector): Function {
    var initializers = moduleDefinition.__init__ || [];
    return function() {
        initializers.forEach(function(initializer) {
            try {
                if (typeof initializer === 'string') {
                    injector.get(initializer);
                } else {
                    injector.invoke(initializer);
                }
            } catch (error) {
                if (typeof AggregateError !== 'undefined') {
                    throw new AggregateError([ error ], 'Failed to initialize!');
                }
                throw new Error('Failed to initialize! ' + error.message);
            }
        });
    };
}


直到这里为止,都依然在 Injector 的实例化过程中,在 injector 实例上,目前 _instances 属性也只有在初始化时挂载的 injector 本身。但 _providers 属性上已经包含了所有的模块定义。


这里是通过遍历 moduleDefinition 来更新 _providers 对象,所以后面我们才可以用同名模块来覆盖 bpmn.js 原有的模块


并且为 init 定义了一个模块实例的初始化函数,内部使用 initialized 变量(闭包)避免二次初始化。


3.2 Diagram


在 3.1 Injector 已经简单解析了 new Injector() 的过程,这时已经对所有的 modules 进行了处理,但是插件实例依然还是空值。


所以在 new Diagram() 中,会继续调用 injector.init() 执行模块实例的处理。这里会通过 new Injector()bootstrap 方法返回的函数,去遍历闭包里面的 initializers 数组,进行初始化 initializer()


initializers = moduleDefinition.__init__ || [];
initializers.forEach(function(initializer) {
    if (typeof initializer === 'string') {
        injector.get(initializer);
    } else {
        injector.invoke(initializer);
    }
})


因为 initializers 保存的是模块定义中的 __init__ 属性,在 bpmn.jsdiagram.js 中基本都是字符串数组,所以都是通过 injector.get(name, strict) 来进行实例化。该方法主要是 name 参数,查找 injector._instance 是否有该名称对应的实例;否则调用 injector._providers[name] 进行实例化,保存实例化结果并返回;如果都不存在,则调用 new Injector() 时传入的 parent 参数的 get 方法。简易代码如下:


function get(name, strict) {
    // 这里是用来处理类似 config.canvas 这类配置项数据
    if (!providers[name] && name.indexOf('.') !== -1) {
        var parts = name.split('.');
        var pivot = get(parts.shift());
        while (parts.length) {
            pivot = pivot[parts.shift()];
        }
        return pivot;
    }
    if (hasOwnProp(instances, name)) {
        return instances[name];
    }
    if (hasOwnProp(providers, name)) {
        if (currentlyResolving.indexOf(name) !== -1) {
            currentlyResolving.push(name);
            throw error('Cannot resolve circular dependency!');
        }
        currentlyResolving.push(name);
        instances[name] = providers[name][0](providers[name][1]);
        currentlyResolving.pop();
        return instances[name];
    }
    return parent.get(name, strict);
}


上文我们说到,在 new Diagram() 时会在传递的 diagramOptions 参数中添加一个 configModule 和 基础插件依赖 coreModule。这里的 coreModule 主要包含以下模块:


  1. canvas:主要的画布区域,负责创建和管理图层、元素 class 标记管理、创建删除 svg 元素、查找根节点等等


  1. elementRegistry:元素 id 与 元素图形、实例之间的关系表,用于元素查找等


  1. elementFactory:基础的元素实例构造函数,管理基础的几个元素类型构造函数,用来创建新的元素实例


  1. eventBus:事件总线模块,通过发布订阅模式,联通各个模块之间的处理逻辑


  1. graphicsFactory:负责 svg 元素创建和删除


并且依赖了 defaultRendererstyles 模块。


  1. defaultRenderer:默认的 svg 渲染函数,继承自抽象构造函数 BaseRenderer,用来校验和绘制 svg 元素,并设置了三种默认样式 CONNECTION_STYLESHAPE_STYLEFRAME_STYLE


  1. styles:样式处理函数,用来合并元素的颜色配置


在以上步骤都完成之后,我们的画布也就基本上初始化结束。但是,diagram.js的内容远远不止于此!


以上几个模块,主要是作为 diagram.js 根据默认配置进行初始化时会依赖的核心插件模块。diagram.js 还提供了一个 features 目录,存放了 21 个扩展插件模块,包含对齐、属性更新、元素替换、上下文菜单等等,这部分内容稍后会进行部分讲解。下面就到了最激动人心的 bpmn.js 了。


3.3 Bpmn BaseViewer


在第三节开头,我们说过在 new Diagram() 之前会进行配置合并、_moddle_container 属性创建等一系列操作,都是在 BaseViewer 这里完成的。

BaseViewertypescript 声明大致如下:


declare class BaseViewer extends Diagram {
    constructor(options?: ViewerOptions<Element>)
    importXML(xml: string): Promise<DoneCallbackOpt>
    open(diagram: string): Promise<DoneCallbackOpt>
    saveXML(options?: WriterOptions): Promise<DoneCallbackOpt>
    saveSVG(options?: WriterOptions): Promise<DoneCallbackOpt>
    clear(): void
    destroy(): void
    on<T extends BPMNEvent, P extends InternalEvent>(
      event: T,
      priority: number | BPMNEventCallback<P>,
      callback?: EventCallback<T, any>,
      that?: this
    ): void
    off<T extends BPMNEvent, P extends InternalEvent>(
      events: T | T[],
      callback?: BPMNEventCallback<P>
    ): void
    attachTo<T extends Element>(parentNode: string | T): void
    detach(): void
    importDefinitions(): ModdleElement
    getDefinitions(): ModdleElement
    protected _setDefinitions(definitions: ModdleElement): void
    protected _modules: ModuleDefinition[]
}


该函数主要是创建一个只包含导入导出、挂载销毁、解析规则定义等基础功能 BPMN 2.0 流程图查看器,不能移动和缩放,也不能按照不同元素类型绘制 svg 图形来显示,所以这个构造函数一般也不会使用,除非我们需要按照其他业务需求定制查看器。


BaseViewer 提供了 baseViewer.on()baseViewer.offbaseViewer._emit 来创建、销毁和触发监听事件的方法,内部也是调用的 injector.get('eventBus') 来实现的,所以 modeler.on()baseViewer.on()injector.get('eventBus').on()modeler.get('eventBus').on() 最终效果与显示逻辑都是一致的,我们按照习惯任意选择一种即可。


同理, baseViewer.offbaseViewer._emit 也是一样。


3.4 Bpmn BaseModeler


BaseModeler 实际上与 BaseViewer 差异不是很大,只是在初始化时增加了两个监听事件,并在原型上添加了两个方法( 有一个是重写覆盖 )。


declare class BaseModeler extends BaseViewer {
    constructor(options?: ViewerOptions<Element>)
    _createModdle(options: Object): BpmnModdle
    _collectIds(definitions: ModdleElement, elementsById: Object): void
}


3.5 Bpmn Modeler


ModelerBaseModeler 的基础上,添加了一个 createDiagram() 方法,用来创建一个默认的 BPMN 2.0 流程图(默认 id 为 Process_1,并包含一个 id 为 StartEvent_1 的开始事件节点)。


在原型上添加了以下几个属性:


  1. Viewer:指向 bpmn.jsViewer 构造函数地址


  1. NavigatedViewer:指向 bpmn.jsNavigatedViewer 构造函数地址


  1. _interactionModules:键盘、鼠标等互动模块,包含 KeyboardMoveModule, MoveCanvasModule, TouchModule, ZoomScrollModule,均来自 diagram-js/lib/features


  1. _modelingModules:核心的建模工具模块,包含用来更新元素实例属性的 ModelingModule、元素上下文菜单 ContextPadModule、元素选择器侧边栏 PaletteModule


  1. _modules:合并了 Viewer.prototype._modules_interactionModules_modelingModules 之后的插件模块配置数组


Viewer.prototype._modules 则包含了 bpmn.js 相关的元素绘制、元素选择、图层管理等相关模块,也包含元素实例和画布 svg 元素关联的模块。


因为 Modeler 构造函数对 _modules 进行了重定义,引入完整的建模扩展插件(模块),所以在使用时,我们仅需要指定 container 配置项,即可得到一个完整的建模器。


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


当然,由于没有引入流程引擎对应的解析文件与 panel 属性侧边栏,所以这种方式实际作用不是很大。


4. Properties Panel


🚩🚩 在 bpmn-js-properties-Panel 的 1.x 版本进行了颠覆性的更新,不仅重写了 UI 界面,1.x 版本之前的部分 API 和属性编辑栏构造函数都进行了重写,并将属性栏 DOM 构建与更新方式改写为 React JSX HooksComponents 的形式,迁移到了 @bpmn-io/properties-panel 仓库中。


4.1 Basic Properties Panel


使用侧边栏的方式与引入一个 additionalModule 一样,代码如下:


import Modeler from 'bpmn-js/lib/Modeler';
import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel';
import 'bpmn-js-properties-panel/dist/assets/properties-panel.css';
const modeler = new Modeler({
  container: '#canvas',
  propertiesPanel: {
    parent: '#properties'
  },
  additionalModules: [
    BpmnPropertiesPanelModule,
    BpmnPropertiesProviderModule
  ]
});


这样我们就已经引入了一个最基础的属性侧边栏模块。


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


当然这里需要注意以下几点:


  1. 必须引入 properties-panel.css 样式文件


  1. new Modeler() 时,必须传入配置项 propertiesPanel,并设置 parent 属性,用来指定侧边栏挂载的 DOM 节点


  1. additionalModules 需要同时引入 BpmnPropertiesPanelModuleBpmnPropertiesProviderModule ,否则不能正常使用。


这里对第二、三点大致解释一下:


在第 3 节的开头,我们说到过在进行实例化的时候,会把 new Modeler(options) 时的 options 作为一个 configModule 注入到依赖系统里面。其他 module 可以通过声明构造函数属性 Constructor.$inject = ['config'] 或者 Constructor.$inject = ['config.xxxModule'] 来读取配置项数据。


BpmnPropertiesPanelModule 作为属性侧边栏的 DOM 构造器,主要用来渲染侧边栏基础界面,并在流程创建完成或者元素属性更新之后,通过 additionalModules 内引用的 PropertiesProviderModules 来创建具体的属性编辑表单项。


BpmnPropertiesProviderModule 作为 bpmn.js 本身依赖的基础属性构造器,主要包含以下部分:


  1. Id, NameDocumentation 属性,以及 Process 节点或者具有 processRef 定义的 Participant 节点特有的 isExecutable 属性


  1. 具有 “特殊事件定义” 的事件节点(例如 StartEvent, EndEvent, BoundaryEvent 节点等),可以配置的 Message, Error, Singal


  1. 具有 “多实例定义” 的任务类型节点,可以配置的 MultiInstance 属性(又分为 LoopCardinalityCompletionCondition)


4.2 BpmnPropertiesPanelModule, BpmnPropertiesPanelPropertiesProviderModule


4.2.1 BpmnPropertiesPanelModule


上文我们已经讲过,BpmnPropertiesPanelModule 主要用于构建基础的属性侧边栏面板,并通过 PropertiesProviderModule 来生成对应的属性表单项。


declare class BpmnPropertiesPanelModule extends ModuleConstructor {
    constructor(config: Object, injector: Injector, eventBus: EventBus)
    _eventBus: EventBus
    _injector: Injector
    _layoutConfig: undefined | Object
    _descriptionConfig: undefined | Object
    _container: Element
    attachTo(container: Element): void
    detach(): void
    registerProvider(priority: number | PropertiesProvider, provider?: PropertiesProvider): void
    _getProviders(element?: Base): PropertiesProvider[]
    _render(element?: Base): void
    _destroy(): void
}


BpmnPropertiesPanelModule 在初始化时,会监听三个事件:


  1. diagram.init:在画布初始化时,调用 attach 方法将自己的 _container 面板节点挂载到 config.propertiesPenal.parent


  1. diagram.destroy:在画布销毁时,将面板节点从 _container.parentNode 移除


  1. root.added:在根节点创建完成后,调用 _render() 方法,创建一个 BpmnPropertiesPanel 组件并渲染


4.2.2 BpmnPropertiesPanel 组件


BpmnPropertiesPanel 组件的写法与 React Hooks Component 的写法一样,主要实现一下几个方面的功能:


  1. 通过 EventBus 实例来设置 selection.changed, elements.changed, propertiesPanel.providersChanged, elementTemplates.changed, root.added 几个事件的监听函数,根据选中元素变化来更新当前状态。


  1. 通过 BpmnPropertiesPanelModule._getProviders() 获取已注册的 PropertiesProviderModules 数组,遍历数组,调用 PropertiesProviderModule.getGroups(element) 来获取当前元素对应的属性配置项分组,用于后面的组件渲染。


const eventBus = injector.get('eventBus');
const [ state, setState ] = useState({ selectedElement: element });
const selectedElement = state.selectedElement;
// 1
useEffect(() => {
    const onSelectionChanged = (e) => {
        const { newSelection = [] } = e;
        if (newSelection.length > 1) {
            return _update(newSelection);
        }
        const newElement = newSelection[0];
        const rootElement = canvas.getRootElement();
        if (isImplicitRoot(rootElement)) {
            return;
        }
        _update(newElement || rootElement);
    };
    eventBus.on('selection.changed', onSelectionChanged);
    return () => {
        eventBus.off('selection.changed', onSelectionChanged);
    };
}, [])
useEffect(() => {
    const onElementsChanged = (e) => {
        const elements = e.elements;
        const updatedElement = findElement(elements, selectedElement);
        if (updatedElement && elementExists(updatedElement, elementRegistry)) {
            _update(updatedElement);
        }
    };
    eventBus.on('elements.changed', onElementsChanged);
    return () => {
        eventBus.off('elements.changed', onElementsChanged);
    };
}, [selectedElement])
// 省略了 useEffect 部分,详细内容见源码 https://github.com/bpmn-io/bpmn-js-properties-panel/blob/master/src/render/BpmnPropertiesPanel.js
const onRootAdded = (e) => {
    const element = e.element;
    _update(element);
};
eventBus.on('root.added', onRootAdded);
const onProvidersChanged = () => {
    _update(selectedElement);
};
eventBus.on('propertiesPanel.providersChanged', onProvidersChanged);
const onTemplatesChanged = () => {
    _update(selectedElement);
};
eventBus.on('elementTemplates.changed', onTemplatesChanged);
// 2
const providers = getProviders(selectedElement);
const groups = useMemo(() => {
    return reduce(providers, function(groups, provider) {
        if (isArray(selectedElement)) return [];
        const updater = provider.getGroups(selectedElement);
        return updater(groups);
    }, []);
}, [ providers, selectedElement ]);


4.2.3 PropertiesProviderModule


该模块(或者说这类模块)主要用来注册元素的属性配置项,依赖 BpmnPropertiesPanelModule 组件,通过实例化时调用 BpmnPropertiesPanelModule.registerProvider(this) 来将自身注册到属性侧边栏面板的构造器当中。当然,通过 BpmnPropertiesPanel 组件的内部逻辑,我们知道每个 PropertiesProviderModule 还需要提供一个 getGroups 方法,用来获取当前元素对应的属性配置项分组。


// 基础的 Provider ts 定义
declare class PropertiesProviderModule {
    constructor(propertiesPanel: BpmnPropertiesPanelModule)
    getGroups(element: Base): () => Group[]
}
// 下面是 bpmn 基础属性栏的 PropertiesProviderModule 定义
function getGroups$1(element) {
    const groups = [
        GeneralGroup(element),
        DocumentationGroup(element),
        CompensationGroup(element),
        ErrorGroup(element),
        LinkGroup(element),
        MessageGroup(element),
        MultiInstanceGroup(element),
        SignalGroup(element),
        EscalationGroup(element),
        TimerGroup(element)
    ];
    return groups.filter(group => group !== null);
}
export default class BpmnPropertiesProvider {
    constructor(propertiesPanel) {
        propertiesPanel.registerProvider(this);
    }
    getGroups(element) {
        return (groups) => {
            groups = groups.concat(getGroups$1(element));
            return groups;
        };
    }
}
BpmnPropertiesProvider.$inject = [ 'propertiesPanel' ];


这里需要注意的是 getGroups 最终返回的是一个函数,通过传入参数 groups 来合并当前 PropertiesProviderModule 的属性分组定义


4.3 Camunda Properties Panel


bpmn.io 的团队介绍中,可以得知该团队主要成员均来自 camunda 的团队,所以官方也针对 camunda 流程引擎开发了对应的 Properties Panel 插件,主要用来编辑一些不能体现在可视界面上的特殊属性(也包含通用属性,类似 Id、name、documentation 等)。


基础属性侧边栏可配置的属性非常少,基本上不能满足一个业务流程的配置需求。所以 camunda 的团队针对自身的流程引擎对属性侧边栏进行了补充。引用代码如下:


import Modeler from 'bpmn-js/lib/Modeler';
import {
  BpmnPropertiesPanelModule,
  BpmnPropertiesProviderModule,
  CamundaPlatformPropertiesProviderModule
} from 'bpmn-js-properties-panel';
import CamundaExtensionModule from 'camunda-bpmn-moddle/lib'
import camundaModdleDescriptors from 'camunda-bpmn-moddle/resources/camunda';
const modeler = new Modeler({
  container: '#canvas',
  propertiesPanel: {
    parent: '#properties'
  },
  additionalModules: [
    BpmnPropertiesPanelModule,
    BpmnPropertiesProviderModule,
    CamundaPlatformPropertiesProviderModule,
    CamundaExtensionModule
  ],
  moddleExtensions: {
    camunda: camundaModdleDescriptors
  }
});


这里与引入基础属性侧边栏相比,增加了一下几点配置项:


  1. additionalModules 增加 CamundaExtensionModule(扩展校验模块,用来校验复制粘贴、属性移除等) 和 CamundaPlatformPropertiesProviderModule(提供异步控制属性、监听器配置、扩展属性、条件配置等)


  1. moddleExtensions 配置属性 camunda: camundaModdleDescriptors,用来解析与识别 camunda 流程引擎配置的特殊业务属性以及属性关联格式等。


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


具体的 moddleExtension 配置可以查看 Bpmn-js自定义描述文件说明-掘金


4.4 Custom Properties Panel


虽然 camunda 官方提供了一个属性编辑面板,但是内部对属性的更新和读取都与 camunda 流程引擎做了强关联,所以在没有使用 camunda 流程引擎的时候,如何去更新元素属性就成了一个亟需解决的问题(特别是国内使用率最多的除了国产流程引擎外就是 flowableactiviti)。


对于这个问题,bpmn-io 官方也编写了一个示例项目properties-panel-extension,对如何扩展属性侧边栏进行了简单说明,这里我们也以这个例子进行讲解。


4.4.1 Properties Moddle Extension


首先,在创建自定义的属性编辑面板之前,需要先定义相关的自定义属性,这里我们以 flowable 流程引擎对应的属性为例。


第一步:定义相关的属性


{
  "name": "Flowable",
  "uri": "http://flowable.org/bpmn",
  "prefix": "flowable",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "associations": [],
  "types": [
    {
      "name": "JobPriorized",
      "isAbstract": true,
      "extends": ["bpmn:Process"],
      "properties": [
        {
          "name": "jobPriority",
          "isAttr": true,
          "type": "String"
        }
      ]
    },
    {
      "name": "Process",
      "isAbstract": true,
      "extends": ["bpmn:Process"],
      "properties": [
        {
          "name": "candidateStarterGroups",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "candidateStarterUsers",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "versionTag",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "historyTimeToLive",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "isStartableInTasklist",
          "isAttr": true,
          "type": "Boolean",
          "default": true
        }
      ]
    }
  ]
}


在这个 json 文件里面,我们对 Process 节点进行了扩展,增加了 versionTag, jobPriority 等属性。


4.4.2 CustomPropertiesProviderModule


第二步:创建属性对应的 PropertiesProviderModule


import { is } from 'bpmn-js/lib/util/ModelUtil';
class FlowablePropertiesProvider {
    constructor(propertiesPanel: BpmnPropertiesPanelModule) {
        propertiesPanel.registerProvider(this)
    }
    getGroups(element) {
        return function (groups) {
            if (is(element, 'bpmn:Process')) {
                // 这里只用 versionTag 属性的配置项作为示例
                const group = [VersionTag(element)]
                groups.concat(group)
            }
            return groups
        }
    }
}
FlowablePropertiesProvider.$inject = ['propertiesPanel']
export default FlowablePropertiesProvider


4.4.3 CustomPropertiesGroup


第三步:实现自定义属性栏分组与 VsersionTag 属性编辑组件


import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil';
import { useService } from 'bpmn-js-properties-panel';
import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel';
// 创建 VersionTag 的属性编辑栏入口 Entry
function VersionTag(props) {
    const { element } = props;
    const commandStack = useService('commandStack');
    const modeling = useService('modeling');
    const debounce = useService('debounceInput');
    const processBo = getBusinessObject(element);
    const getValue = () => processBo.get('flowable:versionTag') || ''
    const setValue = (value) => {
        // 写法 1
        commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: processBo,
            properties: { 'flowable:versionTag': value }
        });
        // 写法 2
        modeling.updateModdleProperties(element, processBo, { 'flowable:versionTag': value })
    };
    // 返回一个属性编辑组件
    return TextFieldEntry({
        element,
        id: 'versionTag',
        label: 'Version Tag',
        getValue,
        setValue,
        debounce
    });
}
// 返回获取自定义属性面板分组的函数
export default function (element) {
    return [
        {
            id: 'custom version',
            element,
            component: VersionTag,
            isEdited: isTextFieldEntryEdited
        }
    ]
}


4.4.4 Use CustomPropertiesProviderModule


第四步:引入自定义属性构造器 FlowablePropertiesProvider


// 省略 modeler 部分引入
// 引入属性声明文件
import flowableDescriptor from 'xxx/flowable.json'
// 引入自定义属性编辑组件的构造函数
import FlowablePropertiesProvider from 'xxx/FlowablePropertiesProvider.ts'
// 组成符合 ModuleDefinition 格式的对应 (可以像官方实例那样放到一个 index 文件内部)
const FlowablePropertiesProviderModule = {
    __init__: [ 'flowablePropertiesProvider' ],
    flowablePropertiesProvider: [ 'type', FlowablePropertiesProvider ]
}
const bpmnModeler = new BpmnModeler({
    container: '#js-canvas',
    propertiesPanel: {
        parent: '#js-properties-panel'
    },
    additionalModules: [
        BpmnPropertiesPanelModule,
        BpmnPropertiesProviderModule,
        FlowablePropertiesProviderModule
    ],
    moddleExtensions: {
        flowable: flowableDescriptor
    }
});


效果大致如下(引用的官方demo的图片,可能字段不一样):


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


目录
相关文章
|
4天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
22天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
9天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
81 9
|
7天前
|
JavaScript 前端开发 开发者
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第27天】在前端开发领域,Vue.js和Angular是两个备受瞩目的框架。本文对比了两者的优劣,Vue.js以轻量级和易上手著称,适合快速开发小型到中型项目;Angular则由Google支持,功能全面,适合大型企业级应用。选择时需考虑项目需求、团队熟悉度和长期维护等因素。
14 1
|
22天前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
33 1
|
1月前
|
数据采集 JSON 前端开发
JavaScript逆向爬虫实战分析
JavaScript逆向爬虫实战分析
|
30天前
|
缓存 JSON JavaScript
Node.js模块系统
10月更文挑战第4天
34 2
|
8天前
|
JavaScript 前端开发 API
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第26天】前端技术的飞速发展让开发者在构建用户界面时有了更多选择。本文对比了Vue.js和Angular两大框架,介绍了它们的特点和优劣,并给出了在实际项目中如何选择的建议。Vue.js轻量级、易上手,适合小型项目;Angular结构化、功能强大,适合大型项目。
11 0
|
1月前
|
数据采集 JavaScript 前端开发
JavaScript逆向爬虫——无限debugger的原理与绕过
JavaScript逆向爬虫——无限debugger的原理与绕过
|
22天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理、应用与代码演示
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理、应用与代码演示