why axios?
前后端交互最常见的就是 http 请求,为了提高效率,需要对 http 请求进行封装,目前的现代开发过程中,可以使用 Axios,一种对于 http 请求的封装,或者是fetch,全新的异步请求api,本文主要是介绍我们项目中是如何根据后端返回的类型,对请求进行封装。
Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js。api 简单,返回一个 Promise 对象,以供异步的处理。
Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
事实上两种都可以,相信你看了这篇文章之后也可以自己封装一下 fetch,甚至可以使用适配器模式去一统两种 api。那下面就看看是怎么对 axios 进行封装的。
Requests
首先先看看实际生产中,axios 需要做什么工作:
拦截器,错误处理
axios 在使用的过程中需要生成一个 axiosBase 实例,从开始发送请求到收到响应可以分成以下几个过程:
- 发起请求
axiosInstance.get() - 进入请求拦截器
axiosBase.interceptors.request.use(...requestIntercepter); - (server) 服务端进行响应
- 进入响应拦截器
axiosBase.interceptors.response.use(responseIntercepter); - 返回响应,在业务中进行使用。
可以看出除了请求和响应之外,axios 提供的最多的配置就是请求拦截和响应拦截,程序设计的目的就是写出可维护并且能复用的代码,因此在两个拦截器中类似管道做通用的处理。
请求和响应
- 业务中常见的有
GET/POST/PUT请求,post请求又会根据content-type分成两种,针对这些变化的量,锚定住代码中不变的量,需要进行设计。 - 在常见的业务中,可能会是使用
access-token的方式进行鉴权,在请求的拦截器中,可以拿到config参数,可以添加认证信息 - 对于返回的响应报文,由于一般的返回报文是一样的,在响应拦截器中对响应进行第一步的通用处理,减少业务端的重复代码。
业务异常 VS Http 异常
响应拦截中,最常见的就是对异常情况进行处理,由于 axios 返回的是一个 Promise对象,因此要对返回的结果进行处理判断,之后返回 Promise.reject / Promise.resolve; 对于 http 的异常来说,由于本身就是一个 error,一般会放在 Promise.catch 里面去处理。
Wrapping
生成实例
一般来说 baseURL 是不太会改变的,如果项目如果是比较稳定的话,可以把全局的设置也写上,如: withCredentials
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.withCredentials = true;
const baseURL = NODE_ENV === 'development' ? '/api' : VUE_APP_PROD_API;
// 基本的axios实例
const axiosBase = axios.create({
baseURL: baseURL,
});
如果项目中依赖多个 api,那么这里可以生成多个实例,配置不同的 baseURL (开发中需要配置对于的 proxies).
// next.js 之类的 jamstack,可以自己生成 api routes 的,具有不同的 backend
const axioRoutes = axios.create({
baseURL: '/api-routes',
});
拦截器
拦截器的主要功能就是对请求和响应进行处理,包装,最常见的就是附带 token 进行鉴权的操作:
axiosBase.interceptors.request.use((config: AxiosRequestConfig) => {
const token = sessionStorage.getItem('token');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axiosBase.interceptors.response.use(
(res: AxiosResponse<IResponse<any>>) => {
if (!res) {
return false;
}
if (Object.prototype.hasOwnProperty.call(res.data, 'token')) {
sessionStorage.setItem('token', res.data.token as string);
}
return res;
},
(err: AxiosError<{ errorMessage: string; success: boolean }>) => {
// handle the error
return Promise.reject(err);
}
);
处理异常
请求一个很重复的操作就是处理异常,一般来说异常都很有规律性,可分成业务操作错误导致的业务异常和由于请求失败导致的 HTTP 异常。
业务异常
AxiosInstance 会返回一个 Promise,对于业务异常都是在 http returnCode 为 200 的时候。以 POST 请求为例子:
定义一个 标准的返回体:
export interface IResponse<T> {
data: T;
errorMessage: string;
success: boolean;
token?: string;
}
当返回 success 为 false 的时候,表示出现了业务异常。 由于是 Promise.then,可以在拦截器里面进行处理,也可以在实例返回中进行处理,这个地方如果不同的请求方法处理方式不同,就放到对应请求的实例里面去处理,反之就存在拦截器就可以。一般来说不同的请求方式返回的应是一致的:
axiosBase.interceptors.response.use(
(res: AxiosResponse<IResponse<any>>) => {
if (!res) {
return false;
}
if (!res.data.success) {
notify.warning(res.data.errorMessage);
}
// token...
},
(err: AxiosError<{ errorMessage: string; success: boolean }>) => {
notify.error(err.response?.data.errorMessage as string);
if (err.response?.status === 401 && isNoAuth(window.location.pathname)) {
setTimeout(() => {
window.location.href = '/person/login';
}, 1000);
}
return Promise.reject(err);
}
);
http error
对于 http 的异常,为了能够在封装中对其进行统一处理,需要对实例返回的 Promise 进行二次封装,还是以 POST 请求为例:
const httpFuncs = {
post<T>(
url: string,
data: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<IResponse<T>>> {
return new Promise((resolve, reject) => {
axiosInstance
.post(url, data, config)
.then((res: AxiosResponse<IResponse<T>>) => resolve(res))
// http error
.catch((err: AxiosError<IErrorProps>) => {
console.log(err.response);
});
});
},
};
在 Promise.catch中,处理返回的 http error, 主要是 401 和其他的 500 错误,这样就无需在具体的业务中关心这些异常的处理了。
More
到此,基本就是完成了对 axios 等请求库的常规封装。
设计模式?
或许可以使用一个 httpFactory 来对 http 请求进行统一的管理,这样就不会在从 mock 升级到正式的情况都时候,要到每一个实例里面去修改了。
enum enumType {
BASE,
MOCK,
}
export class HttpFactory {
public static getHttp(type: enumType) {
switch (type) {
case enumType.BASE:
return http;
case enumType.MOCK:
return httpMock;
default:
return http;
}
}
}
使用的时候就直接使用 HttpFactory.getHttp(enumType.MOCK).post<IUserInfo>('/userinfo'), 这样升级的时候,直接把枚举修改一下即可。
实际生产中,可以使用配置文件来写这个枚举,这样就可以做到统一的管理。
End
总的来说最适合应用的才是最好的,本文只是介绍了我们在改造为 ts + axios + next.js 时候的经验,事实上对于 next.js,可以使用 useSWR 等库,也进行了很好的封装。
