请求的并发控制,变为串行

前言


本次的背景为由于某些原因,每个WEB的请求必须携带唯一的标识Token,而这个标识也是用接口请求回来的,要实现每个接口携带唯一标识,就必须串行所有请求。

InvokeAPI


众所周知,目前WEB请求库Axios无出其右,我们的项目也不例外,目前大部分项目都对其进行再次包装,将url对象作为入参传入后,返回promise形式的请求对象InvokeAPI服务于业务。

  • url对象
const API = {
    common: {
        getToken: `/api/token`, // 获取token
    },
}
  • InvokeAPI对象
// /js/net/invoke.js

import axios from 'axios'
import InvokeApi from './invoke-api'


const initOptions = {
    baseURL: '/api',
    // ...其他配置
}
// axios拦截器
const interceptors = {
    request: [
        config => {
            // 自定义处理
            return config
        },

        error => {
            // 自定义处理
            return Promise.reject(error)
        },
    ],
    response: [
        response => {
            // 自定义处理

            if (response && response.data && response.data.error == 1) {
                response?.config?.toast != false && Toast.show('服务器出小差了。')
            }
        },
        error => {
            // 自定义处理
            error?.config?.toast != false && Toast.show('服务器出小差了')

            return Promise.reject()
        },
    ],
}


const InvokeAPI = InvokeApi(axios)(API, {
    options: initOptions,
    interceptors,
})


export default InvokeAPI
  • invoke-api
// /js/net/invoke-api/index.js



import { createRequestInstance } from './request'


const createRequest =
    axios =>
    (obj, config = {}) => {
        const { get, post } = createRequestInstance(
            {
                ...config,
                options: {
                    baseURL: '/api',
                    ...(config?.options || {}),
                },
            },
            axios
        )
        const result = {}
        const array = Object.entries(obj)

        for (let index = 0; index < array.length; index++) {
            const [key, value] = array[index]

            if (key.startsWith('post')) {
                result[key] = (data, options) => {
                    const res = post(value, data, options)

                    result[key].cancel = res.cancel

                    return res
                }
            }
            if (key.startsWith('get')) {
                result[key] = (data, options) => {
                    const res = get(value, data, options)


                    result[key].cancel = res.cancel
                    return res
                }
            }
        }

        return result
    }

const InvokeApi = (obj, config) => {
    const result = {}
    const array = Object.entries(obj)

    for (let index = 0; index < array.length; index++) {
        const [key, value] = array[index]

        if (typeof value == 'object') {
            result[key] = createRequest(axios)(value, config)
        }
    }

    return result
}

export default InvokeApi
// /js/net/invoke-api/request.js

import Qs from 'qs'



const ResponseCode = {
    InterceptorRequestAbort: -101,
    InterceptorRequestError: -102,
    InterceptorResponseAbort: -103,
    InterceptorResponseError: -104,
    RequestCancel: -105,
}

const RequestInterceptorAbort = 'RequestInterceptorAbort'
const ResponseInterceptorAbort = 'ResponseInterceptorAbort'
const defaultResponseInterceptorError = error => ({
    rescode: ResponseCode.InterceptorResponseError,
    resmsg: '服务器出小差了',
    data: error,
})

const proxyInterceptorRequestBeforeFn = fn => async opt => {
    const res = await fn?.(opt)


    if (res === false) {
        return Promise.reject({
            ...opt,
            interceptorsResult: {
                abort: true,
                msg: RequestInterceptorAbort,
            },
        })

    }

    return res || opt
}


const proxyInterceptorRequestErrorFn = fn => async opt => {
    const res = await fn?.(opt)

    if (res === false) {
        return Promise.resolve({
            ...opt,
            interceptorsResult: {
                abort: true,
                msg: RequestInterceptorAbort,
            },
        })
    }


    return res || { rescode: ResponseCode.InterceptorRequestError, resmsg: '服务器出小差了', data: opt }
}

const proxyInterceptorResponseBeforeFn = axios => fn => async opt => {
    if (opt?.interceptorsResult?.abort) {
        return opt?.data
    }
    if (axios.isCancel(opt)) {
        opt = { rescode: ResponseCode.RequestCancel, resmsg: 'request abort by SDK' }
    }

    const res = await fn?.(opt)

    if (res === false) {
        return { ...(opt?.data || {}), rescode: ResponseCode.InterceptorResponseAbort, resmsg: '服务器出小差了' }
    }

    return res || opt?.data || {}
}

const proxyInterceptorResponseErrorFn = axios => fn => async opt => {
    if (opt?.interceptorsResult?.abort) {
        return { rescode: ResponseCode.InterceptorRequestAbort, resmsg: '服务器出小差了', data: {} }
    }
    if (axios.isCancel(opt)) {
        opt = { rescode: ResponseCode.RequestCancel, resmsg: 'request abort by SDK' }
    }
    const res = await fn?.(opt)

    if (res === false) {
        return Promise.reject({
            ...opt,
            interceptorsResult: {
                abort: true,
                msg: RequestInterceptorAbort,
            },
        })
    }

    return res || defaultResponseInterceptorError(opt)
}

/**
 * 合并拦截器
 */
const mergeInterceptors =
    axios =>
    ({ request = [], response = [] }) => ({
        request: [proxyInterceptorRequestBeforeFn(request?.[0]), proxyInterceptorRequestErrorFn(request?.[1])],
        response: [
            proxyInterceptorResponseBeforeFn(axios)(response?.[0]),
            proxyInterceptorResponseErrorFn(axios)(response?.[1]),
        ],
    })

const createRequestInstance = ({ options, interceptors }, axios) => {
    const interceptorsObj = mergeInterceptors(axios)(interceptors || {})
    const axiosInstance = axios.create({
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
            'X-Requested-With': 'XMLHttpRequest',
        },
        ...options,
    })

    // 请求拦截器
    axiosInstance.interceptors.request.use(interceptorsObj.request[0], interceptorsObj.request[1])

    // 响应拦截器
    axiosInstance.interceptors.response.use(interceptorsObj.response[0], interceptorsObj.response[1])

    const request = opt => {
        const config = {
            url: '',
            method: 'get',
            data: {},
            responseType: 'json',
            requestType: 'form',
            headers: {},
            paramsSerializer: params => Qs.stringify(params, { arrayFormat: 'repeat' }),
            ...opt,
        }

        if (config.method.toLowerCase() == 'post') {
            if (
                config?.headers?.['Content-Type'] != 'multipart/form-data' &&
                config.requestType.toLowerCase() != 'json'
            ) {
                config.data = Qs.stringify(config.data, { arrayFormat: 'repeat' })
            }
        } else {
            config.params = config.data
        }

        let controller

        if (config?.requireCancel) {
            controller = new AbortController()
            config.signal = controller.signal
        }

        const req = axiosInstance.request(config)
        let cancel

        if (config?.requireCancel) {
            cancel = () => {
                controller.abort()
            }
            req.cancel = cancel
        }

        return req
    }

    const get = (url = '', data = {}, opt = {}) => {
        return request({ data, url, ...opt, method: 'get' })
    }

    const post = (url = '', data = {}, opt = {}) => {
        return request({ data, url, ...opt, method: 'post' })
    }

    return {
        get,
        post,
    }
}

export {
    createRequestInstance
}
export default createRequestInstance

  • 注入全局
// InvokeAPI使用inject/provide方式注入全局
    // ...vite配置
    inject({
        InvokeAPI: '/js/net/invoke',
    }),
    // ...vite配置


    // ...webpack配置
    new webpack.ProvidePlugin({
        InvokeAPI: ['@/js/net/invoke', 'default'],
    })

    // ...webpack配置
  • 使用
// ...业务代码
const getPageInfo = useMemoizedFn(async val => {
    const { code, message, data } = await InvokeAPI.common
        .getPageInfo({
            id: val,
        })
        .catch(e => {
            console.log('getPageInfo catch')
        })
        .finally(e => {
            console.log('getPageInfo finally')
        })
})
// ...业务代码

axios使用的版本为0.18.1~0.22.3之间

Token


每个请求唯一Token服务于部分项目,因为一直无法解决唯一性的问题,并没有全量匹配请求,大部分使用Axios原生提供的“拦截器”整合队列方式仅能部分实现串行,由于浏览器并行请求导致Token唯一性失去了生产一个消费一个的基本前提。

  • 之前的解决方案
// 逻辑位于axios的“拦截器”
// /js/net/invoke.js



// 拆分以避免循环调用问题
const tokenInvoke = InvokeApi(axios)(API, {
    interceptors: {
        request: [
            opt => {

                return opt
            },
        ],
    },
})

// 获取token
const getToken = () => {
    return tokenInvoke.common
        .getToken({})
        .then(res => {
            if (res.code == 0) {
                return res.token
            }

            return ''
        })
        .catch(err => {

            return ''
        })

}
// axios拦截器
const interceptors = {
    request: [
        async config => {
            // 自定义处理
            
            const token = await getToken()

            // token 接口出错,阻断后续请求
            if (bockRequest(token)) return false
            // 添加请求头
            config.headers['X-TOKEN'] = token
            return config
        },
    // ...
}

并发控制


  • 核心实现
class SerialControl {
    constructor(tasks = [], limit = 1) { // 由于需要串行,此处每次仅执行一个task
        this.tasks = tasks.slice() // 避免修改原数据
        this.queue = new Set() // 任务队列
        this.limit = limit // 最大并发数
    }


    nextTask(task) {
        this.queue.delete(task)
        if (this.tasks.size > 0) {
            this.runTask() // 继续
        }
    }

    runTask() {
        // 边界判断
        if (this.tasks.length == 0) return


        // 当任务队列有剩余,继续
        while (this.queue.size < this.limit) {
            const task = this.tasks.shift() // 先入先出


            if (!task) {
                return
            }
            this.queue.add(task) // 添加当前任务
            task.token.req().then(res => {
                if (task.token.resultFormat(task.config, res) == false) {
                    this.nextTask(task)

                    return
                }
                task.resolve(
                    task.req(task.config).finally(() => {
                        this.nextTask(task)
                    })
                )
            })
        }
    }


    addTask(task) {
        // 添加任务
        this.tasks.push(task)
        // 执行
        this.runTask()
    }

}
  • 使用
// 实例化后即可增加任务并自动触发执行
const serialControl = new SerialControl()



// 增加任务
new Promise(resolve => {
    serialControl.addTask({
      token: options.token, // 获取token的函数
      config, // 当前请求的配置
      req: axiosInstance.request, // 创建请求的函数,axios版本不同会有出入
      resolve, // 拿到token后继续下一步
    })

})

InvokeAPI改造


axois拦截器为运行时执行,故token配置需放在初始化配置处

  • 改造初始化配置
// /js/net/invoke.js




const initOptions = {
    baseURL: '',
    // ...其他配置
    // 增加token配置
    token: {
        req: getToken,
        resultFormat: (config, token) => {
            console.log('token', token)
            // token 接口出错,阻断后续请求
            if (!token) return false
            // 添加请求头
            config.headers['X-TOKEN'] = token
        },

    },
}


// axios拦截器
const interceptors = {
    request: [
        async config => {
            // 自定义处理

            // 去除token逻辑
            // const token = await getToken()

            // // token 接口出错,阻断后续请求
            // if (bockRequest(token)) return false
            // // 添加请求头
            // config.headers['X-TOKEN'] = token
            return config
        },
    // ...
}

  • 改造request.js核心
// /js/net/invoke-api/request.js




    // 代理axios请求实例创建过程
    // ...
    const req = options.token
        ? new Promise(resolve => {
              serialControl.addTask({
                  token: options.token,
                  config,
                  req: axiosInstance.request,
                  resolve,
              })
          })
        : axiosInstance.request(config)
    // ...

改造完成,本文仅贴出核心实现,大家可自己根据需要调整拓展

总结


首先axios作为目前web最流行的请求库,随着不断迭代,已经逐渐支持请求取消、缓存、拦截等特性;但对于复杂多变的业务场景来说,依然需要根据具体场景进行二次封装;本文的InvokeAPI为便于使用,将axios提供的“拦截器”代理以实现跳过请求,并将请求取消作为静态方法绑定至函数;本次进一步整合并发控制能力以支持请求串行发送。

处于多线程时代却用串行,是人性的泯灭还是道德的沦丧?

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYzgAz4u' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片