前言
关于axios全局loading的封装博主已经发过一次了,这次是在其基础上增加了token的无感刷新。
token无感刷新流程
- 首次登录的时候会获取到两个token(AccessToken,RefreshToken)
- 持久化保存起来(localStorage方案)
- 正常请求业务接口的时候携带AccessToken
- 当接口口返回401权限错误时,使用RefreshToken请求接口获取新的AccessToken
- 替换原有旧的AccessToken,并保存
- 继续未完成的请求,携带AccessToken
- RefreshToken也过期了,跳转回登录页面,重新登录
后端设计
这里采用node简单实现的后台接口服务
- 后端存有两个字段,分别保存长短token,并且每一段时间更新他们
- 短token过期,返回 returncode:104;长token过期,返回 returncode: 108;请求成功返回returncode: 0;
- 请求头中pass用来接收客户端长token,请求头中authorization用来接收客户端短token
1、创建一个新文件夹,通过vscode打开,运行:
npm init -y
2、安装koa
npm i koa -s
3、安装nodemon
npm i nodemon -g
4、使用路由中间件
npm i koa-router -S
5、跨域处理
npm i koa2-cors
6、新建routes/index.js
const router = require("koa-router")();
let accessToken = "init_s_token"; //短token
let refreshToken = "init_l_token"; //长token
/* 5s刷新一次短token */
setInterval(() => {accessToken = "s_tk" + Math.random();
}, 5000);
/* 一小时刷新一次长token */
setInterval(() => {refreshToken = "l_tk" + Math.random();
}, 600000);
/* 登录接口获取长短token */
router.get("/login", async (ctx) => {ctx.body = {returncode: 0,accessToken,refreshToken,};
});
/* 获取短token */
router.get("/refresh", async (ctx) => {//接收的请求头字段都是小写的let { pass } = ctx.headers;if (pass !== refreshToken) {ctx.body = {returncode: 108,info: "长token过期,重新登录",};} else {ctx.body = {returncode: 0,accessToken,};}
});
/* 获取应用数据1 */
router.get("/getData", async (ctx) => {let { authorization } = ctx.headers;if (authorization !== accessToken) {ctx.body = {returncode: 104,info: "token过期",};} else {ctx.body = {code: 200,returncode: 0,data: { id: Math.random() },};}
});
/* 获取应用数据2 */
router.get("/getData2", async (ctx) => {let { authorization } = ctx.headers;if (authorization !== accessToken) {ctx.body = {returncode: 104,info: "token过期",};} else {ctx.body = {code: 200,returncode: 0,data: { id: Math.random() },};}
});
module.exports = router;
7、创建index.js文件
const Koa = require('koa')
const app = new Koa();
const index = require('./routes/index')
const cors = require('koa2-cors');
app.use(cors());
app.use(index.routes(),index.allowedMethods())
app.listen(4000,() => {console.log('server is listening on port 4000')
})
8、`配置package.json
"dev":"nodemon index.js",
9、运行 npm run dev,这时服务端已准备好
npm run dev
前端源码
interceptors.ts
/** axios封装
* 请求拦截、相应拦截、错误统一处理
*/
import Axios from "axios";
import { ElMessage, ElLoading } from "element-plus";
import _ from "lodash";
import router from "@/router";
import BaseRequest from "@/request/request";
const axios = Axios.create({
//baseURL: localStorage.getItem("address")?.toString(), // url = base url + request url
// timeout: 50000 // request timeout
});
// loading对象
let loadingInstance: { close: () => void } | null;
// 变量isRefreshing
let isRefreshing = false;
// 后续的请求队列
let requestList: ((newToken: any) => void)[] = [];
// 请求合并只出现一次loading
// 当前正在请求的数量
let loadingRequestCount = 0;
// post请求头
axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
// request interceptor
axios.interceptors.request.use(
(config: any) => {
let loadingTarget = "body";
if (config.headers.loadingTarget) {
loadingTarget = config.headers.loadingTarget;
}
const isShowLoading = config.headers.isShowLoading;
const target = document.querySelector(loadingTarget);
if (target && !isShowLoading) {
// 请求拦截进来调用显示loading效果
showLoading(loadingTarget);
}
// do something before request is sent
// if (sessionStorage.getItem("token")) {
// config.headers.Authorization =
// "Bearer " + sessionStorage.getItem("token"); // 让每个请求携带自定义 token 请根据实际情况自行修改
// }
if (config.url) {
// 此处为 Refresh Token 专用接口,请求头使用 Refresh Token
if (config.url.indexOf("/refresh") >= 0) {
config.headers.Authorization = localStorage.getItem("RefreshToken");
} else if (!(config.url.indexOf("/login") !== -1)) {
// 其他接口,请求头使用 Access Token
config.headers.Authorization = localStorage.getItem("accessToken");
}
}
return config;
},
(error) => {
// do something with request error
console.log(error); // for debug
return Promise.reject(error);
}
);
// http response 拦截器
axios.interceptors.response.use(
async (response) => {
setTimeout(() => {
hideLoading();
}, 200);
const data = response.data;
if (data.code == "401") {
// 控制是否在刷新token的状态
if (!isRefreshing) {
// 修改isRefreshing状态
isRefreshing = true;
// 这里是获取新token的接口,方法在这里省略了。
const url = `/refresh`;
const BaseRequestFun = new BaseRequest(url, "");
BaseRequestFun.get().then(async (res) => {
if (res && res.accessToken) {
console.log("a");
// 新token
const newToken = res.accessToken;
// 保存新的accessToken
localStorage.setItem("accessToken", newToken);
// 替换新accessToken
response.config.headers.Authorization = newToken;
// token 刷新后将数组里的请求队列方法重新执行
requestList.forEach((cb) => cb(newToken));
// 重新请求完清空
requestList = [];
// 继续未完成的请求
const resp = await axios.request(response.config);
// 重置状态
isRefreshing = false;
// 返回请求结果
return resp;
} else {
// 清除token
localStorage.clear();
// 重置状态
isRefreshing = false;
// 跳转到登录页
router.replace("/");
}
});
} else {
// 后面的请求走这里排队
// 返回未执行 resolve 的 Promise
return new Promise((resolve) => {
// 用函数形式将 resolve 存入,等待获取新token后再执行
requestList.push((newToken) => {
response.config.headers.Authorization = newToken;
resolve(axios(response.config));
});
});
}
}
return data;
},
(err) => {
setTimeout(() => {
hideLoading();
}, 200);
// 返回状态码不为200时候的错误处理
ElMessage({
message: err.toString(),
type: "error",
duration: 5 * 1000,
});
return Promise.resolve(err);
}
);
// 显示loading的函数 并且记录请求次数 ++
const showLoading = (target: any) => {
if (loadingRequestCount === 0) {
loadingInstance = ElLoading.service({
lock: true,
text: "加载中...",
target: target,
background: "rgba(255,255,255,0.5)",
});
}
loadingRequestCount++;
};
// 隐藏loading的函数,并且记录请求次数
const hideLoading = () => {
if (loadingRequestCount <= 0) return;
loadingRequestCount--;
if (loadingRequestCount === 0) {
toHideLoading();
}
};
// 防抖:将 300ms 间隔内的关闭 loading 便合并为一次. 防止连续请求时, loading闪烁的问题。
const toHideLoading = _.debounce(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
loadingInstance.close();
loadingInstance = null;
}, 300);
export default axios;
request.ts文章来源:https://www.toymoban.com/news/detail-833313.html
import instance from "./interceptors";
import { ElMessage } from "element-plus";
export default class baseRequest {
private url: any;
private params: any;
constructor(url: any, params: any) {
this.url = url;
this.params = typeof params === "undefined" ? {} : params;
}
get(...params: any[]) {
return instance
.get(this.url, {
params: this.params,
headers: {
loadingTarget: params[0],
isShowLoading: params[1] === undefined ? true : params[1],
},
})
.then((res: any) => {
if (res.code === 200) {
return Promise.resolve(res);
} else {
ElMessage({
message: res.entitys[Object.keys(res.entitys)[0]],
type: "error",
duration: 5 * 1000,
});
return Promise.resolve(false);
}
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
post(...params: any[]) {
return instance
.post(this.url, this.params, {
headers: {
loadingTarget: params[0],
isShowLoading: params[1] === undefined ? true : params[1],
},
})
.then((res: any) => {
if (res.code === "200") {
return Promise.resolve(res.entitys);
} else {
ElMessage({
message: res.entitys[Object.keys(res.entitys)[0]],
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
put(...params: any[]) {
return instance
.put(this.url, this.params, {
headers: {
loadingTarget: params[0],
isShowLoading: params[1] === undefined ? true : params[1],
},
})
.then((res: any) => {
if (res.code === "200") {
return Promise.resolve(res.entitys);
} else {
ElMessage({
message: res.entitys[Object.keys(res.entitys)[0]],
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
delete(...params: any[]) {
return instance
.delete(this.url, {
params: this.params,
headers: {
loadingTarget: params[0],
isShowLoading: params[1] === undefined ? true : params[1],
},
})
.then((res: any) => {
if (res.code === "200") {
return Promise.resolve(res.entitys);
} else {
ElMessage({
message: res.entitys[Object.keys(res.entitys)[0]],
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
upfile(...params: any[]) {
return instance
.post(this.url, this.params, {
headers: {
"Content-Type": "multipart/form-data",
"X-Requested-With": "XMLHttpRequest",
loadingTarget: params[0],
isShowLoading: params[1] === undefined ? true : params[1],
},
})
.then((res: any) => {
if (res.code === "200") {
return Promise.resolve(res.entitys);
} else {
ElMessage({
message: res.entitys[Object.keys(res.entitys)[0]],
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
downfile(...params: any[]) {
return instance
.post(this.url, this.params, { responseType: "blob" })
.then((res: any) => {
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
try {
const jsonData = JSON.parse(e.target.result); // 说明是普通对象数据,后台转换失败
if (jsonData.code) {
ElMessage({
message: jsonData.message,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
} catch (err) {
// 解析成对象失败,说明是正常的文件流
const url = window.URL.createObjectURL(res);
const eleLink = document.createElement("a");
eleLink.href = url;
eleLink.download = params[2];
// eleLink.download = "1.xls";
document.body.appendChild(eleLink);
eleLink.click();
window.URL.revokeObjectURL(url);
}
};
fileReader.readAsText(res);
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
icd9Export() {
return instance
.post(this.url, this.params, { responseType: "blob" })
.then((res: any) => {
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
try {
const jsonData = JSON.parse(e.target.result); // 说明是普通对象数据,后台转换失败
if (jsonData.code) {
ElMessage({
message: jsonData.message,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
} catch (err) {
// 解析成对象失败,说明是正常的文件流
const url = window.URL.createObjectURL(res);
const eleLink = document.createElement("a");
eleLink.href = url;
eleLink.download = "icd9.xls";
document.body.appendChild(eleLink);
eleLink.click();
window.URL.revokeObjectURL(url);
}
};
fileReader.readAsText(res);
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
icd10Export() {
return instance
.post(this.url, this.params, { responseType: "blob" })
.then((res: any) => {
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
try {
const jsonData = JSON.parse(e.target.result); // 说明是普通对象数据,后台转换失败
if (jsonData.code) {
ElMessage({
message: jsonData.message,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
}
} catch (err) {
// 解析成对象失败,说明是正常的文件流
const url = window.URL.createObjectURL(res);
const eleLink = document.createElement("a");
eleLink.href = url;
eleLink.download = "icd10.xls";
document.body.appendChild(eleLink);
eleLink.click();
window.URL.revokeObjectURL(url);
}
};
fileReader.readAsText(res);
})
.catch((e) => {
ElMessage({
message: e,
type: "error",
duration: 5 * 1000,
});
Promise.resolve(false);
});
}
}
测试vue文章来源地址https://www.toymoban.com/news/detail-833313.html
<template>
<div>
<el-button type="primary" @click="login()">登录</el-button>
<el-button type="primary" @click="getData()">接口一</el-button>
<el-button type="primary" @click="getData2()">接口二</el-button>
</div>
</template>
<script lang="ts" setup>
import BaseRequest from "@/request/request";
const login = () => {
const url = `/login`;
const BaseRequestFun = new BaseRequest(url, "");
BaseRequestFun.get().then((res) => {
if (res) {
console.log();
localStorage.setItem("accessToken", res.accessToken);
localStorage.setItem("RefreshToken", res.refreshToken);
}
});
};
const getData = () => {
const url = `/getData`;
const BaseRequestFun = new BaseRequest(url, "");
BaseRequestFun.get().then((res) => {
if (res) {
console.log(res);
}
});
};
const getData2 = () => {
const url = `/getData2`;
const BaseRequestFun = new BaseRequest(url, "");
BaseRequestFun.get().then((res) => {
if (res) {
console.log(res);
}
});
};
</script>
<style lang="scss"></style>
到了这里,关于axios封装终极版实现token无感刷新及全局loading的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!