前言
本次的背景为由于某些原因,每个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提供的“拦截器”代理以实现跳过请求,并将请求取消作为静态方法绑定至函数;本次进一步整合并发控制能力以支持请求串行发送。
处于多线程时代却用串行,是人性的泯灭还是道德的沦丧?
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END