本文已参与[新人创作礼]活动,一起开启掘金创作之路
由于Typescript越来越普及化,许多项目都引入了Ts,所以使用Ts对Axios进行封装势在必行,所谓一人封装,整体受益
先谈谈为什么要使用Ts,以及Ts相对于Js有什么优点?
Typescript特点
- 图灵完备。(虽然不太清楚这意味着什么)
- 渐进的类型系统,所有类型标注都是可选的,既是天使又是恶魔的 any 类型。(被 Haskell 大牛 Colliot 称为 ts 类型系统的漏洞)
- 支持局部类型推导。
- 丰富的类型层面的计算,如 index types, mapped types, conditional types 等等。
- 支持鸭子类型。(或叫结构子类型?)
- 像 js 支持对象字面量一样支持方便的对象字面类型(object literal type),字符串和数字还有布尔值字面类型。
- 空安全。
- 基于控制流的类型分析。
还有比如支持类型别名,泛型,协变逆变双变等等...
优点:
其一,静态类型检查可以做到early fail,即你编写的代码即使没有被执行到,一旦你编写代码时发生类型不匹配,语言在编译阶段(解释执行也一样,可以在运行前)即可发现。针对大型应用,测试调试分支覆盖困难,很多代码并不一定能够在所有条件下执行到。而假如你的代码简单到任何改动都可以从UI体现出来,这确实跟大型应用搭不上关系,那么静态类型检查确实没什么作用。
其二,静态类型对阅读代码是友好的,比如我们举个例子 jQuery API Documentation 这是大家都非常喜欢用的jQuery.ajax,在这份文档中,详尽地解释了类型为object的唯一一个参数settings,它是如此之复杂,如果没有文档,我们只看这个函数声明的话,根本不可能有人把这个用法猜对。针对大型应用,方法众多,调用关系复杂,不可能每个函数都有人编写细致的文档,所以静态类型就是非常重要的提示和约束。而假如你的代码像jQuery这样所有函数基本全是API,根本没什么内部函数,而且逻辑关系看起来显而易见,这确实跟大型应用搭不上关系,那么静态类型对阅读代码确实也没什么帮助
总结:
- Ts提供类型判断,提早告知错误,不像Js运行后再报错
- 代码友好性高
Axios封装
这边封装Axios的同时还实现了类似于节流效果,过滤后续重复请求
Axios封装
// axios.ts
// 封装axios
import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
// import { setToken, getToken, getTokenKey, removeToken, } from "./cookie";
import { addPending, removePending, checkRequest } from './pending';
const showStatus = (status: number) => {
let message = '';
switch (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 = `连接出错(${status})!`;
}
return `${message},请检查网络或联系管理员!`;
};
export class Interceptors {
instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL as string, // 看个人配置
timeout: 30 * 1000,
withCredentials: true,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json;charset=utf-8',
},
});
this.setupInterceptors();
}
// 初始化拦截器
setupInterceptors() {
// 请求接口拦截器
this.instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (!checkRequest(config)) {
// 如果pending中没有该请求,则加入pending
// 如果有的话,会在该方法中取消
addPending(config)
}
// 判断一下是否有cookie 如果有的话则把cookie放入请求头中
// if (getToken()) {
// config.headers[getTokenKey()] = getToken();
// }
return config;
},
() => {
// 错误抛到业务代码
const error = { data: { msg: '服务器异常,请联系管理员!' } };
return Promise.resolve(error);
},
);
// 响应拦截器
this.instance.interceptors.response.use((response: AxiosResponse) => {
removePending(response); // 在请求结束后,移除本次请求
return response.data;
}, (error: any) => {
let msg;
let code = -1;
if (axios.isCancel(error)) {
msg = '';
code = -2;
} else {
const { status } = error?.response;
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
msg = showStatus(status);
}
}
return Promise.resolve({ msg, code });
});
}
// 返回一下
getInterceptors() {
return this.instance;
}
}
导出http
// index.ts
// 导出
import { AxiosPromise } from 'axios';
import { Interceptors } from './axios';
import { message } from 'ant-design-vue';
export interface HttpRequest {
method?: string
url: string
data?: any
timeout?: number
}
// 接口响应通过格式 (由各自响应格式定义)
export interface HttpResponse {
code: number
data: any
msg: string
}
// 请求配置
export class HttpServer {
axios: any;
// 获取axios实例
constructor() {
this.axios = new Interceptors().getInterceptors();
}
// 简单封装一下方法
request(config: HttpRequest): AxiosPromise {
return new Promise((resolve, reject) => {
this.axios(config).then((res: HttpResponse) => {
if (res.code === 0) {
resolve(res?.data);
} else if (res.code === -2) { // 多次请求取消
console.log('请求多次。');
} else {
const msg = res?.msg || '系统繁忙,请稍后重试';
message.error(msg);
reject(msg);
}
})
.catch((err: any) => {
err?.msg && message.error(err?.msg);
reject(err);
});
});
}
}
const http = new HttpServer();
export default http;
实现去重
// pending.ts
// 实现去重
import axios, { AxiosRequestConfig } from 'axios';
// CancelToken 使用说明 https://github.com/axios/axios
// 参考文章 https://segmentfault.com/a/1190000039806000
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
export const pending = new Map();
export const getKey = (config: AxiosRequestConfig) => JSON.stringify([config.method, config.url, config.params, config.data]);
/**
* 添加请求
* @param {Object} config
*/
export const addPending = (config: AxiosRequestConfig): void => {
const url = getKey(config);
// eslint-disable-next-line no-param-reassign
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
pending.set(url, cancel);
}
});
};
/**
* 移除请求
* @param {Object} config
*/
export const removePending = (config: AxiosRequestConfig): void => {
const url = getKey(config);
if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pending.get(url);
cancel(url);
pending.delete(url);
}
};
/**
* 清空 pending 中的请求(在路由跳转时调用)
*/
export const clearPending = (): void => {
for (const [url, cancel] of pending) {
cancel(url);
}
pending.clear();
};
// 校验是否有相同请求在请求中
export const checkRequest = (config: AxiosRequestConfig): boolean => {
const url = getKey(config);
if (pending.has(url)) {
new axios.CancelToken((cancel) => {
cancel(url);
})
return true;
} else return false;
}
// cookie.ts
import Cookies from 'js-cookie';
const tokenKey = 'token';
export const getToken = () => Cookies.get(tokenKey);
export const getTokenKey = () => tokenKey;
export const setToken = (token: string) => Cookies.set(tokenKey, token);
export const removeToken = () => Cookies.remove(tokenKey);
在路由跳转时撤销所有请求
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Login from '@/views/Login/Login.vue'
//引入在axios暴露出的clearPending函数
import { clearPending } from "@/utils/http/pending"
....
....
....
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
//在跳转路由之前,先清除所有的请求
clearPending()
// ...
next()
})
export default router
请求例子
// promise式
http.request({ url: /api/xxx/xxx, method: 'get', data: { username, password } }).then((res: any) => {
...
}).catch((err) => {
...
})
// async/await式
// 函数头要加async
const res = await http.request({ url: /api/xxx/xxx, method: 'get', data: { username, password } });