axios封装,无感刷新token
2021-12-08 本文已影响0人
Baby_ed6e
主要原理
1、当第一个请求发现token过期,创建一个Promise对象开始请求api刷新token
2、这个刷新过程中再发现token过期,或者再触发新的接口请求,都是给这个Promise 对象增加then方法(then链)。
3、相当于收集后面排队的请求,统一等刷新完毕后依次放行!
业务要求
1、token过期无感刷新token,没有提示等
2、页面性能过得去,不能用setTimeout setInterval
3、可能同时请求不同接口,分别发现token过期,两次触发非常接近,属于并发,第一个发现token过期的接口刚知道,另一个也已经发现了token过期,所以要将它们收集并挂起,在刷新token后自动再次请求一次接口,把第二次接口返回内容返回给第一次请求。这种情况接口会执行两次,一次是发现,一次是刷新后重新执行。
4、如果追求性能细节,那么在刷新的时候,后面触发接口不应该直接请求api,因为就算请求了也是401,不如暂时挂起,等待刷新后再用新的token执行,这种情况只执行一次。
以下是代码实现过程
实现过程
1、创建一个刷新token的类
/**
* token刷新方法,
* 1、当第一次出现401的时候,立即从api里请求token,并将“异步体”保存为 refreshPromise
* 2、后续的请求相当于都是给 apiRefreshToken 添加then
* 3、当token刷新完毕,refreshPromise 应当清空为下次做准备
*/
class RefreshToken {
constructor() {
this.refreshPromise = undefined;
}
apiRefreshToken() {
if (!this.refreshPromise) {
this.refreshPromise = new Promise((resolve) => {
// 获取到token,存放到本地缓存,此处忽略
setTimeout(() => {
this.refreshPromise = undefined;
resolve();
}, 8000);
});
}
return this.refreshPromise;
}
refreshIsLoading() {
return Boolean(this.refreshPromise);
}
}
export default new RefreshToken();
2、封装axios
import axios from "axios";
import { useRouter } from "vue-router";
import tokenServer from "./tokenServer";
import { ElMessage, ElMessageBox } from "element-plus";
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 5000,
});
// 请求头拦截器,组合token
service.interceptors.request.use(
(config) => {
config.headers["Content-Type"] = "application/json;charset=UTF-8";
config.headers["X-Token"] = "abc"; // 这里在本地缓存拿token
return config;
},
(error) => Promise.reject(error)
);
// 请求结果拦截器
service.interceptors.response.use(
async (response) => {
const { ret_code, result, ret_msg } = response.data;
if (ret_code == "200") {
return Promise.resolve(result);
} else if (ret_code == 401) {
// 过期执行刷新token
// 1、apiRefreshToken 执行刷新并返回正在刷新token的Promise对象, 且同时只有一个。
// 2、每次执行 apiRefreshToken,相当于给Promise对象添加 then() 链, 所以不必担心多个接口同时发现token过期,
// 3、等刷新完毕后再次执行api请求。
try {
await tokenServer.apiRefreshToken();
return Promise.resolve(service(response.config._original));
} catch {
const router = useRouter();
ElMessageBox.alert("登录已失效,请重新登录!", "提示", {
confirmButtonText: "去登录",
callback: () => {
router.push({
path: "/login",
});
},
});
return Promise.reject();
}
} else {
ElMessage.error(ret_msg);
return Promise.reject(response.data);
}
},
(error) => {
ElMessage.error("服务器链接失败,请重试!");
return Promise.reject(error.response);
}
);
// 如果正在刷新token时,需等待token刷新完毕再触发接口请求(暂时挂起)
// 拓展入参_original,保留原始请求的入参,目的是为了拦击里再次请求接口所需参数
const request = async (params) => {
let refreshIsLoading = tokenServer.refreshIsLoading();
if (refreshIsLoading) {
await tokenServer.apiRefreshToken();
}
params._original = JSON.parse(JSON.stringify(params));
return service(params);
};
export default request;