在我以往的面试中,听到候选人最多的就是项目中二次封装axios
,但是当真正深入挖掘,往往得不到有用的信息。 那么面试官是想听到什么样的亮点呢?这篇文章我们重点分析一下,并且也可以封装自己的axios
请求库。
需求
在项目中,我们可能存在这些痛点:
- 接口统一管理
- 支持多host问题
- 支持区分env
- 支持restful风格
- 支持取消请求
- 支持接口错误重试
- 支持缓存
- 支持限流
请求方法的统一封装
代码语言:javascript复制export class Apis{
public common: RequestOptions;
// 默认的server配置
public base!: string;
// server服务的集合
public serverMap: ServerMap;
// 对象形式的请求方法集合
public apiMap: ApisMap;
// 挂载所有请求方法的集合对象
public apis: ApisInstance;
// axios实例化对象
public instance: AxiosInstance;
constructor(common?: RequestOptions, serverMap?: ServerMap, apiMap?: ApisMap) {
}
public get<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
request = { ...request, method: 'GET' };
return this.request(url, request);
}
public delete<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
request = { ...request, method: 'DELETE' };
return this.request(url, request);
}
public post<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
request = { ...request, method: 'POST' };
return this.request(url, request);
}
public put<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
request = { ...request, method: 'PUT' };
return this.request(url, request);
}
public patch<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
request = { ...request, method: 'PATCH' };
return this.request(url, request);
}
public request<T extends Record<string, any> = any>(url: string, request: RequestOptions): Promise<RestyResponse<T>> {
const rest = request.rest || {};
let path = url;
if (Object.keys(rest).length) {
path = this.restful(url, rest);
}
// 合并公共配置
const options = { ...this.common, ...request };
return this.instance.request({
...options,
url: path,
});
}
}
接口统一管理
在项目中,实际每个请求的写法都是一样的,开发过程中,我不想在每个页面都重复写请求方法,我想通过JSON配置的方式做接口统一管理,比如:
在Home Module下,新建apis.ts文件:
export default {
getBaseInfo: {
method: 'get',
url: '/base/get'
},
getBaseRestInfo: {
method: 'get',
url: '/base/info'
}
}
实现
:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import {
ApisMap,
ServerMap,
ApisInstance,
ApisConfig,
ResolvedFn,
RejectedFn,
Middleware,
Rest
} from './types'
class Apis {
base: string
serverMap: ServerMap
apiMap: ApisMap
instance: ApisInstance
axiosInstance: AxiosInstance
constructor(serverMap: ServerMap, apiMap: ApisMap, common?: AxiosRequestConfig) {
/**
* 支持公共配置
*/
this.axiosInstance = axios.create(common)
this.serverMap = serverMap
this.apiMap = apiMap
this.instance = {}
this.base = this.getDefault()
this.combine2Request()
}
/**
* 获取默认的配置
*/
getDefault(): string {
let base = ''
for (const key of Object.keys(this.serverMap)) {
/**
* 找到默认的配置值
*/
if (this.serverMap[key].default) {
base = key
}
}
if (!base) {
console.error('apis: 找不到默认服务器配置')
}
return base
}
combine2Request(): void {
for (const key of Object.keys(this.apiMap)) {
this.instance[key] = (config?: ApisConfig) => {
let result: ApisConfig = this.apiMap[key]
if (config) {
result = this.rest2Combine(this.apiMap[key], config)
}
return this.axiosInstance.request(result)
}
}
}
}
export default createInstance
支持多host
应用可能需要跟多个服务交互,这时候涉及多host,我们想统一处理:
- 定义serverMap:
serverMap
对象定义了两个服务:baseServer
和api-test
。- 对于每个服务,都提供了一个
baseMap
对象来描述不同环境下的base URLs。 baseServer
有一个default
属性设置为true
,表示它可能是默认选择的服务器。- 定义apiMap:
apiMap
对象定义了两个API:getBaseInfo
和getBaseRestInfo
。- 每个API都有一个HTTP方法(
method
)和一个URL路径(url
)。
import createInstance from 'apis'
import { ApisMap } from 'apis/types'
const serverMap = {
baseServer: {
baseMap: {
localprod: '',
prod: 'https://wwww.baidu.com',
stage: 'https://wwww.baidu.com',
test: 'https://wwww.baidu.com',
dev: 'https:/wwww.baidu.com',
local: 'http://127.0.0.1:4320',
baseURL: 'https://localhost:8080'
},
default: true
},
'api-test': {
baseMap: {
localprod: '',
prod: 'https://www.baidu.com',
stage: 'https://www.baidu.com',
test: 'https://www.baidu.com',
dev: 'https:/www.baidu.com',
local: `http://127.0.0.1:4320`,
baseURL: 'https://localhost:8080'
}
}
}
const apiMap: ApisMap = {
getBaseInfo: {
method: 'get',
url: '/base/get'
},
getBaseRestInfo: {
method: 'get',
url: '/base/get/:id/kill/:test'
}
}
let apis = createInstance(serverMap, apiMap)
apis.getBaseInfo({ params: { name: 'linwu' } }).then(res => {
console.log(res)
})
实现
/**
* 给个请求
* 配置正确的baseURL
* 如果没有baseURL就读默认的
*/
formatConfigUrl(): void {
for (const key of Object.keys(this.apiMap)) {
const item = this.apiMap[key]
if (!item.server) {
item.server = this.base
}
this.apiMap[key] = { ...this.serverMap[item.server], ...item }
}
}
支持区分env
代码语言:javascript复制这样配置有个好处。有时候我们新增的接口想走本地mock,比如后端还未实现,已有的接口走线上数据,这样以配置,就简单明了多了
baseMap: {
prod: 'https://wwww.baidu.com',
test: 'https://wwww.baidu.com',
local: 'http://127.0.0.1:4320',
baseURL: 'https://localhost:8080'
},
代码语言:javascript复制export default {
getBaseInfo: {
method: 'get',
url: '/base/get',
env:'local'
},
getBaseRestInfo: {
method: 'get',
url: '/base/get/:id/kill/:test',
env:'test'
}
}
支持restful风格
代码语言:javascript复制export default {
getBaseInfo: {
method: 'get',
url: '/base/get',
env:'local'
},
getBaseRestInfo: {
method: 'get',
url: '/base/get/:id/qs/:test',
env:'test'
}
}
实现
:
/**
* 替换restful请求中的url
*/
restful(url: string, rest: Rest): string {
/**
* [xyz]一个字符集合。匹配方括号中的任意字符
* 比如正则表达式是[abcd]==>匹配brisket"中的‘b’
*/
const regex = /:[^/]*/g
/**
* 一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。
* 另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式,那么这个方法将被多次调用,每次匹配都会被调用。
* 匹配模式是这样的:[^/]* 为一个整体 全局g下多次匹配 也就是多次调用fn
* [^/]匹配得到的是一个字符 只要匹配的url出现的一个字符在 [^/]中出现就匹配成功 但是是单个的 所以要多次匹配
*/
return url.replace(regex, p => {
console.log(p)
/**
* :id ===>返回id
*/
const key = p.slice(1)
if (rest[key]) {
return rest[key]
}
return p
})
}
支持取消请求
代码语言:javascript复制从
v0.22.0
开始,Axios 支持以 fetch API 方式——AbortController
取消请求:
// 1. 支持取消请求
const cancelMap: { [key: string]: any } = {}
Apis.reqMiddleware.push({
onFulfilled: (config) => {
const source = axios.CancelToken.source()
config.cancelToken = source.token
cancelMap[config.url!] = source
return config
}
})
createInstance.cancel = function(url: string) {
if (cancelMap[url]) {
cancelMap[url].cancel('Request was cancelled')
delete cancelMap[url]
}
}
支持重新请求
代码语言:javascript复制function createInstance(
serverMap: ServerMap,
apiMap: ApisMap,
common?: AxiosRequestConfig
): ApisInstance {
Apis.resMiddleware.push({
onFulfilled: void 0,
onRejected: function axiosRetryInterceptor(err) {
const config = err.config
/**
* 如果没有retry配置那么就不走这个拦截器
* 因为发生错误,我们还在这个拦截器中 request2 interceptor --> request1 interceptor-->dispatchRequest--> response1 interceptor--> response2 interceptor
* 这时候在拦截器中重新发起请求把得到的响应结果发给最后的Promise
* 最后的Promise注册中我们成功和失败的业务
* 这样可以避免:其他的那个几十个.vue页面的 this.$axios的get 和post 的方法根本就不需要去修改它们的代码。
*/
if (!config || !config.retry) return Promise.reject(err)
/**
* 已经尝试retry的次数
*/
config.__retryCount = config.__retryCount || 0
if (config.__retryCount >= config.retry) {
return Promise.reject(err)
}
config.__retryCount = 1
/**
* 等待多少秒后才进行retry
*/
const backoff = new Promise(function(resolve) {
setTimeout(function() {
resolve()
}, config.retryDelay || 1)
})
return backoff.then(function() {
/**
* 返回结果
* 是个Promise对象
* 不reject 返回的数据被成功的回调拿到
*/
return axios(config)
})
}
})
const apis = new Apis(serverMap, apiMap, common)
/**
* new过后清空以前的拦截器队列
* 因为new完一个实例过后,拦截器信息可以作废
* 所以要确保你实例化之前先注册拦截器
*/
Apis.reqMiddleware = []
Apis.resMiddleware = []
return apis.instance
}
支持缓存
代码语言:javascript复制// 3. 支持接口缓存
const cacheMap: { [key: string]: any } = {}
Apis.resMiddleware.push({
onFulfilled: (response) => {
cacheMap[response.config.url!] = response
return response
}
})
Apis.reqMiddleware.push({
onFulfilled: (config) => {
if (cacheMap[config.url!]) {
throw new axios.Cancel('Request was cached')
}
return config
}
})
支持限流
代码语言:javascript复制// 2. 支持接口限流
const LIMIT = 5 // 例如,5个请求每秒
const INTERVAL = 1000 // 1秒
let tokens = LIMIT
setInterval(() => { tokens = LIMIT }, INTERVAL)
Apis.reqMiddleware.push({
onFulfilled: (config) => {
if (tokens > 0) {
tokens--
return config
} else {
throw new Error('Rate limit exceeded')
}
}
})
总结
上述的apis是我实际项目中封装的,当然文章给的大部分是伪代码,但是思路是对的,大家可以按照这个思路封装自己项目中的请求库,然后发布成npm包