前端在做接口调用的时候,往往需要做按钮阻塞,即当前请求未返回,禁止再次发送。
以前常规的做法是请求后把按钮disabled,接口响应后再放开,这种做法需要一个一个去加开关,而且在给老工程做按钮阻塞时工作量繁重且容易漏。
本文利用 axios.CancelToken Api来对接口进行全局拦截,一劳永逸,且优雅。
首先封装Http请求类
http-request.ts
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import md5 from "js-md5";
import { ElMessage } from "element-plus";
interface Interceptor {
onFulfilled: any;
onRejected: any;
}
/**
* Http类,对axios的封装,用于发送http请求
* 主要实现重复请求的拦截(当同一请求未返回而再次发送的情况)
* @param {AxiosRequestConfig} config - axios基础配置
* @param {Interceptor} reqInterceptor - 请求拦截器自定义方法
* @param {Interceptor} resInterceptor - 响应拦截器自定义方法
* @author Ywq
* @date 2022/8/9 10:07
*/
class Http {
private instance: AxiosInstance; // axios 实例
private pending: any = {}; // 网络请求记录map结构
private CancelToken = axios.CancelToken;
constructor(config: AxiosRequestConfig, reqInterceptor: Interceptor, resInterceptor: Interceptor) {
this.instance = axios.create(config);
this.instance.interceptors.request.use(
(config) => {
// 通过请求url、method、params、data字段生成md5值
const key = this.getMd5String(config);
config.cancelToken = new this.CancelToken(c => {
if (this.pending[key]) {
// 上次接口未返回时走此逻辑
if (Date.now() - this.pending[key] > 5000) {
// 超过5s,删除对应的请求记录,重新发起请求,即使未返回
delete this.pending[key];
} else {
// 同一请求未返回,5s内再次发送,取消发送
c("repeated:" + config.url);
}
}
// 记录当前的请求,已存在则更新时间戳
this.pending[key] = Date.now();
});
const token = localStorage.getItem("token");
if (token) {
if (!config.params) {
config.params = {
"access_token": token
};
} else {
config.params.access_token = token;
}
}
if (reqInterceptor.onFulfilled) {
return reqInterceptor.onFulfilled(config);
}
return config;
},
(err) => {
let error = err;
if (reqInterceptor.onRejected) {
error = reqInterceptor.onRejected(err);
}
ElMessage.warning(error);
return Promise.reject(error);
}
);
this.instance.interceptors.response.use(
(response) => {
const key = this.getMd5String(response.config);
if (this.pending[key]) {
// 请求结束,删除对应的请求记录
delete this.pending[key];
}
if (resInterceptor.onFulfilled) {
return resInterceptor.onFulfilled(response);
}
return response;
},
(err) => {
if (err.message.includes("repeated")) {
// Toast("您的操作过于频繁,请稍后再试");
return Promise.reject(err);
}
const key = this.getMd5String(err.config);
if (this.pending[key]) {
// 请求结束,删除对应的请求记录
delete this.pending[key];
}
// 除repeated外报错逻辑
let error = err;
if (resInterceptor.onRejected) {
error = resInterceptor.onRejected(err);
}
ElMessage.warning(error?.message || error);
return Promise.reject(error); // 返回接口返回的错误信息
}
);
}
private getMd5String(config: AxiosRequestConfig): string {
return md5(`${config.url}&${config.method}&${JSON.stringify(config.data)}&${JSON.stringify(config.params)}`);
}
get(url: string, params: any) {
return this.instance.get(url, { params });
}
post(url: string, data: any, config: AxiosRequestConfig | undefined = undefined) {
return this.instance.post(url, data, config);
}
}
export default Http;
封装请求方法,此处实现了三个实例,分别对响应做出了不同程度的拆包,开发时按需取用
http.ts
import axios, { AxiosResponse } from "axios";
import { ElMessage } from "element-plus";
import Http from "@/utils/class/http-request";
const isPro = process.env.NODE_ENV === "production";
const baseURL = location.origin + (isPro ? process.env.VUE_APP_SERVER_DIR : "");
const options = {
baseURL,
timeout: 60000
};
/**
* 用于未登录情况下跳入登录页
* @code {number}状态码
*/
const errorLogic = (code: number): void => {
if (/^8\d{2}$/.test(String(code))) {
ElMessage.warning("程序验证已过期,请重新登录");
const timer = setTimeout(() => {
if (process.env.NODE_ENV === "production") {
location.href = process.env.BASE_URL + "login";
} else {
location.href = "/login";
}
clearTimeout(timer);
}, 2000);
}
};
axios.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
axios.defaults.withCredentials = true; // 让ajax携带cookie
const r = new Http(options, {
onFulfilled: null,
onRejected: null
},
{
onFulfilled: (response: AxiosResponse) => {
const { data: { code, message, data } } = response;
if (code === 0 || code === 200) {
return data;
}
ElMessage.warning(message);
return Promise.reject(data);
},
onRejected: null
}
);
const req = new Http(options, {
onFulfilled: null,
onRejected: null
},
{
onFulfilled:
(response: AxiosResponse) => {
const { data } = response;
const { code, message } = data;
if (code === 0 || code === 200) {
return data;
}
ElMessage.warning(message);
return Promise.reject(data);
},
onRejected: null
}
);
const request = new Http(options, {
onFulfilled: null,
onRejected: null
},
{
onFulfilled: (response: any) => {
const { data: { code, message } } = response;
if (code === 0 || code === 200) {
return response;
}
ElMessage.warning(message);
return Promise.reject(response);
},
onRejected: null
}
);
/**
* r实例对应的响应
*/
type R = Promise>;
/**
* req例对应的响应
*/
type Res = Promise>;
/**
* request实例对应的响应
*/
type Response = Promise>>>;
export { r, req, request, R, Res, Response };
// #utf8 编码否则会出现中文乱码
// 200=成功
// 0=成功
// 400=参数异常
// 404=资源不存在
// 401=需要认证后访问
// 403=系统拒绝访问
// 500=系统异常
// 504=请求超时
// 700=用户名不能为test
// 701=用户不存在
// 800=验证码不正确
// 801=不支持的登录类型
// 802=登录失败
// 803=账户名或者密码输入错误
// 804=账户被锁定,请联系管理员
// 805=密码过期,请联系管理员
// 806=账户过期,请联系管理员
// 807=账户被禁用,请联系管理员
// 808=账户没有登录
// 809=没有权限访问
// 813=未查到法院信息
// 814=不支持跨省查询
// 815=不支持自建查询
使用时
import { r, req, request, R, Res, Response } from "../http";
export interface ModelData {
name: string;
age: number;
}
export interface BaseParam {
startDate: string; // 开始时间
endDate: string; // 截止时间
page: number; // 页码
size: number; // 每页大小
}
export const getApi1 = (param: BaseParam): R => {
return r.post("/xx", param);
};