引子
前后端交互最常见的就是http请求,为了提高效率,需要对http请求进行封装,目前的现代开发过程中,可以使用 Axios,一种对于http请求的封装,或者是fetch,全新的异步请求api,本文主要是介绍我们项目中是如何根据后端返回的类型,对请求进行封装。
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 的,具有不同的 backendconst 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 }>) =>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 }>) =>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>) => {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 reject(err);});});}}
在 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 httpcase enumType.MOCK:return httpMockdefault:return http}}}
使用的时候就直接使用 HttpFactory.getHttp(enumType.MOCK).post<IUserInfo>('/userinfo'), 这样升级的时候,直接把枚举修改一下即可。
实际生产中,可以使用配置文件来写这个枚举,这样就可以做到统一的管理。
End
在线演示
总的来说最适合应用的才是最好的,本文只是介绍了我们在改造为 ts + axios + next.js 时候的经验,事实上对于 next.js,可以使用 useSWR 等库,也进行了很好的封装。
