Type Script 在流程設計器的落地實踐

暗夜餘暉發表於2022-12-03

流程設計器專案介紹

從事過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/#svg-line MIT license 僅支援基礎的圖形繪製能力
G6 圖視覺化引擎 canvas https://g6.antv.vision/zh MIT license 上手容易,功能面廣
X6 圖視覺化引擎 SVG https://x6.antv.vision/zh/examples/showcase/practices#bpmn 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

輔助框架

class-transformer

普通JS物件與TS物件互轉利器

class-validator

流程模型驗證利器,類似 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開發多年,歡迎有志同道合之友來擾!

相關文章