從事過BPM行業的大佬必然對流程建模工具非常熟悉,做為WFMC三大體系結構模型中的核心模組,它是工作流的能力模型,其他模組都圍繞工作流定義來構建。
成熟的建模工具通過視覺化的操作介面和行業BPMN規範描述使用者容易理解的工作流的各種構成圖元,例如圓圈表示事件,方框表示活動。
VUE3 + TS + Ant Design Vue
選擇TS做為首選語言我們是經過充分考慮和驗證的,並不是單純的因為TS比較流行、時髦而去無腦應用。流程設計器是對流程的建模,必然涉及到大量的業務屬性資料建模,這些屬性可以通過類的方式抽象、繼承、維護,也就是物件導向開發,而這恰好是TS的優勢。我們的專案中大概有80多個業務模型,如果用JS去表示,那將是何種場景!在驗證的過程中我們發現,使用TS開發可以簡化開發複雜度和提高產品的成功率。
VUE3 + TS 使用的過程中並不是很順暢,主要是型別檢查方面做的並不是很好。如 vuex、混入 等。
AntV X6
對於流程圖基本的圖形繪製能力,我們調研過多個開源的框架,最終選擇了 X6。下面附上調研結果,僅當參考(作者對這些框架都帶著敬畏之心,並沒有惡意,如有不適,勿噴)。
底層技術 | 瀏覽器支援情況 | 事件處理 | 渲染效果 |
---|---|---|---|
SVG | IE9++、Edge、Chrome、Safari、Opera、360、Firefox | 友好 | 適合複雜度低的流程圖 |
Canvas | IE9++、Edge、Chrome、Safari、Opera、360、Firefox | 基於位置的定位事件不友好 | 更適合影象密集型的遊戲應用 |
框架 | 底層技術 | 檔案地址 | 協定 | 點評 |
---|---|---|---|---|
SVG.JS | SVG | https://svgjs.dev/docs/3.0/shape-elements/ | MIT license | 僅支援基礎的圖形繪製能力 |
G6 圖視覺化引擎 | canvas | https://g6.antv.vision/zh | MIT license | 上手容易,功能面廣 |
X6 圖視覺化引擎 | SVG | https://x6.antv.vision/zh/examples/showcase/practices | MIT license | 上手容易,比較專注流程圖領域 |
D3.js | SVG | https://d3js.org/ https://github.com/d3/d3/wiki/API--中文手冊 | BSD license | 複雜度高,難上手。 |
logic-flow | SVG | http://logic-flow.org/ | Apache-2.0 License | 上手容易,更專注流程圖領域,功能不全,較為粗超 |
bpmn.js | SVG | https://bpmn.io/toolkit/bpmn-js/ | Apache-2.0 License | 專業的流程繪製框架,沒檔案,完全遵循BPMN2.0 |
普通JS物件與TS物件互轉利器
流程模型驗證利器,類似 C# 中 Attribute,java 中的註解,通過在屬性上加註解實現驗證。
BPMN2.0規範中對圖元做了定義,如圓圈表示事件、方框表示人工任務、菱形表示閘道器。但是我們的BPM產品主要面對的是國內的客戶,規範中的圖元太抽象,不適合國內,基於X6基礎圖形我們定義了一套新圖元。
右側的屬性面板是設定業務的區域,右下角有儲存和重置兩個按鈕。點選重置後需要對屬性面板內所有元件的內容進行重新初始化,因為元件不止一個,多是多級巢狀的,所以需要遞迴重置。
專案中我們採用vue區域性混入的方式,在每個元件上傳遞 currentUUID props 的方式,層層下鑽通知子元件重新初始化內容。
vue3 + ts 使用混入比較繁瑣噁心,下面是核心程式碼:
declare module 'vue' {
interface ComponentCustomProperties {
/* 定義更新當前元件ID的混入方法 */
updateCurrentUUID: (from: string) => void
}
}
export default defineComponent({
props: {
/** 父元件的UUID */
parentUUID: {
type: Object
}
},
data () {
return {
/** 當前元件的UUID */
currentUUID: {
uuid: v4(),
from: '' // 驅動來源
},
/** 支援的級聯更新來源 */
supportFroms: [
'propertyReset', // 屬性面板重置
'ruleChange'
]
}
},
methods: {
/** 初始化資料,要求所有子元件的初始化都放到該方法內 */
initComponentData () {
/* 子元件資料初始化的方法 */
},
/** 更新當前元件UUID */
updateCurrentUUID (from: string) {
this.currentUUID.from = from
this.currentUUID.uuid = v4()
}
},
watch: {
/** */
parentUUID: {
handler: function (val) {
// 如果來源在 supportFroms 集合中,才支援重新初始化
if (this.supportFroms.indexOf(val.from) > -1) {
this.initComponentData()
this.$forceUpdate()
this.$nextTick(() => {
this.updateCurrentUUID(val.from)
})
}
},
deep: true
}
}
})
右側的屬性面板在點選儲存時需要驗證資料的完整性,而這些資料又分佈在不同的子元件內,所以需要每個子元件自己完成資料驗證。專案中我們採用混入 + 釋出訂閱設計模式完成該功能。
子元件在 mounted 時訂閱驗證事件,unmounted 時刪除訂閱,點選儲存時釋出驗證事件,每個子元件完成自身的驗證後返回一個 Promise,當所有子元件都驗證完成後,再將資料儲存到資料庫。
declare module 'vue' {
interface ComponentCustomProperties {
componentValidate: (data?: any) => Promise<ValidateResult>
}
}
/**
* 元件驗證結果
*/
export interface ValidateResult {
/** 是否驗證通過 */
isOk: boolean,
/** 驗證失敗的訊息 */
msgs?: string[]
}
export default defineComponent({
props: {
},
data () {
return {
}
},
mounted () {
const pubSub = inject<PubSub>('pubSub')
if (pubSub) {
unref(pubSub).on(this.currentUUID.uuid, this.componentValidate)
}
},
beforeUnmount () {
const pubSub = inject<PubSub>('pubSub')
if (pubSub) {
unref(pubSub).off(this.currentUUID.uuid)
}
},
unmounted () {
const pubSub = inject<PubSub>('pubSub')
if (pubSub) {
unref(pubSub).off(this.currentUUID.uuid)
}
},
methods: {
/** 元件驗證 */
componentValidate (data?: any): Promise<ValidateResult> {
return Promise.resolve({
isOk: true
})
}
}
})
<template>
<div>
</div>
</template>
<script lang="ts">
export default defineComponent({
name: 'BaseTabView',
mixins: [resetMixin], // 混入元件驗證模組
props: {
},
data () {
return {
}
},
setup () {
},
mounted () {
},
methods: {
componentValidate (data?: any): Promise<ValidateResult> {
const result: ValidateResult = {
isOk: true,
msgs: []
}
return Promise.resolve(result)
}
}
})
</script>
export class PubSub {
// eslint-disable-next-line @typescript-eslint/ban-types
handles: Map<string, Function> = new Map<string, Function>()
/** 訂閱事件 */
on (eventType: string, handle: any) {
if (this.handles.has(eventType)) {
throw new Error('重複註冊的事件')
}
if (!handle) {
throw new Error('缺少回撥函數')
}
this.handles.set(eventType, handle)
return this
}
/** 釋出事件 所有事件 */
emitAll (data?: any): Promise<any[]> {
const result: Promise<any>[] = []
this.handles.forEach(item => {
// eslint-disable-next-line prefer-spread
result.push(item.apply(null, data))
})
return Promise.all(result)
}
/** 釋出事件 */
emit (eventType: string, data?: any) {
if (!this.handles.has(eventType)) {
throw new Error(`"${eventType}"事件未註冊`)
}
const handle = this.handles.get(eventType)!
// eslint-disable-next-line prefer-spread
handle.apply(null, data)
}
/** 刪除事件 */
off (eventType: string) {
this.handles.delete(eventType)
}
}
關於作者:本人從事BPM開發多年,歡迎有志同道合之友來擾!