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)

  • 移动端城市区县二级联动选择功能实现包 good2know
    本文还有配套的精品资源,点击获取简介:本项目是一套为移动端设计的jQuery实现方案,用于简化用户在选择城市和区县时的流程。它包括所有必需文件:HTML、JavaScript、CSS及图片资源。通过动态更新下拉菜单选项,实现城市到区县的联动效果,支持数据异步加载。开发者可以轻松集成此功能到移动网站或应用,并可基于需求进行扩展和优化。1.jQuery移动端解决方案概述jQuery技术简介jQuery
  • uniapp微信小程序 - 详解微信小程序平台用户授权登录全流程,uniapp v3版本中小程序端开发下用户点击登录后获取手机号/昵称/性别/头像等信息完成登录(提供完整示例代码,一键复制开箱即用) 十一猫咪爱养鱼 前端组件与功能(开箱即用)uniapp常见问题解决uniappvue3uniapp3小程序授权登录微信小程序登录获取用户信息教程获取用户昵称手机号头像信息登录vue3版本小程序平台授权登录uniap小程序端用户登录流程uni完整的小程序平台登录源码
    效果图在uniapp微信小程序端开发中,超详细实现用户授权登录完整功能源码,用户授权后获取手机号/昵称/头像/性别等,提供完整思路流程及逻辑讲解。uniappVue3和Vue2都能用,你也可以直接复制粘贴,然后改下参数放到你的项目中去就行。整体思路做功能之前,先来看一下整体流程是
  • 用代码生成艺术字:设计个性化海报的秘密
    本文围绕“用代码生成艺术字:设计个性化海报的秘密”展开,先概述代码生成艺术字在海报设计中的独特价值,接着介绍常用的代码工具(如HTML、CSS、JavaScript等),详细阐述从构思到实现的完整流程,包括字体样式设计、动态效果添加等,还分享了提升艺术字质感的技巧及实际案例。最后总结代码生成艺术字的优势,为设计师提供打造个性化海报的实用指南,助力提升海报设计的独特性与吸引力,符合搜索引擎SEO标准
  • vue element 封装表单 影子信息 vuevue.jsjavascript前端
    背景:在前端系统开发中,系统页面涉及到的表单组件比较多,所以进行了简单的封装。封装的包括一些Form表单组件,如下:input输入框、select下拉框、等实现效果:理论知识:表单组件官方链接:点击跳转封装组件:封装组件的思路:不封装element组件,每一个input组件绑定一个form对象,例如官网。简单封装element组件,利用for循环生成form表单的每一项el-form-item。进
  • 大学社团管理系统(11831) codercode2022 javaspringbootspringechartsspringcloudsentineljava-rocketmq
    有需要的同学,源代码和配套文档领取,加文章最下方的名片哦一、项目演示项目演示视频二、资料介绍完整源代码(前后端源代码+SQL脚本)配套文档(LW+PPT+开题报告)远程调试控屏包运行三、技术介绍Java语言SSM框架SpringBoot框架Vue框架JSP页面Mysql数据库IDEA/Eclipse开发四、项目截图有需要的同学,源代码和配套文档领取,加文章最下方的名片哦!
  • VUE 座位图功能+扩展 NUZGNAW vue.jsjavascript前端
    1、通过循环画出页面座位图0"style="display:table;margin:0auto;min-height:472px;position:relative;">{{i.sign}}2、画出右下角的预览图3、编写对应js方法watch:{seatList:{handler(newVal,oldVal){if(this.seatList&&!stringBlank(this.seatLis
  • 为什么学习Web前端一定要掌握JavaScript? web前端学习指南
    为什么学习Web前端一定要掌握JavaScript?在前端的世界里,没有什么是JavaScript实现不了的,关于JS有一句话:凡是可以用JavaScript来写的应用,最终都会用JavaScript,JavaScript可运行在所有主要平台的所有主流浏览器上,也可运行在每一个主流操作系统的服务器端上。现如今我们在为网站写任何一个主要功能的时候都需要有懂能够用JavaScript写前端的开发人员。
  • Vue CSR 到 Nuxt 3 SSR 迁移:技术实现与问题解决实录 二倍速播放 前端vue.js
    1.迁移动机与技术选型1.1CSR架构的局限性基于Vue3和Vite构建的客户端渲染(CSR)单页应用(SPA)提供了良好的开发体验和用户交互流畅性。但是其核心局限在于:搜索引擎优化(SEO):初始HTML响应仅包含一个根div元素,实际内容由JavaScript在浏览器端动态生成。虽然主流搜索引擎(如Google)能够执行部分JavaScript,但其抓取效率和稳定性不如直接获取完整HTML。非
  • vue项目 阿什么名字不会重复呢 vue.js前端javascript
    vue进行配置创建vue项目vuecreateone//one就是项目名字npmielement-ui--Snpmiaxios--savenpmiecharts--save配置main.jsimportVuefrom'vue'importAppfrom'./App.vue'importrouterfrom'./router'importstorefrom'./store'importElement
  • 开发避坑短篇(7):Vue+window.print()打印实践 帧栈 避坑指南vue.js前端elementui
    需求vue项目中如何打印当前页面内容?解决办法使用浏览器原生APIwindow.print(),这是最简单直接的方式,通过调用window.print()方法触发浏览器打印功能‌。在vue项目中直接调用print()方法即可,无须引入任何插件。如下:functionhandlePrint(){setTimeout(_=>print(),500)}打印页面样式控制可以使用@mediaprint来控制
  • 12. 什么是事件委托 yqcoder 前端面试-CSScss面试
    总结事件委托(EventDelegation)是JavaScript中一种重要的事件处理机制,它利用了事件冒泡的特性,将事件的处理程序绑定到父元素或祖先元素上,而不是直接绑定到具体的子元素上。什么是事件委托?事件冒泡:在DOM中,事件通常会从触发元素开始,然后逐级向上冒泡到其父元素、祖先元素,直到window对象。核心思想:事件委托的核心思想是利用事件冒泡机制,在父元素上监听事件,而不是在每个子元
  • vue2中实现leader-line-vue连线文章对应字符 小莉爱编程 vuebug记录vue.js前端javascript
    效果展示通过点击右边的tag,触发连接操作第一步:获取右边tag展示1.右边的tag列表展示,我这边是分为两个list嵌套的数据结构;{"人员":[{
  • 关于前端的性能优化
    性能优化主要涵盖了以下四个方面:(tip:仅代表个人总结,如有不当,还希望看到的大佬多多指示)减少网络请求:合并文件、使用CDN、启用缓存。优化资源加载:代码分割、懒加载、图片压缩。提升渲染性能:减少重绘回流、防抖节流、使用WebWorker。监控和迭代:定期使用工具检测性能,持续优化。一、网络层面优化减少HTTP请求合并文件:将多个CSS或JavaScript文件合并成一个,减少请求次数。使用C
  • 时间组件库Day.js那些事 前端小白花 javascript开发语言ecmascript
    一、简介1.什么是Day.jsDay.js是一个轻量级,易于使用的JavaScript日期库,提供了强大的日期和时间处理功能,与Moment.js的API设计相似,但具有更高的性能和更小的体积。Day.js官网https://day.js.org/docs/zh-CN/installation/installation2.优势a.特点轻量级:Dayjs的压缩后大小只有2KB左右,远小于Moment
  • 【antdv4.0FormItem更新tooltip属性引发的思考】
    北京时间2023-10-19,ant-design-vue最近发布了4.0.4-4.0.6的小版本更新,其中4.0.4新增了对Form组件中FormItem的tooltip属性支持,这个功能我觉得在后台管理场景下还是非常实用的,tooltip属性作用于FormItem中的FormItemLabel区域,用来配置提示信息,当用户不确定该行的用处或者不知道填什么内容时,可以作为input里placeh
  • JavaScript正则表达式去除括号但保留内容与去除括号与内容 Selicens javascript正则表达式
    项目上碰到一个需求,是取多个递增文件的文件名,类似于test(1).txt、test(2).txt,但是不需要括号,只要test1、test2这种格式,最开始想到的办法就是js里的replace替换,先上一个比较笨但是也能实现效果的例子letname="test(1).txt"letdata=name.split('.')[0].replace('(','').replace(')','')con
  • 零基础入门uniapp Vue3组合式API版本 鹤早早 uni-app
    前言:小程序学习笔记,课程来源up主咸虾米_。仅记录笔记,大家想学习可以去关注他。1.已安装HBuiderX(目前是4.36版本),微信开发者工具(但还没注册小程序码),相关配置OK了。1.16相关架构学习1.pages-index-index.vuebox1box2.layout{border:1pxsolidred;.box1{border:1pxsolidgreen;}.box2{borde
  • Vue3组合API初体验 DTcode7 Vue实战指南VUEHTMLwebvue框架前端
    Vue3组合API初体验基本概念与作用说明示例一:使用ref创建响应式数据示例二:使用reactive创建响应式对象示例三:使用computed计算属性示例四:使用watch监听数据变化示例五:使用provide/inject进行父子组件间通信功能使用思路与实际开发技巧1.何时使用ref与reactive?2.如何在组合式API中保持逻辑的清晰?3.如何处理异步操作?随着Vue3的发布,组合式AP
  • 页面开发样式和布局入门:Vite + Vue 3 + Less
    页面开发样式和布局入门:Vite+Vue3+Less引言在现代前端开发中,样式和布局是页面开发的核心部分。随着技术的不断发展,Vite、Vue3和Less等工具和框架的出现,使得前端开发变得更加高效和灵活。然而,尽管这些工具和框架提供了强大的功能,但在实际开发中仍然会遇到各种样式和布局的问题。本文将结合Vite、Vue3和Less,详细介绍在页面开发中常见的样式和布局问题,并提供解决方案和最佳实践
  • 网络安全第14集 不灭锦鲤 web安全安全
    前言:小迪安全14集,这集重点内容:0、什么是js渗透测试?在javascript中也存在变量和函数,存在可控变量和函数就有可能存在在漏洞,js开发的web应用和php、java开发的区别是,js能看得到的源代码,php看不到,但是风险就是未授权访问、配置信息泄露(加密算法、key秘钥等),源代码看得到,存在更多的url泄露,从而可能会出现未授权访问,从url,前提:web应用可以采用前端语言或后
  • 25年不倒的“犀牛书“:为什么《JavaScript权威指南第7版》仍是程序员的案头圣经? 阿蒙Armon javascript开发语言ecmascript
    25年不倒的"犀牛书":为什么《JavaScript权威指南第7版》仍是程序员的案头圣经?当一本技术书能连续畅销25年,历经7次迭代仍稳坐领域第一梯队,它必然藏着超越时代的硬实力。《JavaScript权威指南(原书第7版)》就是这样一部传奇——豆瓣9.4分的封神之作,程序员口中的"犀牛书",从网景时代到ES2020,它始终是JavaScript开发者的终极参考书。如果你还在为原型链、闭包、异步编
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Pokedex(宝可梦图鉴)
    我们继续50个小项目挑战!——Pokedex组件仓库地址:https://github.com/SunACong/50-vue-projects项目预览地址:https://50-vue-projects.vercel.app/使用Vue3结合PokeAPI来创建一个炫酷的宝可梦图鉴应用。通过这个项目,你将能够浏览前150只宝可梦的信息,包括它们的名字、类型、身高、体重以及能力等。而且,每只宝可梦
  • 前端基础知识Vue3系列 - 04(Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同)
    开始之前CompositionAPI可以说是Vue3的最大特点,那么为什么要推出CompositionApi,解决了什么问题?通常使用Vue2开发的项目,普遍会存在以下问题:代码的可读性随着组件变大而变差每一种代码复用的方式,都存在缺点TypeScript支持有限以上通过使用CompositionApi都能迎刃而解正文一、OptionsApiOptionsAPI,即大家常说的选项API,即以vue
  • 什么是JSON,如何与Java对象转化 doublez234 javajsonspringspringboot
    JSON概念JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。它易于人阅读和编写,同时也易于机器解析和生成。JSON基于JavaScript(ECMAScript(欧洲计算机协会制定的js规范))编程语言的一个子集,但它是一种独立于语言的数据格式,被广泛应用于各种编程语言和平台之间的数据交换。JSON的主要特点:轻量级:JSON格式非常简洁,只包含必要的数据信
  • Vue3判断对象是否为空方法 滿 vue.js前端javascript
    1、Object.keys()检查对象自身可枚举属性的数量:constisEmpty=Object.keys(obj).length===0;2、JSON.stringify()将对象转为JSON字符串判断:constisEmpty=JSON.stringify(obj)==='{}';3、Reflect.ownKeys()(包含Symbol属性)constisEmpty=Reflect.ownK
  • Vue 工程化开发入门 dawn191228 前端学习vue.js前端javascript前端框架
    文章目录一、Vue工程化开发概述二、Vue脚手架三、项目运行流程四、组件化开发五、组件注册六、总结在当今的前端开发领域,Vue.js以其简洁、高效的特性受到了广泛的欢迎。Vue的工程化开发能够帮助我们更好地组织和管理项目,提高开发效率和代码质量。本文将带你了解Vue工程化开发入门知识,包括脚手架、项目运行流程、组件化以及组件注册。一、Vue工程化开发概述工程化开发是一种将软件开发过程规范化、标准化
  • APP开发入门:了解主流的编程语言 agi大模型 Python职业与职场程序员开发语言数据分析编程语言
    前言在过去的几年里,有许多程序员开始学习和使用编程语言。这其中包括C、C++、Java和Python。尽管有许多语言可供选择,但大多数程序员都会选择最容易学习的编程语言。如今,有很多编程语言供选择。程序员们在学习这些语言时可以自由地选择他们喜欢的方式,因为他们的目标是构建任何软件,而不仅仅是创建一个应用程序。你可以在Linux上学习C/C++、Java、Python、C#或JavaScript,你
  • vue 使用 iFrame 嵌套带有登录的网站 登录成功 无法跳转 WHY<=小氣鬼=> 前端
    这个问题有点好玩,当时做的时候没想起来,哎不说讲一下问题1.项目在使用iFrame标签嵌套其他,网址或者自己的网址的时候如果没有登录只是浏览是没有问题的2.如个你使用Frame标签嵌套的网址是带有登录的需要注意两个网站是不是在统一域名下面如果在就可以正常登录,如果不在的话会出现登录成功但是无法跳转的问题3.在Vue项目中使用iFrame嵌套本地.html文件,本地.html文件向父页面抛出的方法或
  • 探秘CommonJS:Node.js模块化核心解析
    CommonJS是JavaScript的模块化规范,主要应用于服务器端环境(尤其是Node.js),其核心目标是解决代码组织、依赖管理和作用域隔离问题。以下是其核心要点:一、核心特性同步加载模块通过require()同步加载并执行,后续代码需等待模块加载完成后执行,适用于I/O快速的服务器环境(如本地文件读取)。作用域隔离每个文件视为独立模块,模块内定义的变量、函数默认私有(不污染全局作用域),仅
  • 壹脉销客AI电子名片源码核心架构
    为什么选择源码部署AI电子名片?在数字化转型浪潮中,越来越多的企业意识到拥有自主可控的电子名片系统的重要性。源码部署相比SaaS服务具有三大核心优势:数据完全自主-客户信息存储在企业自有服务器深度定制自由-可根据业务需求二次开发长期成本优化-一次部署永久使用壹脉销客AI电子名片源码核心架构壹脉销客提供的企业级电子名片解决方案采用前后端分离架构:前端技术栈(小程序端)javascript//小程序a
  • 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.