TypeScript实战之用TS封装Axios
Axios几个常用类型
在使用TypeScript
封装Axios
之前我们先来看看Axios
几个重要的类型。
AxiosRequestConfig
AxiosRequestConfig
是我们使用axios
发送请求传递参数的类型。当然它也是我们请求拦截器里面的参数类型。
1
| axios(config: AxiosRequestConfig)
|
可以看到,这个config
里面的参数还是挺多的。我们常用的有url、method、params、data、headers、baseURL、timeout
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| export interface AxiosRequestConfig { url?: string; method?: Method; baseURL?: string; transformRequest?: AxiosTransformer | AxiosTransformer[]; transformResponse?: AxiosTransformer | AxiosTransformer[]; headers?: any; params?: any; paramsSerializer?: (params: any) => string; data?: any; timeout?: number; timeoutErrorMessage?: string; withCredentials?: boolean; adapter?: AxiosAdapter; auth?: AxiosBasicCredentials; responseType?: ResponseType; xsrfCookieName?: string; xsrfHeaderName?: string; onUploadProgress?: (progressEvent: any) => void; onDownloadProgress?: (progressEvent: any) => void; maxContentLength?: number; validateStatus?: ((status: number) => boolean) | null; maxBodyLength?: number; maxRedirects?: number; socketPath?: string | null; httpAgent?: any; httpsAgent?: any; proxy?: AxiosProxyConfig | false; cancelToken?: CancelToken; decompress?: boolean; transitional?: TransitionalOptions }
|
AxiosInstance
AxiosInstance
是我们使用axios
实例对象类型。
我们使用axios.create(config?: AxiosRequestConfig)
创建出来的对象都是AxiosInstance
类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export interface AxiosInstance { (config: AxiosRequestConfig): AxiosPromise; (url: string, config?: AxiosRequestConfig): AxiosPromise; defaults: AxiosRequestConfig; interceptors: { request: AxiosInterceptorManager<AxiosRequestConfig>; response: AxiosInterceptorManager<AxiosResponse>; }; getUri(config?: AxiosRequestConfig): string; request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>; get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; head<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; options<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>; put<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>; patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>; }
|
可以发现,我们可以使用axios.create、axios.all、axios.spread
方法,但是AxiosInstance
上并没有create、all、spread
等方法,那我们的axios
到底是什么类型呢?
AxiosStatic
1 2 3 4 5 6 7 8 9 10 11
| export interface AxiosStatic extends AxiosInstance { create(config?: AxiosRequestConfig): AxiosInstance; Cancel: CancelStatic; CancelToken: CancelTokenStatic; isCancel(value: any): boolean; all<T>(values: (T | Promise<T>)[]): Promise<T[]>; spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R; isAxiosError(payload: any): payload is AxiosError; }
declare const axios: AxiosStatic;
|
可以发现,axios
其实是AxiosStatic
类型,并且继承了AxiosInstance
类型。所以是两者的结合。
相较axios.create(config?: AxiosRequestConfig)
创建出来的实例对象,axios
功能是更强大的。
AxiosResponse
AxiosResponse
是非常重要的,我们的axios
请求返回值类型都是AxiosResponse
类型。
并且我们可以发现AxiosResponse
是一个接口泛型,这个泛型会应用到后端返回的data
上。所以这块我们可以根据后端接口返回定义不同的类型传递进去。
后面笔者在封装常用方法的时候会细说。
1 2 3 4 5 6 7 8
| export interface AxiosResponse<T = any> { data: T; status: number; statusText: string; headers: any; config: AxiosRequestConfig; request?: any; }
|
AxiosError
AxiosError
这个类型也是我们必须要知道的。在我们响应拦截器里面的错误就是AxiosError
类型。
1 2 3 4 5 6 7 8
| export interface AxiosError<T = any> extends Error { config: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse<T>; isAxiosError: boolean; toJSON: () => object; }
|
说完了Axios
的几个常用类型,接下来我们正式开始使用TS
来封装我们的Axios
。
基础封装
首先我们实现一个最基本的版本,实例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import axios from 'axios' import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
class Request { instance: AxiosInstance baseConfig: AxiosRequestConfig = {baseURL: "/api", timeout: 60000}
constructor(config: AxiosRequestConfig) { this.instance = axios.create(Object.assign(this.baseConfig, config)) } public request(config: AxiosRequestConfig): Promise<AxiosResponse> { return this.instance.request(config) } }
export default Request
|
在实际项目中有了基本的请求方法还是远远不够的,我们还需要封装拦截器和一些常用方法。
拦截器封装
拦截器封装只需要在类中对axios.create()
创建的实例调用interceptors
下的两个拦截器即可,实例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| constructor(config: AxiosRequestConfig) { this.instance = axios.create(Object.assign(this.baseConfig, config)) this.instance.interceptors.request.use( (config: AxiosRequestConfig) => { const token = localStorage.getItem("token") as string config.headers!.Authorization = token; return config }, (err: any) => { return Promise.reject(err) }, ) this.instance.interceptors.response.use( (res: AxiosResponse) => { return res }, (err: any) => { let message = ""; switch (err.response.status) { case 400: message = "请求错误(400)"; break; case 401: message = "未授权,请重新登录(401)"; break; case 403: message = "拒绝访问(403)"; break; case 404: message = "请求出错(404)"; break; case 408: message = "请求超时(408)"; break; case 500: message = "服务器错误(500)"; break; case 501: message = "服务未实现(501)"; break; case 502: message = "网络错误(502)"; break; case 503: message = "服务不可用(503)"; break; case 504: message = "网络超时(504)"; break; case 505: message = "HTTP版本不受支持(505)"; break; default: message = `连接出错(${err.response.status})!`; } return Promise.reject(err.response) }, ) }
|
在这里我们分别对请求拦截器和响应拦截器做了处理。
在请求拦截器我们给请求头添加了token
。
在响应拦截器,我们返回了整个response
对象,当然你也可以只返回后端返回的response.data
,这里可以根据个人喜好来处理。其次对http
错误进行了全局处理。
常用方法封装
在基础封装的时候我们封装了一个request
通用方法,其实我们还可以更具体的封装get、post、put、delete
方法,让我们使用更方便。
并且,我们前面分析到,AxiosResponse
其实是一个泛型接口,他可以接受一个泛型并应用到我们的data
上。所以我们可以在这里再定义一个后端通用返回的数据类型。
比如假设我们某个项目后端接口不管请求成功与失败,返回的结构永远是code、message、results
的话我们可以定义一个这样的数据类型。
1 2 3 4 5
| type Result<T> = { code: number, message: string, result: T }
|
然后传递个各个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public get<T = any>( url: string, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.get(url, config); }
public post<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.post(url, data, config); }
public put<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.put(url, data, config); }
public delete<T = any>( url: string, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.delete(url, config); }
|
这样当我们调用接口的时候就可以看到我们返回的data
的类型啦。就是我们定义的Result
类型。
所以我们可以直接得到自动提示
上面调用接口的时候并没有传递接口数据类型,所以我们的result
是any
类型,要想要每个接口都有类型提示,我们还需要给方法传递泛型。
我们再改进下,我们再定义一个login
接口返回值类型loginType
1 2 3
| type loginType = { token: string; };
|
然后再调用方法的地方传递进去,然后我们再看看返回值data
的类型。
可以看到他是Result<loginType>
类型,这个loginType
就是result
的类型。
所以我们的result
还可以进一步的得到提示
当然每个接口都定义返回值类型固然好,但是会大大加大前端的工作量。我们在写请求方法的时候也可以不传递接口返回值类型,这样result
的类型就是any
。这个可以根据自身项目需求来选择使用。
看到这小伙伴们是不是都弄懂了呢?如还有疑问欢迎留言。
总结
说了这么多,有些小伙伴们可能有点晕了,下面笔者分享下整个axios
的封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| import axios from "axios"; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
type Result<T> = { code: number; message: string; result: T; };
class Request { instance: AxiosInstance; baseConfig: AxiosRequestConfig = { baseURL: "/api", timeout: 60000 };
constructor(config: AxiosRequestConfig) { this.instance = axios.create(Object.assign(this.baseConfig, config));
this.instance.interceptors.request.use( (config: AxiosRequestConfig) => { const token = localStorage.getItem("token") as string; config.headers!.Authorization = token;
return config; }, (err: any) => { return Promise.reject(err); } );
this.instance.interceptors.response.use( (res: AxiosResponse) => { return res; }, (err: any) => { let message = ""; switch (err.response.status) { case 400: message = "请求错误(400)"; break; case 401: message = "未授权,请重新登录(401)"; break; case 403: message = "拒绝访问(403)"; break; case 404: message = "请求出错(404)"; break; case 408: message = "请求超时(408)"; break; case 500: message = "服务器错误(500)"; break; case 501: message = "服务未实现(501)"; break; case 502: message = "网络错误(502)"; break; case 503: message = "服务不可用(503)"; break; case 504: message = "网络超时(504)"; break; case 505: message = "HTTP版本不受支持(505)"; break; default: message = `连接出错(${err.response.status})!`; } return Promise.reject(err.response); } ); }
public request(config: AxiosRequestConfig): Promise<AxiosResponse> { return this.instance.request(config); }
public get<T = any>( url: string, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.get(url, config); }
public post<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.post(url, data, config); }
public put<T = any>( url: string, data?: any, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.put(url, data, config); }
public delete<T = any>( url: string, config?: AxiosRequestConfig ): Promise<AxiosResponse<Result<T>>> { return this.instance.delete(url, config); } }
export default Request;
|
十分钟封装一个好用的axios
通用能力
列一下我想要这个通用请求能达到什么样的效果
正常请求该有的(跨域携带cookie,token,超时设置)
请求响应拦截器
请求成功,业务状态码200,解析result给我,我不想一层一层的去判断拿数据
http请求200, 业务状态码非200,说明逻辑判断这是不成功的,那就全局message提示服务端的报错
http请求非200, 说明http请求都有问题,也全局message提示报错
http请求或者业务状态码401
都做注销操作
全局的loading配置, 默认开启,可配置关闭(由于后端的问题,经常会让前端加防抖节流或者loading不让用户在界面上疯狂乱点,行吧行吧,你们的问题前端帮你们解决,你的规矩就是规矩是吧)
统一文件下载处理 (不要再去各写各的下载了,你写一个,他写一个,一个项目就是这样整的跟屎一样)
一步一步添加功能实现
正常请求该有的
1 2 3 4 5 6 7 8 9 10 11 12
| import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
export const createAxiosByinterceptors = ( config?: AxiosRequestConfig ): AxiosInstance => { const instance = axios.create({ timeout: 1000, withCredentials: true, ...config, }); return instance; };
|
请求响应拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import { Message } from "element-ui"; import { jumpLogin } from "@/utils";
export const createAxiosByinterceptors = ( config?: AxiosRequestConfig ): AxiosInstance => { const instance = axios.create({ timeout: 1000, withCredentials: true, ...config, });
instance.interceptors.request.use( function (config: any) { console.log("config:", config); return config; }, function (error) { return Promise.reject(error); } );
instance.interceptors.response.use( function (response) { console.log("response:", response); const { code, data, message } = response.data; if (code === 200) return data; else if (code === 401) { jumpLogin(); } else { Message.error(message); return Promise.reject(response.data); } }, function (error) { console.log("error-response:", error.response); console.log("error-config:", error.config); console.log("error-request:", error.request); if (error.response) { if (error.response.status === 401) { jumpLogin(); } } Message.error(error?.response?.data?.message || "服务端异常"); return Promise.reject(error); } ); return instance; };
|
全局的loading配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import { Message } from "element-ui"; import { jumpLogin } from "@/utils"; import { Loading } from "element-ui"; import { ElLoadingComponent } from "element-ui/types/loading";
let loadingInstance: ElLoadingComponent | null = null; let requestNum = 0;
const addLoading = () => { requestNum++; if (requestNum == 1) { loadingInstance = Loading.service({ text: "正在努力加载中....", background: "rgba(0, 0, 0, 0)", }); } };
const cancelLoading = () => { requestNum--; if (requestNum === 0) loadingInstance?.close(); };
export const createAxiosByinterceptors = ( config?: AxiosRequestConfig ): AxiosInstance => { const instance = axios.create({ timeout: 1000, withCredentials: true, ...config, });
instance.interceptors.request.use( function (config: any) { const { loading = true } = config; console.log("config:", config); if (loading) addLoading(); return config; }, function (error) { return Promise.reject(error); } );
instance.interceptors.response.use( function (response) { console.log("response:", response); const { loading = true } = response.config; if (loading) cancelLoading(); const { code, data, message } = response.data; if (code === 200) return data; else if (code === 401) { jumpLogin(); } else { Message.error(message); return Promise.reject(response.data); } }, function (error) { console.log("error-response:", error.response); console.log("error-config:", error.config); console.log("error-request:", error.request); const { loading = true } = error.config; if (loading) cancelLoading(); if (error.response) { if (error.response.status === 401) { jumpLogin(); } } Message.error(error?.response?.data?.message || "服务端异常"); return Promise.reject(error); } ); return instance; };
|
统一文件下载处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
| import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import { Message } from "element-ui"; import { jumpLogin, downloadFile } from "@/utils"; import { Loading } from "element-ui"; import { ElLoadingComponent } from "element-ui/types/loading";
let loadingInstance: ElLoadingComponent | null = null; let requestNum = 0;
const addLoading = () => { requestNum++; if (requestNum == 1) { loadingInstance = Loading.service({ text: "正在努力加载中....", background: "rgba(0, 0, 0, 0)", }); } };
const cancelLoading = () => { requestNum--; if (requestNum === 0) loadingInstance?.close(); };
export const createAxiosByinterceptors = ( config?: AxiosRequestConfig ): AxiosInstance => { const instance = axios.create({ timeout: 1000, withCredentials: true, ...config, });
instance.interceptors.request.use( function (config: any) { const { loading = true } = config; console.log("config:", config); if (loading) addLoading(); return config; }, function (error) { return Promise.reject(error); } );
instance.interceptors.response.use( function (response) { console.log("response:", response); const { loading = true } = response.config; if (loading) cancelLoading(); const { code, data, message } = response.data; if (response.data instanceof Blob) { return downloadFile(response); } else { if (code === 200) return data; else if (code === 401) { jumpLogin(); } else { Message.error(message); return Promise.reject(response.data); } } }, function (error) { console.log("error-response:", error.response); console.log("error-config:", error.config); console.log("error-request:", error.request); const { loading = true } = error.config; if (loading) cancelLoading(); if (error.response) { if (error.response.status === 401) { jumpLogin(); } } Message.error(error?.response?.data?.message || "服务端异常"); return Promise.reject(error); } ); return instance; };
src/utils/index.ts
import { Message } from "element-ui"; import { AxiosResponse } from "axios"; import vm from "@/main";
export const jumpLogin = () => { vm.$Cookies.remove("vue_admin_token"); vm.$router.push(`/login?redirect=${encodeURIComponent(vm.$route.fullPath)}`); };
export const downloadFile = (response: AxiosResponse) => { console.log("response.data.type:", response.data.type); return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = function () { try { console.log("result:", this.result); const jsonData = JSON.parse((this as any).result); if (jsonData?.code !== 200) { Message.error(jsonData?.message ?? "请求失败"); reject(jsonData); } } catch (err) { const blob = new Blob([response.data]); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; const filename = response?.headers?.["content-disposition"] ?.split("filename*=")?.[1] ?.substr(7); link.setAttribute("download", decodeURI(filename)); document.body.appendChild(link); link.click(); resolve(response.data); } }; fileReader.readAsText(response.data); }); };
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { createAxiosByinterceptors } from "@/api/request";
const request = createAxiosByinterceptors({ baseURL: localhost:7007, });
export const appList = (params: any): Promise<any> => request.get("/app", { params, loading: true });
export const exportGoods = (data: any) =>request.post("/export", data, { responseType: "blob", });
|