1. 程式人生 > >行星工單系統部分實現(1)-流程控制

行星工單系統部分實現(1)-流程控制

1.工單系統的表格頁

工單系統是一種網路軟體系統,根據不同組織,部門和外部客戶的需求,來由針對的管理,維護和追蹤一系列的問題和請求。大多用於記錄、處理、跟蹤一項工作的完成情況。
為使客服同學有序、協同處理客戶問題,客服技術團隊打造了多渠道整合的,可靈活配置的,便於流轉的行星工單系統。

由於工單系統從屬後臺系統,頁面展示基本以如下表格頁為主.為統一表格頁行為,故將其統一封裝成表格頁元件.

表格頁元件分為如下幾塊:

  1. 篩選項區域
  2. 表格主體
  3. 分頁元件
  4. 操作按鈕
  5. 行內操作連結

其中分頁元件可通過向表格頁元件傳入標誌來控制其顯示狀態、篩選項區域及表格主體內容可通過向表格頁元件傳入表格列配置及表單項配置實現不同頁面間各異的邏輯。

但由於操作按鈕、行內操作連結的邏輯各頁基本不同,無法將其封入表格頁元件。因此採用事件處理這部分邏輯從列表頁元件中解耦,我們將不同表格頁按鈕操作名稱作為唯一標識,在入口檔案中將系統全量事件集註冊到全域性,將標識與事件集中事件建立起對應關係,以此實現上述需求。

然後我們發現,這些操作按鈕的邏輯,基本為以下幾類行為的聚合:

  • 彈出提示框讓使用者操作
  • 彈出表單類元件獲取一部分資料
  • 表單及其他資料校驗
  • 呼叫介面去拉取/更新資料

而且比較重要的一點是,它們都是序列的。如編輯使用者的操作:

  • 從介面拉取使用者資料
  • 彈出表單元件將資料填入同時供使用者修改
  • 校驗使用者提交資料
  • 呼叫介面提交資料。

這幾步操作中,如果流程中某一節點的行為出現問題,比較合理的處理方案是截停流程,並丟擲一個錯誤響應給開發者或使用者。所以根據這個處理思路,我們建立工單系統的流程控制方案。

2.簡單的管道

論及序列執行流程行為,並將當前行為的執行結果作為下一行為的引數的操作,我們首先想到的是vue及angular中的管道。下面我們用Array.reduce去實現一個簡單的管道.

const grep = ({srcData, fnList, context}) => fnList.reduce((dist, currFn) => currFn.call(context, dist), srcData)

此函式邏輯很簡單,即遍歷行為集,取各項行為執行,再借用reduce特性將上一行為結果傳入下一行為。據此,我們用類管道函式實現了簡單的流程控制。

如下呼叫:

function filterLessThanFive(srcData){ //[1,3,4,6,7,8] => [1,3,4]
    return srcData.filter(item => Number.isInteger(item) ? item > 5 : false)
}
function sum(srcData){ // [1,3,4] => 1+3+4 => 8
    return srcData.reduce((dist,item) => dist+(parseInt(item, 10)), 0)
}
function plusOne(srcData){ // 8+1 => 9
    return srcData+1
}
const fnList = [filterLessThanFive, sum, plusOne]
const srcData = [1,3,4,6,7,8]
const res = grep({context:this, fnList, srcData}) 

注意:

1.由於grep內部強制更改fnList的上下文,故fnList內部函式不可用箭頭函式及bind進行上下文繫結

2.為了靈活適應需求,不強制規範流轉資料格式,或造成多次遍歷等效能浪費,由於前端資料量較小,故不考慮效能問題.

3.流程控制

但是管道有兩個不能解決的問題:

1.不能處理非同步需求:如需求為介面返回結果之後,將結果作為下一個函式的引數。鑑於fnList(流程行為集)中如果存在非同步函式,那麼非同步流程行為函式在流程中不能流轉資料或僅能流轉promise物件嗎,無法完成上述需求。

2.當前流程節點行為只能獲取上個流程節點行為執行結果:我們需要更靈活的獲取之前行為結果的方式,如跨節點獲取、多節點獲取。

為解決以上兩點我們初步構建事務流處理函式如下:

export async function actionFnSimple(actionList, eventMap) {
    const that = this // 獲取元件上下文
    const defaultEventMap = { ...eventMapSrc, ...this }// 預設行為集
    const eventActionMap = eventMap || defaultEventMap
    const result = await actionList
    .reduce(async (cache, actionItem) => {
        const { actionKey, from = '' } = actionItem
        const ownParams = getRestObj(actionItem, ['actionKey', 'from'])
        const cacheReal = cache.then ? await cache : cache //初次非promise
        const { continueStatus: prevContinueStatus, resultCache: prevResultCache } = cacheReal //continueStatus繼續執行標識   resultCache各個行為執行結果快取
        const paramsFromCache = prevResultCache[from]
        const params = paramsFromCache ? objDeepMerge(ownParams, paramsFromCache) : ownParams // 混合兩個源的引數
        const actionFn = eventActionMap[actionKey] 
        let result = prevContinueStatus && await actionFn.call(that, params, stateObj, prevResultCache) // 顯式指定行為上下文為當前頁面vue例項
        result = result === undefined ? true : result
        const continueStatus = prevContinueStatus && !!result
        const resultCache = { ...prevResultCache, [actionKey]: result }
        return { continueStatus, resultCache }  // 更新執行標識和快取
    }, { continueStatus: true, resultCache: resCache || {} })
}

此函式通過同類管道函式grep一樣,藉助Array.reduce遍歷傳入的配置陣列(actionList),從每項配置(actionItem)中取得行為標識(actionKey)後,從通用行為庫(eventMap)和上下文中獲取到當前行為(actionFn)去執行當前行為。

不同之處在於,grep僅流轉操作後的資料.而此函式通過流轉:

1.繼續執行標誌 (continueStatus),並在執行當前行為(actionFn)前校驗此標誌,來實現出現異常後停止後續行為的操作。

2.全部行為返回的結果的快取 (resultCache),並結合從每項配置(actionItem)中獲取引數來源(from)來靈活指派其傳參,使當前行為的傳參不拘泥於僅為上一行為執行結果。

另外對於當前事務為非同步的情況,我們利用async的特性(1.表象為同步處理 2.返回promise ),並將其用到reduce處理函式中。最終將reduce中每項的處理函式揉為一個整體promise

現在我們的流程控制函式初具雛形,其呼叫方式如下:

function deleteUser(that, params = {}) {
    const { id, name } = params
    const config = [
        {
            actionKey: 'showConfirmModal', msg: `確認刪除員工:【${name}】?`
        },
        {
            actionKey: 'simpleReq',
            url: '/user/transfer/removeUser',
            data: { userId: id },
            otherOption: {
                errorMsg: '使用者刪除失敗!',
                succMsg: '使用者刪除成功!'
            }
        },
        {
            actionKey: 'initTable',
        },
    ]
    actionFnSimple.bind(that, config)
}

其中showConfirmModal,simpleReq取自通用行為集, initTable取自事件觸發元件的methods, 如下:

const eventActionMap = { // 擷取部分通用行為集
     showConfirmModal(params) {
        const { msg = '確認執行當前操作?', title = '提醒' } = params
        return new Promise(resolve => {
            const onOk = () => {resolve(true)}
            const onCancel = () => {resolve(false)}
            Modal.confirm({ title, content: msg, onOk, onCancel })
        })
    },
     async simpleReq(params) {
        let { url, data, resPath = [], otherOption } = params
        const res = await fetchApi(this, url, data, otherOption)
        const resData = getIn(res, resPath, {})
        if (!res) return false
        return { data: resData }
    },
}

4.更靈活的流程控制

雖然我們的流程控制函式可以處理簡單需求,但是將其應用於處理複雜需求還需要解決以下幾點問題:

1.行為只能通過通用行為庫及元件上下文提供,過於單一。如map後端資料這種無需複用的行為存入行為庫較為不妥,又因其不屬於元件邏輯,故也不能放入元件中。因此需建立使用者手動指定行為(customFn)以解決此問題。

2.由於行為結果快取以actionkey作為唯一標識,流程中若引用多個通用行為會造成同名結果快取被覆蓋,,故引入別名(alias)機制以區分引用多個通用行為結果快取的問題。

2.行為傳參通過from標誌指定,,from僅能指定單行為。如果有從多處表單取值校驗並請求介面的需求,則無法實現。一種解決方案是判斷from內容,若為全量標誌(建議用symbol建議避免與actionKey衝突),則將各行為結果快取(resultCache)傳入。但此解決方案會向行為傳入冗餘引數。 另一種方案是允許from為陣列,通過from陣列中的actionkey去resultCache中取得所需行為的結果,,再傳入當前行為。兩種方案皆可實現此需求,,鑑於第二種方案沒有冗餘傳參,此處我們採用方案二。

據此我們得到流程控制函式如下:


export async function actionFnSimple(actionList, eventMap) {
    const that = this 
    const defaultEventMap = { ...eventMapSrc, ...this }
    const eventActionMap = eventMap || defaultEventMap
    const result = await actionList
    .reduce(async (cache, actionItem) => {
        const { actionKey, from = '', customFn, alias } = actionItem
        const ownParams = getRestObj(actionItem, ['actionKey', 'from', 'customFn'])
        const cacheReal = cache.then ? await cache : cache 
        const { continueStatus: prevContinueStatus, resultCache: prevResultCache } = cacheReal 
        const paramsFromCache = Array.isArray(from) ? from.reduce((dist, fromItem) => ({ ...dist, [fromItem]: prevResultCache[fromItem] }), {}) : prevResultCache[from] // 新增支援指定多個from
        const params = paramsFromCache ? objDeepMerge(ownParams, paramsFromCache) : ownParams
        const actionFn = isFunction(customFn) ? customFn : eventActionMap[actionKey] // 新增支援自定義行為
        let result = prevContinueStatus && await actionFn.call(that, params, stateObj, prevResultCache) 
        result = result === undefined ? true : result
        const continueStatus = prevContinueStatus && !!result
        const resultCache = { ...prevResultCache, [alias || actionKey]: result } // 新增別名機制
        return { continueStatus, resultCache } 
    }, { continueStatus: true, resultCache: resCache || {} })
}

5.錯誤處理機制

我們的流程處理函式雖然可以處理複雜邏輯,且可以通過在流程節點的行為裡返回false去截停流程。但在截停流程後,並未對使用者作出反饋。且當行為產生異常時(邏輯錯誤返回false),並不能及時反饋給使用者,也並未在控制檯輸出日誌, 這樣對使用者、開發者都不友好。

故當流程節點行為返回false時,我們引入錯誤處理機制如下:

  • 觸發此節點配置檔案中的onError函式
  • 向控制檯輸出錯誤日誌
export async function actionFnSimple(actionList, eventMap) {
    const that = this
    const defaultEventMap = { ...eventMapSrc, ...this }
    const eventActionMap = eventMap || defaultEventMap
    const result = await actionList
    .reduce(async (cache, actionItem) => {
        const { actionKey, from = '', customFn, alias } = actionItem
        const ownParams = getRestObj(actionItem, ['actionKey', 'from', 'customFn'])
        const cacheReal = cache.then ? await cache : cache
        const { continueStatus: prevContinueStatus, resultCache: prevResultCache } = cacheReal 
        const paramsFromCache = Array.isArray(from) ? from.reduce((dist, fromItem) => ({ ...dist, [fromItem]: prevResultCache[fromItem] }), {}) : prevResultCache[from]
        const params = paramsFromCache ? objDeepMerge(ownParams, paramsFromCache) : ownParams 
        const actionFn = isFunction(customFn) ? customFn : eventActionMap[actionKey] 
        let result = prevContinueStatus && await actionFn.call(that, params, stateObj, prevResultCache)
        result = result === undefined ? true : result
        const isError = prevContinueStatus && !result
        if(isError && onError){ // 新增錯誤處理機制
            const errorIsText = typeof onError === 'string'
            const errorConsoleText = `CONTEXT_NAME: ${that.name};ACTION_NAME: ${alias||actionKey}`
            console.log('ACTION_FN_ERROR', errorConsoleText, params, result)
            if(onError){
                errorIsText ? (()=>{that.$Message && that.$Message.error(onError)})() : onError()
            }
        }
        const continueStatus = prevContinueStatus && !!result
        const resultCache = { ...prevResultCache, [alias || actionKey]: result }
        return { continueStatus, resultCache }
    }, { continueStatus: true, resultCache: resCache || {} })
}

6.TODO

鑑於繼續執行標誌及行為結果快取的共享特性, 可將流程處理函式主體作為方法封入class,繼續執行標誌及行為結果快取可作為class屬性來實現其共享。

流程處理函式核心通過promise流來實現,可改用事件流的形式。