The author | loss

1. Form page of work order system

A work order system is a network software system that manages, maintains, and tracks a series of problems and requests tailored to the needs of different organizations, departments, and external customers. Mostly used for recording, processing and tracking the completion of a job. In order to make customer service students deal with customer problems in an orderly and coordinated way, the customer service technical team has created a planetary work order system with multi-channel integration, flexible configuration and easy circulation.

As the work order system is dependent on the background system, the page display is mainly based on the following table. To unify the table page behavior, it is uniformly encapsulated as a table page component.

The table page component is divided into the following parts:

  1. Filter area

  2. Form the main body

  3. The paging component

  4. Action buttons

  5. Inline action links

The page component can control its display state, filter item area and form body content by passing the flag to the table page component and realize different logic between different pages by passing the table column configuration and form item configuration to the table page component.

However, because the logic of action buttons and in-line action links is basically different from page to page, they cannot be enclosed into the table page component. Therefore, the event processing part of the logic is decoupled from the list page components. We use the button operation names of different table pages as unique identifiers, register the whole system event set globally in the entry file, and establish a corresponding relationship between identifiers and events in the event set to achieve the above requirements.

Then we find that the logic of these buttons is basically the aggregation of the following types of behavior:

  • A dialog box is displayed asking users to perform operations

  • The pop-up form class component retrieves a portion of the data

  • Form and other data verification

  • Call the interface to pull/update the data

And, more importantly, they’re serial. For example, editing user actions:

  • Pull user data from the interface

  • The pop-up form component fills in the data and allows the user to modify it

  • Verify user submitted data

  • Call the interface to submit the data.

In these steps, if there is a problem with the behavior of one node in the process, the logical solution is to stop the process and throw an error response to the developer or user. So according to this processing idea, we establish the process control scheme of work order system.

2. Simple plumbing

Pipes in Vue and Angular are the first things that come to mind when we think about executing a process behavior sequentially and taking the execution result of the current behavior as a parameter to the next behavior. Let’s implement a simple pipe using array.reduce.


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

Copy the code

The logic of this function is very simple, that is, the set of behaviors is traversed, each behavior is executed, and the result of the last behavior is passed to the next behavior by borrowing the Reduce feature. Based on this, we use class pipe function to realize simple flow control.

Call as follows:


     
  1. Function filterLessThanFive(srcData){//[1,3,4,6,7,8] => [1,3,4]

  2.    return srcData.filter(item => Number.isInteger(item) ? item > 5 : false)

  3. }

  4. Function sum(srcData){// [1,3,4] => 1+3+4 => 8

  5.    return srcData.reduce((dist,item) => dist+(parseInt(item, 10)), 0)

  6. }

  7. function plusOne(srcData){ // 8+1 => 9

  8.    return srcData+1

  9. }

  10. const fnList = [filterLessThanFive, sum, plusOne]

  11. Const srcData =,3,4,6,7,8 [1]

  12. const res = grep({context:this, fnList, srcData})

Copy the code

Note:

1. Because grep internally forces to change the context of fnList, the internal functions of fnList cannot be context-bound with the arrow function and bind

2. In order to flexibly adapt to requirements, it is not mandatory to standardize the data format of flow, or cause performance waste such as multiple traversal. Due to the small amount of front-end data, performance issues are not considered.

3. Process control

But pipelines have two problems that they can’t solve:

1. Can’t handle asynchronous requirements: if a requirement returns a result to an interface, it takes the result as an argument to the next function. If there are asynchronous functions in the fnList (process behavior set), then the asynchronous process behavior function cannot flow data or only promise objects in the process, which fails to fulfill the above requirements.

2. The current process node behavior can only obtain the execution result of the last process node behavior: we need a more flexible way to obtain the previous behavior result, such as cross-node acquisition and multi-node acquisition.

To solve the above two points, we preliminarily constructed transaction flow processing functions as follows:


     
  1. export async function actionFnSimple(actionList, eventMap) {

  2. Const that = this // Gets the component context

  3. const defaultEventMap = { ... eventMapSrc, ... This}// Default behavior set

  4.    const eventActionMap = eventMap || defaultEventMap

  5.    const result = await actionList

  6.    .reduce(async (cache, actionItem) => {

  7.        const { actionKey, from = '' } = actionItem

  8.        const ownParams = getRestObj(actionItem, ['actionKey', 'from'])

  9. const cacheReal = cache.then ? Await cache: cache // first non-promise

  10. const { continueStatus: prevContinueStatus, resultCache: PrevResultCache} = cacheReal //continueStatus continues to run the resultCache identifying the resultCache actions

  11.        const paramsFromCache = prevResultCache[from]

  12. const params = paramsFromCache ? ObjDeepMerge (ownParams, paramsFromCache) : ownParams // Mixes the parameters of two sources

  13.        const actionFn = eventActionMap[actionKey]

  14. Let result = prevContinueStatus && await actionFt. call(that, Params, stateObj, prevResultCache) // Explicitly specify the action context as the current page vue instance

  15.        result = result === undefined ? true : result

  16. const continueStatus = prevContinueStatus && !! result

  17. const resultCache = { ... prevResultCache, [actionKey]: result }

  18. Return {continueStatus, resultCache} // Updates the run id and cache

  19.    }, { continueStatus: true, resultCache: resCache || {} })

  20. }

Copy the code

This function iterates through the Array of configurations (actionList) with array.reduce, just like grep in the pipeline function. After obtaining the actionKey from each configuration (actionItem), Get the current behavior (actionFn) from the common Behavior Library (eventMap) and context to execute the current behavior.

The difference is that grep only flows the data after the operation. This function passes through the flow:

1. Continue with flags (continueStatus) and verify this flag before executing the current action (actionFn) to stop subsequent actions if an exception occurs.

2. The resultCache of the results returned by all actions, and the source of parameters from each configuration (actionItem) is obtained to flexibly assign its parameters, so that the parameter transmission of the current action is not limited to the execution result of the last action.

In addition, for the current transaction is asynchronous, we use the characteristics of Async (1. 2. Return promise) and use it in the reduce handler. Finally, the processing function of each item in Reduce is kneaded into an overall promise

Now our process control function is taking shape. It is called as follows:


     
  1. function deleteUser(that, params = {}) {

  2.    const { id, name } = params

  3.    const config = [

  4.        {

  5. ActionKey: 'showConfirmModal', MSG: 'Confirm to delete employee: [${name}]? `

  6.        },

  7.        {

  8.            actionKey: 'simpleReq',

  9.            url: '/user/transfer/removeUser',

  10.            data: { userId: id },

  11.            otherOption: {

  12. ErrorMsg: 'User deletion failed! ',

  13. SuccMsg: 'User deleted successfully! '

  14.            }

  15.        },

  16.        {

  17.            actionKey: 'initTable',

  18.        },

  19.    ]

  20.    actionFnSimple.bind(that, config)

  21. }

Copy the code

Which showConfirmModal, simpleReq from universal behavior, initTable from events trigger the methods of the component, as follows:


     
  1. Const eventActionMap = {// Intercepts part of the common behavior set

  2.     showConfirmModal(params) {

  3. Const {MSG = 'Confirm current operation? ', title = 'remind'} = params

  4.        return new Promise(resolve => {

  5.            const onOk = () => {resolve(true)}

  6.            const onCancel = () => {resolve(false)}

  7.            Modal.confirm({ title, content: msg, onOk, onCancel })

  8.        })

  9.    },

  10.     async simpleReq(params) {

  11.        let { url, data, resPath = [], otherOption } = params

  12.        const res = await fetchApi(this, url, data, otherOption)

  13.        const resData = getIn(res, resPath, {})

  14. if (! res) return false

  15.        return { data: resData }

  16.    },

  17. }

Copy the code

4. More flexible process control

Although our process control function can handle simple requirements, applying it to complex requirements requires solving the following issues:

1. The behavior can only be provided through the common behavior library and component context, which is too simple. For example, map back-end data, which does not need to be reused, is not suitable to be stored in the behavior library and cannot be put into the component because it does not belong to the component logic. Therefore, you need to create a user manually specified behavior (customFn) to solve this problem.

2. Since the behavior result cache is uniquely identified by ActionKey, if multiple generic behaviors are referenced in the process, the result cache of the same name will be overwritten. Therefore, the alias mechanism is introduced to distinguish the problem that multiple generic behavior result caches are referenced.

2. Action parameters are specified by the FROM flag. From can specify only a single action. If there is a requirement to validate and request an interface from multiple form values, this cannot be done. One solution is to determine the content of the FROM, and if it is a full flag (symbol is recommended to avoid collisions with actionKey), pass in the resultCache for each action. But this solution passes redundant parameters to the behavior. The other solution is to allow from to be an array. The actionkey in the FROM array is used to fetch the result of the desired behavior from the resultCache, and then pass in the current behavior. Both schemes can realize this requirement. Since the second scheme has no redundant transmission, scheme 2 is adopted here.

Accordingly, the flow control function is obtained as follows:


     
  1. export async function actionFnSimple(actionList, eventMap) {

  2.    const that = this

  3. const defaultEventMap = { ... eventMapSrc, ... this }

  4.    const eventActionMap = eventMap || defaultEventMap

  5.    const result = await actionList

  6.    .reduce(async (cache, actionItem) => {

  7.        const { actionKey, from = '', customFn, alias } = actionItem

  8.        const ownParams = getRestObj(actionItem, ['actionKey', 'from', 'customFn'])

  9.        const cacheReal = cache.then ? await cache : cache

  10.        const { continueStatus: prevContinueStatus, resultCache: prevResultCache } = cacheReal

  11. const paramsFromCache = Array.isArray(from) ? from.reduce((dist, fromItem) => ({ ... Dist, [fromItem]: prevResultCache[fromItem]}), {}) : prevResultCache[from] // New support for specifying multiple FROM

  12.        const params = paramsFromCache ? objDeepMerge(ownParams, paramsFromCache) : ownParams

  13. const actionFn = isFunction(customFn) ? CustomFn: eventActionMap[actionKey] // Added support for user-defined behavior

  14.        let result = prevContinueStatus && await actionFn.call(that, params, stateObj, prevResultCache)

  15.        result = result === undefined ? true : result

  16. const continueStatus = prevContinueStatus && !! result

  17. const resultCache = { ... PrevResultCache, [alias | | actionKey] : result} / / new alias mechanism

  18.        return { continueStatus, resultCache }

  19.    }, { continueStatus: true, resultCache: resCache || {} })

  20. }

Copy the code

5. Error handling

Our process handler functions can handle complex logic and can intercept the process by returning false in the behavior of the process node. But no feedback was given to users after the process was stopped. In addition, when the behavior is abnormal (logical error returns false), it is not timely reported to the user and does not output logs on the console, which is not friendly to users and developers.

Therefore, when the process node behavior returns false, we introduce the following error handling mechanism:

  • Raises the onError function in the configuration file for this node

  • Output error logs to the console


     
  1. export async function actionFnSimple(actionList, eventMap) {

  2.    const that = this

  3. const defaultEventMap = { ... eventMapSrc, ... this }

  4.    const eventActionMap = eventMap || defaultEventMap

  5.    const result = await actionList

  6.    .reduce(async (cache, actionItem) => {

  7.        const { actionKey, from = '', customFn, alias } = actionItem

  8.        const ownParams = getRestObj(actionItem, ['actionKey', 'from', 'customFn'])

  9.        const cacheReal = cache.then ? await cache : cache

  10.        const { continueStatus: prevContinueStatus, resultCache: prevResultCache } = cacheReal

  11. const paramsFromCache = Array.isArray(from) ? from.reduce((dist, fromItem) => ({ ... dist, [fromItem]: prevResultCache[fromItem] }), {}) : prevResultCache[from]

  12.        const params = paramsFromCache ? objDeepMerge(ownParams, paramsFromCache) : ownParams

  13.        const actionFn = isFunction(customFn) ? customFn : eventActionMap[actionKey]

  14.        let result = prevContinueStatus && await actionFn.call(that, params, stateObj, prevResultCache)

  15.        result = result === undefined ? true : result

  16. const isError = prevContinueStatus && ! result

  17. If (isError && onError){// New error handling mechanism

  18.            const errorIsText = typeof onError === 'string'

  19. const errorConsoleText = `CONTEXT_NAME: ${that.name}; ACTION_NAME: ${alias||actionKey}`

  20.            console.log('ACTION_FN_ERROR', errorConsoleText, params, result)

  21.            if(onError){

  22.                errorIsText ? (()=>{that.$Message && that.$Message.error(onError)})() : onError()

  23.            }

  24.        }

  25. const continueStatus = prevContinueStatus && !! result

  26. const resultCache = { ... prevResultCache, [alias || actionKey]: result }

  27.        return { continueStatus, resultCache }

  28.    }, { continueStatus: true, resultCache: resCache || {} })

  29. }

Copy the code

6.TODO

Because of the shared nature of continue flags and action result caches, the body of the process handler function can be enclosed as a method to the class, and continue flags and action result caches can be shared as class attributes.

The process handler core is implemented as a promise stream, which can be changed to an event stream.