E-COM-NET

  • 首页
  • 在线工具
  • Layui镜像站
  • SUI文档
  • 联系我们
MiyueFE
MiyueFE

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

  • javascript
  • 开发语言
  • ecmascript
  • vue

掘金2023年度人气创作者打榜中,快来帮我打榜吧~ https://activity.juejin.cn/rank/2023/writer/747323639208391?utm_campaign=annual_2023&utm_medium=self_web_share&utm_source=MiyueFE

前言

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

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

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

本篇示例代码将采用 Vue 3.0 结合 Pinia 和 Tsx 来展示,并且 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 () => (
            
) } }) 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(null)

        onMounted(() => {
            const modelerModules = modulesAndModdle()
            initModeler(designer, modelerModules, emit)
            createNewDiagram()
        })

        return () => 
} }) 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 获取到对应的配置项
  2. 调用 initModeler() 来实例化 bpmn.js 的 Modeler 构造函数
  3. 最后调用 createNewDiagram() 来创建一个基础的流程图。

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

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

以下是 initModeler 和 createNewDiagram 方法的具体代码:

// 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}`)
    }
}

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

Bpmn.js 进阶指南之原理分析与模块改造_第1张图片

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

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

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

  1. container :画布挂载的 Div,可以直接传递这个 Div 的元素实例,也可以传递该元素对应的 id 字符串
  2. additionalModules :Bpmn.js 所使用的相关插件,是一个对象数组
  3. moddleExtensions :用来进行 xml 字符串解析以及元素、属性实例定义的声明,是一个对象格式参数,通常 key 是声明的属性前缀,对应的属性值则是一个模块的所有扩展属性定义声明,通常为外部引入的一个json文件或者js对象
  4. 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) 实例化 Injector 与 additionalModules 中配置的插件实例,并触发 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 的实例上创建两个属性:get 和 invoke。

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 module 和 normal module,但是最终返回的都是一个 函数,用来获取 module 插件实例或者函数等(这里主要处理每个插件模块中配置的 __init__ 属性,保存到闭包函数的遍历 initializers 中,供后面 injector.init() 调用)。

private module 私有模块通过某个模块的 moduleDefinition.__exports__ 是否有值来区分,目前 diagram.js 和 bpmn.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.js 和 diagram.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 元素、查找根节点等等
  2. elementRegistry:元素 id 与 元素图形、实例之间的关系表,用于元素查找等
  3. elementFactory:基础的元素实例构造函数,管理基础的几个元素类型构造函数,用来创建新的元素实例
  4. eventBus:事件总线模块,通过发布订阅模式,联通各个模块之间的处理逻辑
  5. graphicsFactory:负责 svg 元素创建和删除

并且依赖了 defaultRenderer 和 styles 模块。

  1. defaultRenderer:默认的 svg 渲染函数,继承自抽象构造函数 BaseRenderer,用来校验和绘制 svg 元素,并设置了三种默认样式 CONNECTION_STYLE、SHAPE_STYLE、FRAME_STYLE
  2. styles:样式处理函数,用来合并元素的颜色配置

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

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

3.3 Bpmn BaseViewer

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

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

同理, baseViewer.off 与 baseViewer._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

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

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

  1. Viewer:指向 bpmn.js 的 Viewer 构造函数地址
  2. NavigatedViewer:指向 bpmn.js 的 NavigatedViewer 构造函数地址
  3. _interactionModules:键盘、鼠标等互动模块,包含 KeyboardMoveModule, MoveCanvasModule, TouchModule, ZoomScrollModule,均来自 diagram-js/lib/features
  4. _modelingModules:核心的建模工具模块,包含用来更新元素实例属性的 ModelingModule、元素上下文菜单 ContextPadModule、元素选择器侧边栏 PaletteModule 等
  5. _modules:合并了 Viewer.prototype._modules、_interactionModules、_modelingModules 之后的插件模块配置数组

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

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

Bpmn.js 进阶指南之原理分析与模块改造_第2张图片

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

4. Properties Panel

在 bpmn-js-properties-Panel 的 1.x 版本进行了颠覆性的更新,不仅重写了 UI 界面,1.x 版本之前的部分 API 和属性编辑栏构造函数都进行了重写,并将属性栏 DOM 构建与更新方式改写为 React JSX Hooks 与 Components 的形式,迁移到了 @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
  ]
});

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

Bpmn.js 进阶指南之原理分析与模块改造_第3张图片

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

  1. 必须引入 properties-panel.css 样式文件
  2. new Modeler() 时,必须传入配置项 propertiesPanel,并设置 parent 属性,用来指定侧边栏挂载的 DOM 节点
  3. additionalModules 需要同时引入 BpmnPropertiesPanelModule 与 BpmnPropertiesProviderModule ,否则不能正常使用。

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

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

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

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

  1. Id, Name 和 Documentation 属性,以及 Process 节点或者具有 processRef 定义的 Participant 节点特有的 isExecutable 属性
  2. 具有 “特殊事件定义” 的事件节点(例如 StartEvent, EndEvent, BoundaryEvent 节点等),可以配置的 Message, Error, Singal 等
  3. 具有 “多实例定义” 的任务类型节点,可以配置的 MultiInstance 属性(又分为 LoopCardinality 和 CompletionCondition)

4.2 BpmnPropertiesPanelModule, BpmnPropertiesPanel 与 PropertiesProviderModule

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 上
  2. diagram.destroy:在画布销毁时,将面板节点从 _container.parentNode 移除
  3. root.added:在根节点创建完成后,调用 _render() 方法,创建一个 BpmnPropertiesPanel 组件并渲染
4.2.2 BpmnPropertiesPanel 组件

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

  1. 通过 EventBus 实例来设置 selection.changed, elements.changed, propertiesPanel.providersChanged, elementTemplates.changed, root.added 几个事件的监听函数,根据选中元素变化来更新当前状态。
  2. 通过 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(提供异步控制属性、监听器配置、扩展属性、条件配置等)
  2. moddleExtensions 配置属性 camunda: camundaModdleDescriptors,用来解析与识别 camunda 流程引擎配置的特殊业务属性以及属性关联格式等。

Bpmn.js 进阶指南之原理分析与模块改造_第4张图片

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

4.4 Custom Properties Panel

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

对于这个问题,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的图片,可能字段不一样):

Bpmn.js 进阶指南之原理分析与模块改造_第5张图片

5. Toolbar

在画布与属性面板都创建好之后,我们就得到了一个完整的流程图编辑器了。

Bpmn.js 进阶指南之原理分析与模块改造_第6张图片

但是,这个模式下的编辑器没有绑定键盘快捷键,也没有导入导出的按钮和入口,并且也不能支持一键对齐等等功能。所以我们可以在此基础上,实现一个工具栏,来优化用户体验。

5.1 Import And Export

导入

首先,我们先实现文件导入的功能。利用 Modeler 实例本身的 importXML(xmlString) 的方法,可以很简单的完成导入,只需要创建一个 input 和一个 button 即可。

通过 button 的点击事件来模拟文件选择 input 的点击来触发文件选择,在确认文件选取之后初始化一个 FileReader 来读取数据并渲染。

这里使用的组件库是 naive ui

import { defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
import modeler from '@/store/modeler'

const Imports = defineComponent({
  name: 'Imports',
  setup() {
    const modelerStore = modeler()
    const importRef = ref(null)

    const openImportWindow = () => {
      importRef.value && importRef.value.click()
    }

    const changeImportFile = () => {
      if (importRef.value && importRef.value.files) {
        const file = importRef.value.files[0]
        const reader = new FileReader()
        reader.readAsText(file)
        reader.onload = function () {
          const xmlStr = this.result
          modelerStore.getModeler!.importXML(xmlStr as string)
        }
      }
    }

    return () => (
      
        
          打开文件
        
        
      
    )
  }
})

export default Imports

导出

至于文件导出的功能,官方在 BaseViewer 的原型上就提供了 saveXML 和 saveSVG 这两个方法,分别用来获取 xml 字符串与 svg 渲染结果。

import { defineComponent } from 'vue'
import { NButton, NPopover } from 'naive-ui'
import { downloadFile, setEncoded } from '@/utils/files'
import modeler from '@/store/modeler'

const Exports = defineComponent({
  name: 'Exports',
  setup() {
    const moderlerStore = modeler()
    // 下载流程图到本地
    /**
     * @param {string} type
     * @param {*} name
     */
    const downloadProcess = async (type: string, name = 'diagram') => {
      try {
        const modeler = moderlerStore.getModeler
        // 按需要类型创建文件并下载
        if (type === 'xml') {
          const { err, xml } = await modeler!.saveXML()
          // 读取异常时抛出异常
          if (err) {
            console.error(`[Process Designer Warn ]: ${err.message || err}`)
          }
          const { href, filename } = setEncoded(type.toUpperCase(), name, xml!)
          downloadFile(href, filename)
        } else {
          const { err, svg } = await modeler!.saveSVG()
          // 读取异常时抛出异常
          if (err) {
            return console.error(err)
          }
          const { href, filename } = setEncoded('SVG', name, svg!)
          downloadFile(href, filename)
        }
      } catch (e: any) {
        console.error(`[Process Designer Warn ]: ${e.message || e}`)
      }
    }

    const downloadProcessAsXml = () => {
      downloadProcess('xml')
    }
    const downloadProcessAsSvg = () => {
      downloadProcess('svg')
    }

    return () => (
       (
            
              导出为...
            
          ),
          default: () => (
            
导出为XML 导出为SVG
) }} >
) } }) export default Exports
// 根据所需类型进行转码并返回下载地址
export function setEncoded(type: string, filename: string, data: string) {
  const encodedData: string = encodeURIComponent(data)
  return {
    filename: `${filename}.${type.toLowerCase()}`,
    href: `data:application/${
      type === 'svg' ? 'text/xml' : 'bpmn20-xml'
    };charset=UTF-8,${encodedData}`,
    data: data
  }
}

// 文件下载方法
export function downloadFile(href: string, filename: string) {
  if (href && filename) {
    const a: HTMLAnchorElement = document.createElement('a')
    a.download = filename //指定下载的文件名
    a.href = href //  URL对象
    a.click() // 模拟点击
    URL.revokeObjectURL(a.href) // 释放URL 对象
  }
}

5.2 Canvas Zoom

因为没有绑定键盘事件,所以当前情况下想通过键盘和鼠标滚轮来控制画布缩放层级也不行。

但是 diagram.js 的核心模块 Canvas,就提供了画布的相关控制方法,我们可以通过 Canvas 的实例来实现对画布的控制。

import { defineComponent, ref } from 'vue'
import { NButton, NButtonGroup, NPopover } from 'naive-ui'
import LucideIcon from '@/components/common/LucideIcon.vue'
import EventEmitter from '@/utils/EventEmitter'
import type Modeler from 'bpmn-js/lib/Modeler'
import type Canvas from 'diagram-js/lib/core/Canvas'
import { CanvasEvent } from 'diagram-js/lib/core/EventBus'

const Scales = defineComponent({
  name: 'Scales',
  setup() {
    const currentScale = ref(1)
    let canvas: Canvas | null = null

    EventEmitter.on('modeler-init', (modeler: Modeler) => {
      canvas = modeler.get('canvas')
      currentScale.value = canvas.zoom()
      modeler.on('canvas.viewbox.changed', ({ viewbox }: CanvasEvent) => {
        currentScale.value = viewbox.scale
      })
    })

    const zoomOut = (newScale?: number) => {
      currentScale.value = newScale || Math.floor(currentScale.value * 100 - 0.1 * 100) / 100
      zoomReset(currentScale.value)
    }

    const zoomIn = (newScale?: number) => {
      currentScale.value = newScale || Math.floor(currentScale.value * 100 + 0.1 * 100) / 100
      zoomReset(currentScale.value)
    }

    const zoomReset = (newScale: number | string) => {
      canvas && canvas.zoom(newScale, newScale === 'fit-viewport' ? undefined : { x: 0, y: 0 })
    }

    return () => (
      
         '缩小视图',
            trigger: () => (
               zoomOut()}>
                
              
            )
          }}
        >
         '重置缩放',
            trigger: () => (
               zoomReset('fit-viewport')}>
                
                  {Math.floor(currentScale.value * 10) * 10 + '%'}
                
              
            )
          }}
        >
         '放大视图',
            trigger: () => (
               zoomIn()}>
                
              
            )
          }}
        >
      
    )
  }
})

export default Scales

5.3 Command Stack

撤销恢复个人觉得是最简单的封装之一,毕竟 CommandStack 本身就记录了相关的图形操作以及属性更新。

import { defineComponent } from 'vue'
import { NButton, NButtonGroup, NPopover } from 'naive-ui'
import EventEmitter from '@/utils/EventEmitter'
import type Modeler from 'bpmn-js/lib/Modeler'
import type CommandStack from 'diagram-js/lib/command/CommandStack'
import { createNewDiagram } from '@/utils'
import LucideIcon from '@/components/common/LucideIcon.vue'

const Commands = defineComponent({
  name: 'Commands',
  setup() {
    let command: CommandStack | null = null

    EventEmitter.on('modeler-init', (modeler: Modeler) => {
      command = modeler.get('commandStack')
    })

    const undo = () => {
      command && command.canUndo() && command.undo()
    }

    const redo = () => {
      command && command.canRedo() && command.redo()
    }

    const restart = () => {
      command && command.clear()
      createNewDiagram()
    }

    return () => (
      
         '撤销',
            trigger: () => (
              
                
              
            )
          }}
        >
         '恢复',
            trigger: () => (
              
                
              
            )
          }}
        >
         '擦除重做',
            trigger: () => (
              
                
              
            )
          }}
        >
      
    )
  }
})

export default Commands

5. Module Configuration

在进行深度自定义之前,这里先介绍 bpmn.js Modeler 本身默认引用的 Modules 的一些配置项。

5.1 BpmnRenderer Configuration

控制画布区域的元素渲染

  1. defaultFillColor:元素填充色,例如任务节点中间的空白部分的填充色,默认为 undefined
  2. defaultStrokeColor:元素边框颜色,也可以理解为路径类元素的颜色,默认为 undefined,显示为黑色
  3. defaultLabelColor:Label 标签字体颜色,默认为 undefined,显示为黑色

可以通过以下方式更改:

const modeler = new Modeler({
    container: 'xx',
    bpmnRenderer: {
        defaultFillColor: '#eeeeee',
        defaultStrokeColor: '#2a2a2a',
        defaultLabelColor: '#333333'
    }
})

5.2 TextRenderer Configuration

控制画布区域的文字渲染

  1. fontFamily: 文字字体,默认为 'Arial, sans-serif'
  2. fontSize: 文字大小,默认 12px
  3. fontWeight: 文字粗细,默认为 'normal'
  4. lineHeight: 文本行高,默认为 1.2
  5. size: 生成的文本标签的大小,默认为 { width: 150, height: 50 }
  6. padding: 文本标签内间距,默认为 0
  7. style: 文本标签其他 css 样式
  8. align: 内部文本对齐方式,默认为 center-top

可以通过传入配置项 textRenderer: {} 更改

5.3 ContextPad Configuration

控制元素的上下文菜单位置与大小缩放

  1. autoPlace:是否调用 AutoPlace 模块来实现新元素创建时自动定位,默认为 undefined,如果配置该属性并设置为 false 的话,在利用 contextPad 创建新元素时需要手动选择新元素位置
  2. scale:缩放的限制范围,默认为 { min: 1.0, max: 1.5 }

可以通过传入配置项 contextPad: {} 更改

5.4 Canvas Configuration

控制画布区域大小与更新频率

  1. deferUpdate: 是否配置延迟更新画布改变,默认为 undefined,如果配置该属性并设置为 false 的话,则会即时更新画布显示(会消耗大量资源)
  2. width: 宽度,默认为 ‘100%’
  3. height: 高度,默认为 ‘100%’

5.5 Keyboard Configuration

键盘事件的绑定对象

  1. bindTo: 设置绑定对象,默认为 undefined,一般会配置为 document 或者 window

可以通过传入配置项 keyboard: {} 配置,默认快捷键列表如下:

Bpmn.js 进阶指南之原理分析与模块改造_第7张图片

5.6 AutoScroll Configuration

鼠标焦点移动到画布边框位置时开启画布滚动,主要配置触发区域与滚动设置

  1. scrollThresholdIn:触发滚动的边界距离最大值,默认为 [ 20, 20, 20, 20 ]
  2. scrollThresholdOut:触发滚动的边界距离最小值,默认为 [ 0, 0, 0, 0 ]
  3. scrollRepeatTimeout:滚动间隔,默认为 15 ms
  4. scrollStep:滚动步长。默认为 6

可以通过传入配置项 autoScroll: {} 配置

5.7 ZoomScroll Configuration

鼠标滚轮缩放的配置

  1. enabled: 是否启动鼠标滚轮缩放功能,默认为 undefined,如果配置该属性并设置为 false 的话,则会禁用鼠标滚动缩放功能
  2. scale: 缩放倍率,默认为 0.75

可以通过传入配置项 zoomScroll: {} 配置

当然,这部分只是 bpmn.js 与 diagram.js 内部的插件模块提供的配置项,在我们的自定义模块也可以通过依赖 config 来配置更多的可用配置项,使 Modeler 更加灵活

下面,进行 Modeler 的核心插件自定义的讲解

6. Custom Element And Properties

在第四节 Properties Panel 中,大概讲解了自定义元素属性的方式。参照 Bpmn-js自定义描述文件说明-掘金 和 bpmn-io/moddle,这里再重新说明一下。

一个 moddleExtension 描述文件的格式为 json,或者是一个可以导出 json 对象的 js/ts 文件,该描述文件(对象)包含以下几个属性:

  1. name: 该部分扩展的名称,一般根据流程引擎来命名,字符串格式
  2. uri: 统一资源标识符,一般是一个地址字符串
  3. prefix: 属性或者元素统一前缀,小写字符串格式
  4. xml: 格式转换时的配置,一般用来配置 { "tagAlias": "lowerCase" }, 表示会将标签名转换为小写驼峰,可省略
  5. types: 核心部分,用来声明元素和属性,以及扩展原有属性等,对象数组格式
  6. enumerations: 枚举值定义部分,可以用来定义 types 中某个配置属性的可选值
  7. associations: 组合定义,暂时作为保留配置

types 作为核心部分,通过一个特定格式的对象数组来描述元素与属性之间的关系,以及每个属性的类型和位置。

type Type = {
    name: string
    extends?: string[]
    superClass?: string[]
    isAbstract?: boolean
    meta?: TypeMeta
    properties: TypeProperty[]
}

type TypeMeta = {
    allowedIn?: string[] | ['*']
}

type TypeProperty = {
    name: string
    type: string // 支持 boolean, string, number 这几个简单类型,此时可以设置 default 默认值;也支持自定义元素作为属性值
    isAttr?: boolean // 是否作为一个 xml 标签属性,为 true 时会将该属性值转换为 boolean, string, number 简单类型,对象等类型会转为 '[object Object]'
    isBody?: boolean // 是否将值插入到 xml 标签内部作为 content,转换方式与 isAttr 一致,但是这两个属性不能共存
    isMany?: boolean // 是否支持多个属性,一般这种情况下 type 是一个继承自 Element 的自定义元素,会将子元素插入到 xml 标签的 content 区域中,默认为 false 
    isReference?: boolean // 是否将 type 指定的自定义元素的 id 作为值,体现在 xml 上时该属性为对应的元素 id 字符串,但是通过 modeler 解析后该属性指向对应的元素实例
    redefines?: string // 重定义继承元素的某个属性配置,通常与 superClass 配合使用,例如 "redefines": "bpmn:StartEvent#id"
    default?: string | number | boolean
}
example = {
    // ...
    // 表示创建属性或者元素时,需要增加的前缀,比如创建 ExampleElement 需要 moddle.create('ex:ExampleElement', {})
    prefix: 'ex',
    types: [
        {
            name: 'ExampleElement',
            /**
             * 继承 Element 的默认属性,表示可以创建一个 xml 元素标签更新到 xml 数据中
             * 该继承关系类似 js 原型链,如果继承的元素最终都继承自 Element,那么该属性也可以生成 xml 元素标签
             */
            superClass: ['Element'],
            /**
             * 与 superClass 相反,extends 表示扩展原始元素的配置,并不代表继承。
             * 使用 extends 之后,该类型定义的 properties 最终都会体现到原始元素上,展示方式为 ex:propertyName='xxx' 
             * (这只代表配置的 propertyName 是一个简单属性,如果是自定义属性的话,需要根据属性类型来区分)
             */
            extends: ['bpmn:StartEvent'],
            /**
             * 设置 allowedIn 来定义该属性可以插入到哪些元素内部,可以设置 ['*'] 表示任意元素
             */
            meta: {
                allowedIn: ['bpmn:StartEvent']
            },
            properties: [
                {
                    name: 'exProp1',
                    type: 'String', 
                    default: '2'
                }
            ]
        }
    ]
}

注意:superClass 与 extends 不能同时使用,两者的区别也可以查看官方回复 issue-21

完整演示见 properties-panel-extension, bpmn-js-example-custom-elements

7. Custom Renderer, Palette and ContextPad

关于如何扩展原始 Renderer, Palette (这里其实应该是 PaletteProvider) 和 ContextPad (这里其实应该是 ContextPadProvider),霖呆呆和 bpmn 官方都给出了示例。

  1. 官方示例/bpmn-js-example-custom-elements
  2. 霖呆呆的文档地址 全网最详bpmn.js教材目录 和示例仓库 bpmn-vue-custom

这里针对核心部分简单讲解一下。

7.1 Renderer

重新自定义元素的渲染逻辑,可以区分为 “部分自定义” 与 “完全自定义”,“部分自定义” 又可以分为 “自定义新增元素类型渲染” 和 “自定义原始类型渲染”,核心逻辑其实就是改变 Renderer 构造函数上的 drawShape 方法。

declare class BpmnRenderer extends BaseRenderer {
    constructor(config: Object, eventBus: EventBus, styles: Styles, pathMap: PathMap, canvas: Canvas, textRenderer: TextRenderer, priority?: number)

    handlers: Record<string, RendererHandler>
    _drawPath(parentGfx: SVGElement, element: Base, attrs?: Object): SVGElement
    _renderer(type: RendererType): RendererHandler
    getConnectionPath<E extends Base>(connection: E): string
    getShapePath<E extends Base>(element: E): string
    canRender<E extends Base>(element: E): boolean
    drawShape<E extends Base>(parentGfx: SVGElement, element: E): SVGRectElement
}

原生 BpmnRenderer 继承自抽象函数 BaseRenderer,通过 drawShape 方法来绘制 svg 元素,之后添加到 canvas 画布上。但是 drawShape 的核心逻辑其实就是根据 element 元素类型来调用 handler[element.type]() 实现元素绘制的。

BpmnRenderer.prototype.drawShape = function(parentGfx, element) {
  var type = element.type;
  var h = this._renderer(type);
  return h(parentGfx, element);
};

在 “自定义新增元素类型渲染” 或者 “对原始 svg 元素增加细节调整” 的时候,可以通过继承 BaseRenderer 之后实现 drawShape 方法来实现。

class CustomRenderer extends BaseRenderer {
    constructor(eventBus: EventBus, bpmnRenderer: BpmnRenderer) {
        super(eventBus, 2000);
        this.bpmnRenderer = bpmnRenderer;
    }
    drawShape(parentNode: SVGElement, element: Base) {
        // 处理自定义元素
        if (is(element, 'ex:ExampleElement')) {
            const customElementsSVGPath = '这里是自定义元素的 svg path 路径'
            const path = svgCreate('path')
            svgAttr(path, { d: customElementsSVGPath })
            svgAttr(path, attrs)
            svgAppend(parentGfx, path)
            // 需要 return 该 svg 元素
            return path
        }
        // 调用 bpmnRenderer.drawShape 来实现原始元素的绘制
        const shape = this.bpmnRenderer.drawShape(parentNode, element);
        // 对原有元素 UserTask 增加细节调整
        if (is(element, 'bpmn:UserTask')) {
            svgAttr(shape, { fill: '#eee' });
        }
        return shape
    }
}
CustomRenderer.$inject = [ 'eventBus', 'bpmnRenderer' ];

// 使用时,需要注意大小写
export default {
    __init__: ['customRenderer'],
    customRenderer: ['type', CustomRenderer]
}

当然,上面这种方式基本上很难满足大部分的自定义渲染需求,毕竟有时候需要的不是给原始元素增加细节,而是需要将整个元素全部重新实现(UI同事的审美通常都比我们要“强”不少),虽然可以在调用 this.bpmnRenderer.drawShape() 来绘制剩余类型之前,我们还可以增加很多个元素的处理逻辑,但这样无疑会使得这个方法变得异常臃肿,而且很难通过配置来实现不同的元素样式。

**所以,我们可以在 BpmnRenderer 的源码基础上,重新实现一个 RewriteRenderer。**不过这部分代码有点长(2000+行),这里暂时就不放出来了

7.2 Palette 与 ContextPad

针对这两个模块,自定义的逻辑其实与 Renderer 类似,只不过是对应的方法不一样。

CustomPaletteProvider 需要依赖 Palette 实例,并实现 getPaletteEntries 方法来将自定义部分的内容插入到 palette 中。

class CustomPaletteProvider {
    // ... 需要定义 _palette 等属性
    constructor(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect) {
        this._palette = palette
        this._create = create
        this._elementFactory = elementFactory
        this._spaceTool = spaceTool
        this._lassoTool = lassoTool
        this._handTool = handTool
        this._globalConnect = globalConnect
        
        // 注册该 Provider
        palette.registerProvider(this);
    }
    getPaletteEntries() {
        return {
            'custom-palette-item': {
                group: 'custom', // 分组标志,group 值相同的选项会出现在同一个区域
                className: 'custom-palette-icon-1',
                title: '自定义选项1',
                action: {
                    click: function (event) {
                        alert(1)
                    },
                    dragstart: function (event) {
                        alert(2)
                    }
                }
            },
            'tool-separator': {
                group: 'tools',
                separator: true // 指定该配置是显示一个分割线
            },
        }
    }
}

export default {
    __init__: ['customPaletteProvider'],
    // 如果要覆盖原有的 paletteProvider, 可以写为 paletteProvider: ['type', CustomPaletteProvider],__init__ 属性此时可以省略
    customPaletteProvider: ['type', CustomPaletteProvider]
}

CustomContextPadProvider 作为元素选中时会提示的上下文菜单,与 CustomPaletteProvider 的实现逻辑基本一致,但是需要注意 AutoPlace 模块的引用。

class CustomContextPadProvider {
    constructor(
        config: Object,
        injector: Injector,
        eventBus: EventBus,
        contextPad: ContextPad,
        modeling: Modeling,
        elementFactory: ElementFactory,
        connect: Connect,
        create: Create,
        popupMenu: PopupMenu,
        canvas: Canvas,
        rules: Rules
    ) {
        if (config.autoPlace !== false) {
            this._autoPlace = injector.get('autoPlace', false);
        }
        contextPad.registerProvider(this);
    }

    getContextPadEntries(element: Base) {
        const actions: Record<string, any> = {}

        const appendUserTask = (event: Event, element: Shape) => {
            const shape = this._elementFactory.createShape({ type: 'bpmn:UserTask' })
            this._create.start(event, shape, {
                source: element
            })
        }

        const append = this._autoPlace
            ? (event: Event, element: Shape) => {
                const shape = this._elementFactory.createShape({ type: 'bpmn:UserTask' })
                this._autoPlace.append(element, shape)
            }
            : appendUserTask

        // 添加创建用户任务按钮
        actions['append.append-user-task'] = {
            group: 'model',
            className: 'bpmn-icon-user-task',
            title: '用户任务',
            action: {
                dragstart: appendUserTask,
                click: append
            }
        }

        // 添加一个与edit一组的按钮
        actions['enhancement-op-1'] = {
            group: 'edit',
            className: 'enhancement-op',
            title: '扩展操作1',
            action: {
                click: function (e: Event) {
                    alert('点击 扩展操作1')
                }
            }
        }

        // 添加一个新分组的自定义按钮
        actions['enhancement-op'] = {
            group: 'enhancement',
            className: 'enhancement-op',
            title: '扩展操作2',
            action: {
                click: function (e: Event) {
                    alert('点击 扩展操作2')
                }
            }
        }

        return actions
    }
}

export default {
    __init__: ['customContextPadProvider'],
    // 如果要覆盖原有的 ContextPadProvider, 可以写为 contextPadProvider: ['type', CustomContextPadProvider],__init__ 属性此时可以省略
    customContextPadProvider: ['type', CustomContextPadProvider]
}

8. Replace Options (PopupMenu)

这部分功能默认是通过 ContextPad 中间的小扳手 来触发的,主要是用来更改当前元素的类型。很多小伙伴反馈说其实里面的很多选项都不需要,这里对如何实现该部分更改进行说明。

  1. 通过 css 隐藏 dev.djs-popup-body 节点下的多余节点,因为不同的元素类型有不同的 css class 类名,可以通过类名设置 display: none 隐藏
  2. 直接修改 ReplaceOptions 的数据
import { TASK } from 'bpmn-js/lib/features/replace/ReplaceOptions';

// 移除多余的选项
GATEWAY.splice(2, GATEWAY.length);

// 注意需要在 new Modeler 之前,并且这种方式不支持 cdn 引入
  1. 修改 ReplaceMenuProvider, 这里与自定义 ContextPadProvider 的逻辑类似。
// 源码位置见 bpmn-js/lib/features/popup-menu/ReplaceMenuProvider.js

import * as replaceOptions from '../replace/ReplaceOptions';

class CustomReplaceMenuProvider extends ReplaceMenuProvider {
    constructor(bpmnFactory, popupMenu, modeling, moddle, bpmnReplace, rules, replaceMenuProvider, translate) {
        super(bpmnFactory, popupMenu, modeling, moddle, bpmnReplace, rules, translate);
        this.register();
    }

    getEntries(element) {
        if (!rules.allowed('shape.replace', { element: element })) {
            return [];
        }
        const differentType = isDifferentType(element);
        if (is(elemeny, 'bpmn:Gateway')) {
            entries = filter(replaceOptions.GATEWAY.splice(2, replaceOptions.GATEWAY.length), differentType);
            return this._createEntries(element, entries);
        }
        return replaceMenuProvider.getEntries(element)
    }
}
ReplaceMenuProvider.$inject = [
    'bpmnFactory',
    'popupMenu',
    'modeling',
    'moddle',
    'bpmnReplace',
    'rules',
    'replaceMenuProvider',
    'translate'
];

9. 自己实现 Properties Panel

虽然根据 第 4.4 小节可以知道,我们可以通过自定义一个属性面板分组,来插入到原生的 Bpmn Properties Panel 中,但是这样实现,第一是基本不符合国内的审美,第二就是写法太复杂,第三则是对控制参数传递的实现十分困难。既然现在的 MVVM 框架都支持 props 数据传递来控制参数改变,并且有很多精美的开源组件库,那可不可以自己实现一个属性面板呢?

答案是当然可以的。

bpmn.js 的属性更新操作都是通过 modeling.updateProperties 与 modeling.updateModdlePropertis 这两个 api 来实现的,实现一个属性面板的核心逻辑就在于监听当前选中元素的变化,来控制对应的属性面板的渲染;并且对属性面板的输出结果通过以上两个 api 更新到元素实例上,从而实现完整的属性更新流程。

后续以 Flowable 流程引擎为例进行讲解。

9.1 第一步:设置监听事件寻找选中元素

如何设置当前的选中元素来控制属性面板的渲染,根据第 4.2 小节,可以结合 BpmnPropertiesPanel 组件的写法,通过监听 selection.changed, elements.changed, root.added(或者 import.done) 几个事件来设置当前元素。这里大致解释一下为什么是这几个事件:

  1. root.added(或者 import.done):在根元素(Process节点)创建完成(或者流程导入结束)时,默认是没有办法通过 selection 模块拿到选中元素,所以我们可以默认设置根元素为选中元素来渲染属性面板
  2. selection.changed:这个事件在鼠标点击选中事件改变时会触发,默认返回一个选中元素数组(可能为空),这里我们取数组第一个元素(为空时设置成根元素)来渲染属性面板
  3. elements.changed:这个事件则是为了控制属性面板的数据回显,因为数据有可能是通过其他方式更新了属性

我们先创建一个 PropertiesPanel 组件:

import { defineComponent, ref } from 'vue'
import debounce from 'lodash.debounce'
import EventEmitter from '@/utils/EventEmitter'
import modelerStore from '@/store/modeler'

const PropertiesPanel = defineComponent({
    setup() {
        // 这里通过 pinia 来共享当前的 modeler 实例和选中元素
        const modeler = modelerStore()
        const penal = ref(null)
        const currentElementId = ref(undefined)
        const currentElementType = ref(undefined)

        // 在 modeler 实例化结束之后在创建监听函数 (也可以监听 modeler().getModeler 的值来创建)
        EventEmitter.on('modeler-init', (modeler) => {
            // 导入完成后默认选中 process 节点
            modeler.on('import.done', () => setCurrentElement(null))
            // 监听选择事件,修改当前激活的元素以及表单
            modeler.on('selection.changed', ({ newSelection }) => setCurrentElement(newSelection[0] || null))
            // 监听元素改变事件
            modeler.on('element.changed', ({ element }) => {
                // 保证 修改 "默认流转路径" 等类似需要修改多个元素的事件发生的时候,更新表单的元素与原选中元素不一致。
                if (element && element.id === currentElementId.value) setCurrentElement(element)
            })
        })

        // 设置选中元素,更新 store;这里做了防抖处理,避免重复触发(可以取消)
        const setCurrentElement = debounce((element: Shape | Base | Connection | Label | null) => {
            let activatedElement: BpmnElement | null | undefined = element
            if (!activatedElement) {
                activatedElement =
                    modeler.getElRegistry?.find((el) => el.type === 'bpmn:Process') ||
                    modeler.getElRegistry?.find((el) => el.type === 'bpmn:Collaboration')

                if (!activatedElement) {
                    return Logger.prettyError('No Element found!')
                }
            }

            modeler.setElement(markRaw(activatedElement), activatedElement.id)
            currentElementId.value = activatedElement.id
            currentElementType.value = activatedElement.type.split(':')[1]
        }, 100)
        
        return () => (
) } })

9.2 第二步:判断元素类型和数据来控制属性面板

在获取到选中元素之后,我们需要根据元素类型来控制显示不同的属性面板组件(这里建议参考官方的属性面板的写法,将判断方法和属性值的更新读取拆分成不同的 hooks 函数)。

比如几个异步属性(asyncBefore, asyncAfter, exclusive),这几个属性只有在选中元素的 superClass 继承链路中有继承 flowable:AsyncCapable 才会体现。所以我们编写一个判断函数:

import { is } from 'bpmn-js/lib/util/ModelUtil'
export function isAsynchronous(element: Base): boolean {
  return is(element, 'flowable:AsyncCapable')
}

在 PropertiesPanel 组件中,就可以通过调用该函数判断是否显示对应部分的属性面板

import { defineComponent, ref } from 'vue'
const PropertiesPanel = defineComponent({
    setup() {
        // ...
        return () => (
            
{isAsynchronous(modeler.getActive!) && ( )}
) } }) export default PropertiesPanel

9.3 第三步:实现对应的属性面板更新组件

上一步,我们通过判断元素时候满足异步属性来显示了 ElementAsyncContinuations 组件,但是 ElementAsyncContinuations 组件内部如何实现元素的读取和更新呢?

具体包含哪些属性,可以查看 flowable.json

首先,我们先实现 ElementAsyncContinuations 组件,包含 template 模板和基础的更新方法。




这里基本实现了根据元素 id 的变化,来更新元素的异步属性配置,并且在属性面板的表单项发生改变时更新该元素的属性。

这里对几个属性的获取和更新方法提取了出来。

import { Base, ModdleElement } from 'diagram-js/lib/model'
import editor from '@/store/editor'
import modeler from '@/store/modeler'
import { is } from 'bpmn-js/lib/util/ModelUtil'

// only in element extends bpmn:Task
export function getACBefore(element: Base): boolean {
  return isAsyncBefore(element.businessObject, 'flowable')
}
export function setACBefore(element: Base, value: boolean) {
  const modeling = modeler().getModeling
  // overwrite the legacy `async` property, we will use the more explicit `asyncBefore`
  modeling.updateModdleProperties(element, element.businessObject, {
    [`flowable:asyncBefore`]: value,
    [`flowable:async`]: undefined
  })
}

export function getACAfter(element: Base): boolean {
  return isAsyncAfter(element.businessObject, 'flowable')
}
export function setACAfter(element: Base, value: boolean) {
  const prefix = editor().getProcessEngine
  const modeling = modeler().getModeling
  modeling.updateModdleProperties(element, element.businessObject, {
    [`flowable:asyncAfter`]: value
  })
}

export function getACExclusive(element: Base): boolean {
  return isExclusive(element.businessObject, 'flowable')
}
export function setACExclusive(element: Base, value: boolean) {
  const prefix = editor().getProcessEngine
  const modeling = modeler().getModeling
  modeling.updateModdleProperties(element, element.businessObject, {
    [`flowable:exclusive`]: value
  })
}

 helper
// 是否支持异步属性
export function isAsynchronous(element: Base): boolean {
  const prefix = editor().getProcessEngine
  return is(element, `flowable:AsyncCapable`)
}

// Returns true if the attribute 'asyncBefore' is set to true.
function isAsyncBefore(bo: ModdleElement, prefix: string): boolean {
  return !!(bo.get(`flowable:asyncBefore`) || bo.get('flowable:async'))
}

// Returns true if the attribute 'asyncAfter' is set to true.
function isAsyncAfter(bo: ModdleElement, prefix: string): boolean {
  return !!bo.get(`flowable:asyncAfter`)
}

// Returns true if the attribute 'exclusive' is set to true.
function isExclusive(bo: ModdleElement, prefix: string): boolean {
  return !!bo.get(`flowable:exclusive`)
}

这样,我们就得到了一个基础的属性面板。

当前模式只能在 id 更新时才更新数据,不是十分完美。建议在 element.changed 事件发生时通过 EventEmitter 来触发业务组件内部的数据更新。

9.4 复杂属性的更新

上一节提到的属性都是作为很简单的属性,可以直接通过 updateModdleProperties(element, moddleElement, { key: value}) 的形式来更新,不需要其他步骤。

但是如果这个属性不是一个简单属性,需要如何创建?这里我们以在 Process 节点下创建 ExecutionListener 为例。

首先,我们在 flowable.json 中查看 ExecutionListener 的属性配置。

{
  "name": "ExecutionListener",
  "superClass": ["Element"],
  "meta": {
    "allowedIn": [
      // ...
      "bpmn:Process"
    ]
  },
  "properties": [
    {
      "name": "expression",
      "isAttr": true,
      "type": "String"
    },
    {
      "name": "class",
      "isAttr": true,
      "type": "String"
    },
    {
      "name": "delegateExpression",
      "isAttr": true,
      "type": "String"
    },
    {
      "name": "event",
      "isAttr": true,
      "type": "String"
    },
    {
      "name": "script",
      "type": "Script"
    },
    {
      "name": "fields",
      "type": "Field",
      "isMany": true
    }
  ]
}

可以看到这个属性继承了 Element 属性,所以肯定可以创建一个 xml 标签;meta 配置里面表示它允许被插入到 Process 节点中。

但是 Process 节点的定义下并没有支持 ExecutionListener 属性的相关配置,所以我们接着查看 bpmn.json,发现也没有相关的定义。这时候怎么办呢?

我们仔细研究一下两个文件里面关于 Process 元素的配置:

// flowable.json
{
  "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
    }
  ]
}
// bpmn.json
{
  "name": "Process",
  "superClass": ["FlowElementsContainer", "CallableElement"],
  "properties": [
    // ...
  ]
}

// 向上查找 FlowElementsContainer
{
  "name": "FlowElementsContainer",
  "isAbstract": true,
  "superClass": ["BaseElement"],
  "properties": [
    //. ..
  ]
}

// 向上查找 BaseElement
{
  "name": "BaseElement",
  "isAbstract": true,
  "properties": [
    {
      "name": "id",
      "isAttr": true,
      "type": "String",
      "isId": true
    },
    {
      "name": "documentation",
      "type": "Documentation",
      "isMany": true
    },
    {
      "name": "extensionDefinitions",
      "type": "ExtensionDefinition",
      "isMany": true,
      "isReference": true
    },
    {
      "name": "extensionElements",
      "type": "ExtensionElements"
    }
  ]
}

// 接着查找 ExtensionDefinition 和 ExtensionElements
{
  "name": "ExtensionElements",
  "properties": [
    {
      "name": "valueRef",
      "isAttr": true,
      "isReference": true,
      "type": "Element"
    },
    {
      "name": "values",
      "type": "Element",
      "isMany": true
    },
    {
      "name": "extensionAttributeDefinition",
      "type": "ExtensionAttributeDefinition",
      "isAttr": true,
      "isReference": true
    }
  ]
}

这里可以找到 Process 节点继承的 BaseElement, 有定义 ExtensionElements,并且 ExtensionElements 的 values 属性支持配置多个 Element。所以这里大概就是我们需要关注的地方了。他们之间的大致关系如下:

BaseElement     (superClass)-->     FlowElementsContainer     (superClass)-->     Process
 ↓ hasProperty
extensionElements(ExtensionElements)
 ↓ hasProperty
values(Element[])
 ↓ hasProperty
Element         (superClass)-->     ExecutionListener

虽然 ExtensionElements 没有声明是继承的 Element 的,但是因为 values 属性是配置的多属性,所以也会在 xml 中插入一个 extensionElements 标签。

既然现在已经找到了这几个元素和属性直接的关系,那么如何给 Process 节点添加 ExecutionListener 就很明了了。

因为这些属性虽然会在 xml 上体现为一个标签,但是并不会显示在图形界面上,所以一般不能用 BpmnFactory 来创建。

这里我们可以通过 Moddle 模块来创建这类属性实例(包含自定义的其他属性也可以用这种方式)

const canvas = modeler.get<Canvas>('canvas');
const moddle = modeler.get<Moddle>('moddle');
const modeling = modeler.get<Modeling>('modeling');

// 1. 获取 Process 节点
const process: Base = canvas.getRootElement();
const businessObject = process.businessObject

// 2. 获取或者创建一个 ExtensionElements 并更新节点业务属性
let extensionElements: ModdleElement & ExtensionElements = businessObject.get('extensionElements')
if (!extensionElements) {
    extensionElements = moddle.create('bpmn:ExtensionElements', { values: [] })
    // 设置 $parent, 指向 业务节点实例的 businessObject
    extensionElements.$parent = process.businessObject
    // 将 extensionElements 更新到节点上
    modeling.updateModdleProperties(process, businessObject, { extensionElements })
}

// 3. 创建一个 ExecutionListener 并更新到 ExtensionElements 上
const listener = moddle.create(`flowable:ExecutionListener`, {
    // ... 这里是相关的属性
    // 如果是 Script, Field 这些属性类型,需要像创建 ExecutionListener 这样创建对应的 script, field 实例,并更新到 listener 上
})
listener.$parent = extensionElements
// 这里注意 values 数组里面需要把原来的数据填充进来
modeling.updateModdleProperties(element, extensionElements, {
    values: [...extensionElements.get('values'), listener]
})

上文说到更新元素属性可以通过 modeling.updateProperties 与 modeling.updateModdlePropertis 来处理,但是这两个方法有一点点细微差别。

updateProperties:接收两个参数 Element 和 properties,内部会获取当前 Element 的所有属性配置,进行以下操作:

  1. 比较 id 是否改变,如果改变则通过 elementRegistry.updateId 来更新索引表中的元素 Id,同时更新该对象的 Id 和对象对应的 DI 图形元素的 id
  2. 如果元素具有 default 属性(用于设置默认路径),则比较该属性的变化并更新
  3. 遍历 properties 对象,更新 element.businessObject 业务属性(如果 properties 中有 key 等于 DI 的,则会更新对应属性到图形配置属性上)
  4. 如果有 name 属性,或者发生了改变,则会更新 Element 对应的 Label 标签。
  5. 计算更新后的元素大小并重新调整位置

updateModdlePropertis:接收三个参数 Element, ModdleElement 和 properties,这个方法内部逻辑比较单一,通过遍历 properties 来读取 ModdleElement 的原始数据,之后再次遍历 properties 将配置的属性更新到 ModdleElement 中。

9.5 快速定位属性类型和更新方式

上面这种方式,需要对 moddleExtension 和 xml 规范比较熟悉才能比较快速找到需要的元素对应的逻辑关系,这种方式无疑耗时巨大。虽然我建议通过阅读 bpmn-js-peroperties-panel 的源码,但是可能很多小伙伴的时间也比较短,没有办法去仔细阅读。

所以这里介绍另外一种方式。

注意,这种方式最好找后端的朋友提供一个配置比较全面的xml,然后将这个 xml 导入到我们的项目中。 之后配置一下 element.click 点击事件的监听,将回调参数打印一下。其中 element.businessObject 的值大致如下:

Bpmn.js 进阶指南之原理分析与模块改造_第8张图片

因为浏览器控制台打印对象时,会提示该对象对应的构造函数名称,我们可以通过这个来判断该使用什么方式。

比如上图中打印的 element.businessObject 提示的类型是 ModdleElement,所以才可以作为 updateModdleProperties 的第二个参数。

后续的 extensionElements 和 extensionElements.values[0] 都是 ModdleElement,所以这种类型的数据都需要通过 moddle.create 来创建,其中以 $ 符号开头的属性更新或者创建的时候可以忽略,主要是用来表示这个 ModdleElement 实例具体属于那种自定义类型,在 moddle.create 创建时第一个参数就是这个 $type 属性。

在创建好对应的属性实例之后,一步一步更新到 element.businessObject 上就大功告成啦。

这里还有一点需要注意:如果 flowable.json 或者 bpmn.json 中定义了某个自定义元素的属性 isReference: true (例如元素的默认流转路径 default),这个体现在 xml 中是作为自定义元素标签的一个 attribute 属性,但是在控制台打印出来则是一个指向该 id 对应的元素的 businessObject 对象,这里需要特别注意。

并且在更新该属性的时候,也需要设置为 default: element ,不能直接使用 default: 'elementId'。

10. 自己实现 Palette

因为原生的 Palette 模块不支持手风琴式操作,想显示元素类型名称或者改变面板显示效果,都需要进行比较大的改动。如果要配合自定义的 Renderer 渲染方式,可能改动更大,这个时候就需要我们自己来实现一个 Palette 组件了。

首先,我们先研究一下 bpmn.js 的 PaletteProvider 里面的显示入口配置(这里省略其他内容,主要查看 getPaletteEntries 的返回数据)。

function createAction(type, group, className, title, options) {
    function createListener(event) {
        var shape = elementFactory.createShape(assign({ type: type }, options));
        if (options) {
            var di = getDi(shape);
            di.isExpanded = options.isExpanded;
        }
        create.start(event, shape);
    }
    var shortType = type.replace(/^bpmn:/, '');
    return {
        group: group,
        className: className,
        title: title || translate('Create {type}', { type: shortType }),
        action: {
            dragstart: createListener,
            click: createListener
        }
    };
}
PaletteProvider.prototype.getPaletteEntries = function(element) {
    // ...
    return {
        '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')
        )
        // ...
    }
}

通过以上代码,可以发现 PaletteProvider 里面的按钮入口主要实现两个类型的功能:

  1. 开启其他工具模块
  2. 创建对应类型的元素

既然已经明白了里面的功能了逻辑,那么实现这样的功能就比较简单了

import { defineComponent } from 'vue'
import { assign } from 'min-dash'
import modelerStore from '@/store/modeler'

const Palette = defineComponent({
    name: 'Palette',
    setup() {
        const store = modelerStore()
        const createElement = (ev: Event, type: string, options?: any) => {
          const ElementFactory: ElementFactory = store.getModeler!.get('elementFactory')
          const create: Create = store.getModeler!.get('create')
          const shape = ElementFactory.createShape(assign({ type: `bpmn:${type}` }, options))
          if (options) {
            shape.businessObject.di.isExpanded = options.isExpanded
          }
          create.start(ev, shape)
        }
        
        const toggleTool = (ev: Event, toolName: string) => {
            const tool = store.getModeler!.get(toolName)
            // 工具基本上都有 toggle 方法,用来改变启用状态
            tool?.toggle()
        }
    
    return () => (
      
工具部分
toggleTool(e, 'handTool')} > 开始
createElement(e, 'StartEvent')} > 开始
任务部分 网关部分
) } }) export default Palette

11. 官方的增强版元素创建与元素更新插件

在 bpmn.js 9.0 版本之后,官方提供了一个增强版的元素选择器,对 PaletteProvider 和 ContextPad 触发的 PopupMenu (ReplaceProvider) 进行了二次配置。具体使用效果如下:

Bpmn.js 进阶指南之原理分析与模块改造_第9张图片
Bpmn.js 进阶指南之原理分析与模块改造_第10张图片

这个插件与使用的流程引擎无关,都可以使用。不过需要注意 bpmn.js 的版本

这个插件的主要依赖是 @bpmn-io/element-template-chooser。

我们先进入 element-template-chooser 插件的入口文件。

import ElementTemplateChooserModule from './element-template-chooser';
import ChangeMenuModule from './change-menu';

export default {
  __depends__: [
    ElementTemplateChooserModule,
    ChangeMenuModule
  ]
};

这里可以看到默认是需要依赖两个插件 ElementTemplateChooserModule 和 ChangeMenuModule。

export default function ChangeMenu(injector, eventBus) {
    // ...
}
ChangeMenu.$inject = [
    'injector',
    'eventBus'
];

export default function ElementTemplateChooser(
    config,
    eventBus,
    elementTemplates,
    changeMenu) {
    // ...
}
ElementTemplateChooser.$inject = [
    'config.connectorsExtension',
    'eventBus',
    'elementTemplates',
    'changeMenu'
];

这里需要特别注意,ElementTemplateChooserModule 会依赖 elementTemplates 模块,所以在实例化 Modeler 时也需要引用该插件。

不过因为这个部分会影响 Palette 和 PopupMenu,所以我们根据官方示例代码使用即可(这里可以不需要 zeebe 模块)。

import BpmnModeler from 'bpmn-js/lib/Modeler';
import {
    BpmnPropertiesPanelModule,
    BpmnPropertiesProviderModule,
    ZeebePropertiesProviderModule,
    CloudElementTemplatesPropertiesProviderModule
} from 'bpmn-js-properties-panel';
import ElementTemplateChooserModule from '@bpmn-io/element-template-chooser';

const modeler = new BpmnModeler({
  container: '#canvas',
  additionalModules: [
    ElementTemplateChooserModule,
    BpmnPropertiesPanelModule,
    BpmnPropertiesProviderModule,
    CloudElementTemplatesPropertiesProviderModule
  ],
  exporter: {
    name: 'element-template-chooser-demo',
    version: '0.0.0'
  }
});

你可能感兴趣的:(javascript,开发语言,ecmascript,vue)

  • vue+react面试题 宇宙超级无敌暴龙嗜血战士 vue.js前端javascript
    一、响应式原理vue2响应式的原理是借助数据劫持和发布订阅者模式1、数据劫持:目的:能够感知到数据的改变。数据劫持是:使用ES5的Object.defineProperty()。把data配置项中的所有数据进行遍历,转成setter和getter(或者说,给每个属性增加set和get函数)既就是:访问器属性。2、发布订阅者模式:目的:当数据改变时,(直接和间接)使用该数据的模板处都会有相应的改变(
  • vue项目做导入excel(通过base64)
    最近项目的需求,记录下笔记要求:1..xls后缀名文件2.文件不超过10M3.转成base64传给后端导入excel//点击导入exceluploadFile(res){letfile=res.filethis.getBase64(file).then(baseFile=>{letdata=baseFile.split(';')[1]//base64的截取,根据后端要求截取的后半截的this.sa
  • Java项目实现Excel导出(Hutool) xwh- 笔记excelspringcloud
    官网:Excel生成-ExcelWriter(hutool.cn)1.使用Hutool工具实现Excel导出(.xlsx格式)业务场景:使用SpringCloud+mysql+mybatis-plus需要将数据库中的数据导出到Excel文件中前端为Vue2第零步:导入依赖(用于生成Excel文件)com.alibabaeasyexcel3.0.5(用于处理MicrosoftOffice格式文件)o
  • vue中导入导出Excel 前端小白一枚 笔记vue导入导出Excel
    以下仅个人做笔记使用:简单版导出Excel1、安装依赖:cnpminstall--savexlsxfile-savercnpmiscript-loader-S2、下载两个js文件:Blob.js和Export2Excel.js(放在最后面)3、添加导出按钮:导出数据4、添加导出事件:derive(){this.$http.post('admin/service_list',{pre_page:th
  • 【vue导入导出Excel】vue简单实现导出和导入复杂表头excel表格功能【纯前端版本和配合后端版本】 2401_84433535 前端vue.jsexcel
    ### 配合后端的两个方法 因为上面的纯前端写法有一个问题,就是有分页的时候我们没法拿到数据。或者数据太大了我们下载实在是有点慢和卡。所以基本上工作中都是后端生成下载链接导出的。这里再分享两个方法。1,[a标签](https://bbs.csdn.net/topics/618166371)下载这种方法核心就是后端直接生成下载链接,前端只需要生成A标签然后下载就行了。较为常用的一个daochu(){
  • Github 2025-06-24Python开源项目日报 Top10 老孙正经胡说 github开源Github趋势分析开源项目PythonGolang
    根据GithubTrendings的统计,今日(2025-06-24统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下:开发语言项目数量Python项目10Swift项目1C++项目1yt-dlp:一个增强版的youtube-dl分支创建周期:1184天开发语言:Python协议类型:TheUnlicenseStar数量:64607个Fork数量:5309次关注人数:64607人贡献
  • 探索适用于鸿蒙原生应用的跨平台开发框架 harmonyos
    探索适用于鸿蒙原生应用的跨平台开发框架在移动应用开发领域,跨平台开发框架的出现为开发者带来了极大的便利。本文将介绍几种适用于开发鸿蒙原生应用的跨平台应用开发技术框架。Uni-appUni-app是基于Vue.js开发前端应用的框架,它的优势在于一套代码能编译到iOS、Android、Web以及各种小程序等多个平台。在鸿蒙原生应用开发方面,它借助华为开发者联盟的开发工具和SDK来开发鸿蒙应用。对于熟
  • 钉钉小程序框架引入 Pinia 状态管理
    上一篇文章描述了如何使用其他手段实现路由守卫文章地址:钉钉小程序开发中实现路由守卫拦截-CSDN博客本文将深入探讨如何在基于这些技术栈的小程序项目中引入状态管理插件一、了解PiniaPinia是一个全新的Vue状态管理库,旨在替代Vuex成为Vue3应用程序中的首选状态管理解决方案。它提供了更加简洁的API和更好的TypeScript支持。通过结合使用UniApp、Vue3、TypeScript和
  • vue-28(服务器端渲染(SSR)简介及其优势) 清幽竹客 VUEvue.jsjavascript
    服务器端渲染(SSR)简介及其优势服务器端渲染(SSR)是现代网络应用的关键技术,特别是使用Vue.js等框架构建的应用。它通过在服务器上渲染初始应用状态来弥补传统单页应用(SPA)的局限性,从而提升性能、SEO和用户体验。本课程将全面介绍SSR,包括其优势以及与客户端渲染的对比。我们将为后续课程中使用Nuxt.js奠定基础,这是一个强大的框架,简化了Vue.js的SSR实现。理解服务器端渲染(S
  • Club_IntelliMatch_Development_Guide Joseit pythonpythonpygamedjangoflask
    ClubIntelliMatch系统-全栈开发流程文档概述ClubIntelliMatch系统是一个现代化的社团活动智能匹配平台,采用前后端分离架构。系统基于PythonFlask构建RESTfulAPI后端,Vue.js3+Vite构建现代化前端,MySQL作为持久化数据存储。本文档深入分析了整个开发流程的技术架构、设计原则和实现细节。系统架构流程图后端API架构前端组件架构app.pyFlas
  • JSON简介及其应用 Jackson@ML MongoDBJavaScriptNode.jsjsonJavaScriptNode.js
    JSON简介及其应用[email protected]的概念JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式,采用键值对(key-value)的方式组织数据,语法类似JavaScript对象,易于人阅读和机器解析。2.JSON的作用JSON有几方面的作用如下:•在前后端数据交互中传
  • 驾驭代码之道:JS/TS SOLID面向对象设计的五大黄金法则 领码科技 低代码技能篇javascript开发语言SOLID原则TypeScript面向对象代码设计AI辅助开发
    摘要在现代JavaScript和TypeScript开发中,代码质量与可维护性至关重要。SOLID原则作为面向对象设计的经典法则,为构建稳健、灵活的系统架构提供了科学指导。本文不仅系统阐释单一职责、开闭、里氏替换、接口隔离和依赖倒置五大原则的核心精髓,还结合当今流行的AI智能辅助、微前端、低代码开发等新技术与新思维,深入剖析它们在实际JS/TS项目中的应用场景和最佳实践。通俗易懂的理论讲解、丰富的
  • JavaScript 事件循环竟还能这样玩! 前端javascript
    JavaScript是一种单线程的编程语言,这意味着它一次只能执行一个任务。为了能够处理异步操作,JavaScript使用了一种称为事件循环(EventLoop)的机制。本文将深入探讨事件循环的工作原理,并展示如何基于这一原理实现一个更为准确的setTimeout、setInterval什么是事件循环?事件循环是JavaScript运行时环境中处理异步操作的核心机制。它允许JavaScript在执
  • 模拟工作队列 - 华为OD机试真题(JavaScript卷) 什码情况 算法面试javascript数据结构华为od
    华为OD机试题库《C++》限时优惠9.9华为OD机试题库《Python》限时优惠9.9华为OD机试题库《JavaScript》限时优惠9.9针对刷题难,效率慢,我们提供一对一算法辅导,针对个人情况定制化的提高计划(全称1V1效率更高)。看不懂有疑问需要答疑辅导欢迎私VX:code5bug题目描述让我们来模拟一个工作队列的运作,有一个任务提交者和若干任务执行者,执行者从1开始编号。提交者会在给定的时
  • 数据分类 - 华为OD机试真题(JavaScript 题解) 什码情况 华为odjavascript开发语言数据结构算法机试
    华为OD机试题库《C++》限时优惠9.9华为OD机试题库《Python》限时优惠9.9华为OD机试题库《JavaScript》限时优惠9.9针对刷题难,效率慢,我们提供一对一算法辅导,针对个人情况定制化的提高计划(全称1V1效率更高)。看不懂有疑问需要答疑辅导欢迎私VX:code5bug题目描述对一个数据a进行分类,分类方法为:此数据a(四个字节大小)的四个字节相加对一个给定的值b取模,如果得到的
  • Django项目前后端类型中,用户注册功能实现笔记(第一部分) 后端django
    用户注册前端逻辑为了学会使用Vue.js的双向绑定实现用户的交互和页面局部刷新效果。1.用户注册页面绑定Vue数据1.准备div盒子标签......2.register.html绑定内容:变量、事件、错误提示等{{csrf_input}}用户名:[[error_name_message]]密码:请输入8-20位的密码确认密码:两次输入的密码不一致号:[[error_mobile_message]]
  • React - 错误边界(Error boundary) 风轻轻~ #React__基础react.js
    React-错误边界(Errorboundary)一.理解二.使用1.使用方式2.使用案例错误边界:https://zh-hans.reactjs.org/docs/error-boundaries.html一.理解部分UI的JavaScript错误不应该导致整个应用崩溃,为了解决这个问题,React16引入了一个新的概念——错误边界。错误边界是一种React组件,这种组件可以捕获发生在其子组件树
  • 【Vue】 keep-alive缓存组件实战指南 Vesper63 vue.js缓存前端
    Vue中keep-alive的使用详解keep-alive是Vue内置的一个抽象组件,用于缓存不活跃的组件实例,避免重复渲染,从而优化性能。基本用法核心功能组件缓存:当组件切换时,不会被销毁状态保留:组件的所有状态(数据、DOM状态等)会被保留生命周期:触发特有的activated和deactivated钩子使用场景标签页切换路由视图缓存需要保存表单数据的场景组件频繁切换但需要保持状态属性配置1.
  • OA门户网站方案,含经典必要功能 Alex艾力的IT数字空间 jenkins运维架构数据库微服务java单元测试
    一、核心功能模块设计新手引导系统功能设计:分步引导:采用蒙层+气泡提示形式,按用户角色(如新员工、管理员)动态展示核心功能路径(如流程提交、知识检索)。场景化教学:嵌入交互式流程演示(如审批流程模拟),支持用户实时操作练习。进度跟踪:记录用户完成状态,未完成引导时在首页置顶提示。技术实现:基于Vue3的动态路由配置,结合用户行为分析(如点击热区)优化引导路径。统一应用入口功能设计:智能导航栏:根据
  • 函数的进阶 小盐巴小严 web前后端开发学习笔记javascript前端es6
    JavaScript函数概念构成函数主体的JavaScript代码在定义之时并不会执行,只有在调用函数时,函数才会执行。调用JavaScript函数的方法:作为函数作为方法作为构造函数通过函数的call()和apply()间接调用函数属性length属性在函数体例,arguments.length表示传入函数的实参的个数函数本身的length属性是只读的,代表函数声明的实际参数的数量functio
  • JavaScript基础-常见网页特效案例 咖啡の猫 javascript开发语言ecmascript
    一、前言在前端开发中,实现网页特效(如轮播图、下拉菜单、Tab切换、拖拽效果等)是提升用户体验和页面交互性的关键手段之一。这些特效不仅能增强用户对网站的粘性,也是衡量一个前端开发者能力的重要标准。JavaScript是实现网页特效的核心技术之一,结合HTML和CSS,可以轻松构建丰富的交互效果。本文将带你深入了解:常见网页特效的实现原理;如何使用原生JavaScript实现经典特效;每个案例附带完
  • JavaScript基础-触屏事件 咖啡の猫 javascript开发语言ecmascript
    一、前言随着移动端设备的普及,网页不仅要适配PC浏览器,更要兼容手机和平板等触摸设备。传统的鼠标事件(如click、mousedown等)在触控操作中存在一定的延迟和局限性,因此JavaScript提供了专门用于处理触摸操作的API——触屏事件(TouchEvents)。本文将带你深入了解:触屏事件的基本概念;常见的触屏事件类型(touchstart、touchmove、touchend等);如何
  • JavaScript 核心对象深度解析:Math、Date 与 String 小宁爱Python 前端javascript开发语言ecmascript
    JavaScript作为Web开发的核心语言,提供了丰富的内置对象来简化编程工作。本文将深入探讨三个重要的内置对象:Math、Date和String,通过详细的代码示例和综合案例帮助你全面掌握它们的用法。一、Math对象Math对象提供了一系列静态属性和方法,用于执行各种数学运算,无需实例化即可使用。常用属性:console.log(Math.PI);//圆周率:3.141592653589793
  • 零基础打造优雅的AI诗词创作助手 BaiYiQingXiang99 html人工智能chatgpt
    零基础打造优雅的AI诗词创作助手:一个纯前端实现的智能写诗工具项目介绍大家好,今天要和大家分享我的一个AI项目——AI诗词创作助手。这是一个完全使用原生JavaScript开发的智能写诗工具,不需要复杂的框架,也不需要后端服务器,就能实现专业级的AI诗词创作功能。在线体验地址GitHub地址主要特性1.多样化的创作选项支持多个主流AI模型(Deepseek、Moonshot(Kimi)、通义千问)
  • 从入门到精通:前端工程师必学的 JSON 全解析 前端视界 前端json状态模式ai
    从入门到精通:前端工程师必学的JSON全解析关键词:JSON、前端工程师、数据交换、JavaScript、数据格式摘要:本文围绕前端工程师必学的JSON展开全面解析。从JSON的基本概念、背景知识入手,深入探讨其核心原理、算法实现、数学模型等方面。通过详细的代码示例和实际应用场景分析,帮助前端工程师从入门到精通掌握JSON的使用。同时,提供了丰富的学习资源、开发工具和相关论文推荐,最后对JSON的
  • Vue 3 最基础核心知识详解 第七种黄昏 vue.js前端javascript
    Vue3作为现代前端主流框架,是前后端开发者都应当掌握的核心技能。本篇文章将带你了解vue3的基础核心知识,适合学习与复习一、Vue3应用创建1.1创建Vue应用的基本步骤//main.jsimport{createApp}from'vue'//1.导入createApp函数importAppfrom'./App.vue'//2.导入根组件constapp=createApp(App)//3.创建
  • 如何在编辑器wangEditor中完美复制粘贴WORD内容? M_Snow 编辑器wordumeditor粘贴wordueditor粘贴wordueditor复制wordueditor上传word图片ueditor导入word
    要求:开源,免费,技术支持编辑器:wangEditor前端:vue2,vue3,vue-cli,html5后端:java,jsp,springboot,asp.net,php,asp,.netcore,.netmvc,.netform群体:学生,个人用户,外包,自由职业者,中小型网站,博客,场景:数字门户,数字中台,站群,内网,外网,信创国产化环境,web截屏行业:医疗,教育,建筑,政府,党政,国
  • 网页版wangEditor如何实现WORD图片的高效粘贴? M_Snow wordumeditor粘贴wordueditor粘贴wordueditor复制wordueditor上传word图片ueditor导入wordueditor导入pdf
    要求:开源,免费,技术支持编辑器:wangEditor前端:vue2,vue3,vue-cli,html5后端:java,jsp,springboot,asp.net,php,asp,.netcore,.netmvc,.netform群体:学生,个人用户,外包,自由职业者,中小型网站,博客,场景:数字门户,数字中台,站群,内网,外网,信创国产化环境,web截屏行业:医疗,教育,建筑,政府,党政,国
  • 基于Java+Vue的数字化人力资源管理系统,高效整合数据,赋能企业人力精细化管理 软件源码专题社区 源码共享软件工程javamysqlvue源代码管理
    前言:在当今数字化浪潮席卷的时代,企业对于人力资源管理的效率和精准度提出了更高要求。传统的人力资源管理模式已难以满足企业快速发展的需求,繁琐的手工操作、信息传递不及时、数据统计不准确等问题,严重制约了企业人力资源管理的效能。数字化人力资源管理系统的出现,为企业提供了一种全新的解决方案,它借助先进的信息技术,将人力资源管理的各个环节进行整合和优化,实现人力资源管理的自动化、智能化和精细化,从而提升企
  • HarmonyOS NEXT仓颉开发语言实战案例:动态广场 幽蓝计划 harmonyos华为
    大家好,今日要分享的是使用仓颉语言开发动态广场页面,也比较像朋友圈页面:整个页面分为两部分,分别是导航栏和状态列表,导航栏比较简单,我们可以先写下导航栏的具体代码和页面的基本结构:Column{Row(10){Text('推荐').fontColor(Color.BLACK).fontSize(17).fontWeight(FontWeight.Bold)Text('关注').fontColor(
  • html 周华华 html
    js 1,数组的排列 var arr=[1,4,234,43,52,]; for(var x=0;x<arr.length;x++){    for(var y=x-1;y<arr.length;y++){      if(arr[x]<arr[y]){     &
  • 【Struts2 四】Struts2拦截器 bit1129 struts2拦截器
    Struts2框架是基于拦截器实现的,可以对某个Action进行拦截,然后某些逻辑处理,拦截器相当于AOP里面的环绕通知,即在Action方法的执行之前和之后根据需要添加相应的逻辑。事实上,即使struts.xml没有任何关于拦截器的配置,Struts2也会为我们添加一组默认的拦截器,最常见的是,请求参数自动绑定到Action对应的字段上。   Struts2中自定义拦截器的步骤是:
  • make:cc 命令未找到解决方法 daizj linux命令未知make cc
    安装rz sz程序时,报下面错误:   [root@slave2 src]# make posix cc   -O -DPOSIX -DMD=2 rz.c -o rz make: cc:命令未找到 make: *** [posix] 错误 127   系统:centos 6.6 环境:虚拟机   错误原因:系统未安装gcc,这个是由于在安
  • Oracle之Job应用 周凡杨 oracle job
    最近写服务,服务上线后,需要写一个定时执行的SQL脚本,清理并更新数据库表里的数据,应用到了Oracle 的 Job的相关知识。在此总结一下。   一:查看相关job信息    1、相关视图  dba_jobs  all_jobs  user_jobs  dba_jobs_running 包含正在运行
  • 多线程机制 朱辉辉33 多线程
    转至http://blog.csdn.net/lj70024/archive/2010/04/06/5455790.aspx 程序、进程和线程: 程序是一段静态的代码,它是应用程序执行的蓝本。进程是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程。线程是比进程更小的单位,一个进程执行过程中可以产生多个线程,每个线程有自身的
  • web报表工具FineReport使用中遇到的常见报错及解决办法(一) 老A不折腾 web报表finereportjava报表报表工具
    FineReport使用中遇到的常见报错及解决办法(一) 这里写点抛砖引玉,希望大家能把自己整理的问题及解决方法晾出来,Mark一下,利人利己。   出现问题先搜一下文档上有没有,再看看度娘有没有,再看看论坛有没有。有报错要看日志。下面简单罗列下常见的问题,大多文档上都有提到的。   1、address pool is full: 含义:地址池满,连接数超过并发数上
  • mysql rpm安装后没有my.cnf 林鹤霄 没有my.cnf
    Linux下用rpm包安装的MySQL是不会安装/etc/my.cnf文件的, 至于为什么没有这个文件而MySQL却也能正常启动和作用,在这儿有两个说法, 第一种说法,my.cnf只是MySQL启动时的一个参数文件,可以没有它,这时MySQL会用内置的默认参数启动, 第二种说法,MySQL在启动时自动使用/usr/share/mysql目录下的my-medium.cnf文件,这种说法仅限于r
  • Kindle Fire HDX root并安装谷歌服务框架之后仍无法登陆谷歌账号的问题 aigo root
    原文:http://kindlefireforkid.com/how-to-setup-a-google-account-on-amazon-fire-tablet/   Step 4: Run ADB command from your PC   On the PC, you need install Amazon Fire ADB driver and instal
  • javascript 中var提升的典型实例 alxw4616 JavaScript
    // 刚刚在书上看到的一个小问题,很有意思.大家一起思考下吧 myname = 'global'; var fn = function () { console.log(myname); // undefined var myname = 'local'; console.log(myname); // local }; fn() // 上述代码实际上等同于以下代码 m
  • 定时器和获取时间的使用 百合不是茶 时间的转换定时器
    定时器:定时创建任务在游戏设计的时候用的比较多   Timer();定时器 TImerTask();Timer的子类  由 Timer 安排为一次执行或重复执行的任务。       定时器类Timer在java.util包中。使用时,先实例化,然后使用实例的schedule(TimerTask task, long delay)方法,设定
  • JDK1.5 Queue bijian1013 javathreadjava多线程Queue
    JDK1.5 Queue LinkedList: LinkedList不是同步的。如果多个线程同时访问列表,而其中至少一个线程从结构上修改了该列表,则它必须 保持外部同步。(结构修改指添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方
  • http认证原理和https bijian1013 httphttps
    一.基础介绍         在URL前加https://前缀表明是用SSL加密的。 你的电脑与服务器之间收发的信息传输将更加安全。         Web服务器启用SSL需要获得一个服务器证书并将该证书与要使用SSL的服务器绑定。 http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后
  • 【Java范型五】范型继承 bit1129 java
    定义如下一个抽象的范型类,其中定义了两个范型参数,T1,T2   package com.tom.lang.generics; public abstract class SuperGenerics<T1, T2> { private T1 t1; private T2 t2; public abstract void doIt(T
  • 【Nginx六】nginx.conf常用指令(Directive) bit1129 Directive
    1. worker_processes    8; 表示Nginx将启动8个工作者进程,通过ps -ef|grep nginx,会发现有8个Nginx Worker Process在运行   nobody 53879 118449 0 Apr22 ? 00:26:15 nginx: worker process
  • lua 遍历Header头部 ronin47 lua header 遍历 
    local headers = ngx.req.get_headers()   ngx.say("headers begin", "<br/>")   ngx.say("Host : ", he
  • java-32.通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小(两数组的差最小)。 bylijinnan java
    import java.util.Arrays; public class MinSumASumB { /** * Q32.有两个序列a,b,大小都为n,序列元素的值任意整数,无序. * * 要求:通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小。 * 例如: * int[] a = {100,99,98,1,2,3
  • redis 开窍的石头 redis
    在redis的redis.conf配置文件中找到# requirepass foobared 把它替换成requirepass 12356789 后边的12356789就是你的密码 打开redis客户端输入config get requirepass 返回 redis 127.0.0.1:6379> config get requirepass 1) "require
  • [JAVA图像与图形]现有的GPU架构支持JAVA语言吗? comsci java语言
          无论是opengl还是cuda,都是建立在C语言体系架构基础上的,在未来,图像图形处理业务快速发展,相关领域市场不断扩大的情况下,我们JAVA语言系统怎么从这么庞大,且还在不断扩大的市场上分到一块蛋糕,是值得每个JAVAER认真思考和行动的事情       
  • 安装ubuntu14.04登录后花屏了怎么办 cuiyadll ubuntu
    这个情况,一般属于显卡驱动问题。 可以先尝试安装显卡的官方闭源驱动。 按键盘三个键:CTRL + ALT  +  F1 进入终端,输入用户名和密码登录终端: 安装amd的显卡驱动 sudo  apt-get  install  fglrx 安装nvidia显卡驱动 sudo  ap
  • SSL 与 数字证书 的基本概念和工作原理 darrenzhu 加密ssl证书密钥签名
    SSL 与 数字证书 的基本概念和工作原理 http://www.linuxde.net/2012/03/8301.html SSL握手协议的目的是或最终结果是让客户端和服务器拥有一个共同的密钥,握手协议本身是基于非对称加密机制的,之后就使用共同的密钥基于对称加密机制进行信息交换。 http://www.ibm.com/developerworks/cn/webspher
  • Ubuntu设置ip的步骤 dcj3sjt126com ubuntu
    在单位的一台机器完全装了Ubuntu Server,但回家只能在XP上VM一个,装的时候网卡是DHCP的,用ifconfig查了一下ip是192.168.92.128,可以ping通。 转载不是错: Ubuntu命令行修改网络配置方法 /etc/network/interfaces打开后里面可设置DHCP或手动设置静态ip。前面auto eth0,让网卡开机自动挂载. 1. 以D
  • php包管理工具推荐 dcj3sjt126com PHPComposer
    http://www.phpcomposer.com/   Composer是 PHP 用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。 中文文档  入门指南  下载  安装包列表 Composer 中国镜像
  • Gson使用四(TypeAdapter) eksliang jsongsonGson自定义转换器gsonTypeAdapter
    转载请出自出处:http://eksliang.iteye.com/blog/2175595 一.概述        Gson的TypeAapter可以理解成自定义序列化和返序列化 二、应用场景举例        例如我们通常去注册时(那些外国网站),会让我们输入firstName,lastName,但是转到我们都
  • JQM控件之Navbar和Tabs gundumw100 htmlxmlcss
    在JQM中使用导航栏Navbar是简单的。 只需要将data-role="navbar"赋给div即可: <div data-role="navbar"> <ul> <li><a href="#" class="ui-btn-active&qu
  • 利用归并排序算法对大文件进行排序 iwindyforest java归并排序大文件分治法Merge sort
      归并排序算法介绍,请参照Wikipeida zh.wikipedia.org/wiki/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F 基本思想: 大文件分割成行数相等的两个子文件,递归(归并排序)两个子文件,直到递归到分割成的子文件低于限制行数 低于限制行数的子文件直接排序 两个排序好的子文件归并到父文件 直到最后所有排序好的父文件归并到输入
  • iOS UIWebView URL拦截 啸笑天 UIWebView
    本文译者:candeladiao,原文:URL filtering for UIWebView on the iPhone说明:译者在做app开发时,因为页面的javascript文件比较大导致加载速度很慢,所以想把javascript文件打包在app里,当UIWebView需要加载该脚本时就从app本地读取,但UIWebView并不支持加载本地资源。最后从下文中找到了解决方法,第一次翻译,难免有
  • 索引的碎片整理SQL语句 macroli sql
    SET NOCOUNT ON DECLARE @tablename VARCHAR (128) DECLARE @execstr VARCHAR (255) DECLARE @objectid INT DECLARE @indexid INT DECLARE @frag DECIMAL DECLARE @maxfrag DECIMAL --设置最大允许的碎片数量,超过则对索引进行碎片
  • Angularjs同步操作http请求with $promise qiaolevip 每天进步一点点学习永无止境AngularJS纵观千象
    // Define a factory app.factory('profilePromise', ['$q', 'AccountService', function($q, AccountService) { var deferred = $q.defer(); AccountService.getProfile().then(function(res) {
  • hibernate联合查询问题 sxj19881213 sqlHibernateHQL联合查询
    最近在用hibernate做项目,遇到了联合查询的问题,以及联合查询中的N+1问题。 针对无外键关联的联合查询,我做了HQL和SQL的实验,希望能帮助到大家。(我使用的版本是hibernate3.3.2)   1 几个常识:  (1)hql中的几种join查询,只有在外键关联、并且作了相应配置时才能使用。  (2)hql的默认查询策略,在进行联合查询时,会产
  • struts2.xml wuai struts
    <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" "http://struts.apache
按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
首页 - 关于我们 - 站内搜索 - Sitemap - 侵权投诉
版权所有 IT知识库 CopyRight © 2000-2050 E-COM-NET.COM , All Rights Reserved.