此博客是针对开源项目:vue3-element-admin 的学习记录,为了帮助自己理清开发这个系统的逻辑.
安装依赖
javascript
npm install axios , qs
Axios实例封装
javascript
// 创建 axios 实例 ,同时给出一些预设配置,比如baseURL,超时时间等等
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
//序列化`params`
paramsSerializer: (params) => qs.stringify(params),
})
其中 paramsSerializer: (params) => qs.stringify(params)
是把发送网络请求时传递的params
参数序列化为url
查询字符串,拼接在URL之后,发送网络请求。
请求拦截器
javascript
// 请求拦截器,如果有token,就添加
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = getToken() //获取访问token
if (config.headers.Authorization !== 'no-auth' && accessToken) {
config.headers.Authorization = accessToken
}
else {
delete config.headers.Authorization
}
return config
},
(error) => {
return Promise.reject(error)
},
)
请求拦截器,在我们的网络请求发送之前,拦截它们,这里我们需要加上token
,有些页面是必须有token
才能访问的,所以我们这里统一拦截,统一添加。
其中
const accessToken = getToken()
是在获取访问token:accessToken
getToken()
的代码如下:
javascript
//获取到的token格式如 Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
function getToken(): string {
return localStorage.getItem(ACCESS_TOKEN_KEY) || ''
}
设置token
javascript
if (config.headers.Authorization !== 'no-auth' && accessToken) {
//把获取到的token设置上
config.headers.Authorization = accessToken
}
else {
delete config.headers.Authorization
}
这段代码的目的是在请求发送之前判断是否需要携带 Token
。如果请求头 Authorization
为 no-auth
,或者没有设置请求头 Authorization
字段,则不会添加 Token
,即删除该字段。
反之则会根据 accessToken
动态地设置 Authorization
头。
在进行登录时,会调用登录方法,成功登录之后,会调用setToken
的方法,保存token
到localStorage
中:
javascript
// 登录操作,这是首先在登录.vue组件中调用的方法
function login(loginData: LoginData) {
return new Promise((resolve, reject) => {
AuthAPI.login(loginData)
.then((data) => {
const { tokenType, accessToken, refreshToken } = data
// 这里在成功登录以后,保存token,这样之后的网络请求都可以获取到token。
setToken(tokenType + ' ' + accessToken) // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx,存储到localStorage中
setRefreshToken(refreshToken)
resolve(data)
})
.catch((err) => {
reject(err)
})
})
}
响应拦截器
javascript
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
// 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等
if (response.config.responseType === "blob") {
return response;
}
const { code, data, msg } = response.data;
if (code === ResultEnum.SUCCESS) {
console.log("response SUCCESS", response);
return data;
}
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
},
async (error: any) => {
// 非 2xx 状态码处理 401、403、500 等
const { config, response } = error;
if (response) {
const { code, msg } = response.data;
if (code === ResultEnum.ACCESS_TOKEN_INVALID) {
// Token 过期,刷新 Token
return handleTokenRefresh(config);
} else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
return Promise.reject(new Error(msg || "Error"));
} else {
ElMessage.error(msg || "系统出错");
}
}
return Promise.reject(error.message);
}
);
service.interceptors.response.use( onFulfilled ,onRejected )
函数接收两个函数作为参数。
onFulfilled
函数是响应成功时执行的回调:所谓响应成功是当 axios
收到一个 2xx
状态码 的响应时,它会认为响应成功,并进入 onFulfilled
函数。此时的 response
对象中包含了以下内容:
response.data:
服务器返回的响应数据(通常是你需要的内容)。
response.status:
响应的 HTTP状态码,如 200, 201 等。
response.statusText:
状态信息文本,如 "OK","Created" 等。
response.headers:
响应头信息。
response.config:
原始请求配置对象。
当axios
收到一个 非 2xx
状态码 的响应时,或者请求失败(如网络错误、超时等),它会进入 onRejected
函数。在 onRejected
函数中,错误对象 error
会包含以下内容:
error.response:
当请求得到了响应但状态码不在 2xx 范围时(如 4xx 或 5xx 错误)才存在的属性。它包含了来自服务器的响应信息。
error.config:
包含了请求时的配置信息等等...
对于请求失败的情况,我们获取到 const { config, response } = error;
通过response.data
获取到服务器返回的错误消息或错误数据,比如code
,msg
等信息。
如果code
代表访问token失效,则需要重新获取一次访问token,这里调用handleTokenRefresh
()函数
如果code
代表刷新token失效,中止请求流程。
这个系统里的分为refreshToken
和accessToken
,通常refreshToken
的有效期是更久的,同时每次accessToken
更新时也会把refreshToken
一起更新,正常来说,基本不会同时出现两个Token同时过期(根据其它代码的推测)。
现在重点关注handleTokenRefresh
函数,相关代码如下:
javascript
// 刷新 Token 的锁
let isRefreshing = false;
// 因 Token 过期导致失败的请求队列
let requestsQueue: Array<() => void> = [];
// 刷新 Token 处理
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
return new Promise((resolve) => {
const requestCallback = () => {
config.headers.Authorization = getToken();
resolve(service(config));
};
requestsQueue.push(requestCallback);
if (!isRefreshing) {
isRefreshing = true;
// 刷新 Token
useUserStoreHook()
.refreshToken()
.then(() => {
// Token 刷新成功,执行请求队列
requestsQueue.forEach((callback) => callback());
requestsQueue = [];
})
.catch((error) => {
console.log("handleTokenRefresh error", error);
// Token 刷新失败,清除用户数据并跳转到登录
ElNotification({
title: "提示",
message: "您的会话已过期,请重新登录",
type: "info",
});
useUserStoreHook()
.clearUserData()
.then(() => {
router.push("/login");
});
})
.finally(() => {
isRefreshing = false;
});
}
});
}
首先有一个刷新 Token 的锁isRefreshing
,因为可能多个网络请求再发送时,token过期了,这时候需要更新token,但是我们只需要更新一次就可以了,所有有一个锁。对于后续的网络请求,发现当前正在更新token时,就不用再次更新token了。
首先定义了一个网络请求的回调函数:
javascript
const requestCallback = () => {
config.headers.Authorization = getToken();
resolve(service(config));
};
//推入数组中
requestsQueue.push(requestCallback);
这段代码是为了保存那些再token
失效时发送的网络请求,等到后面刷新token
以后,在统一调用。
service(config)
:是使用之前创建的axios实例对象,config
则是本次请求的配置信息,因为token
失效,axios
请求失败,通过error
对象获取到本次的config
和response
:
javascript
const { config, response } = error;
之后:
javascript
//如果当前没有进行更新token
if (!isRefreshing) {
//则进行更新token,同时上锁
isRefreshing = true;
// 刷新 Token,调用相关的API更新
useUserStoreHook()
.refreshToken()
.then(() => {
// Token 刷新成功,执行请求队列,即重新发送之前因为token失效而失败的网络请求
requestsQueue.forEach((callback) => callback());
requestsQueue = [];
})
//如果更新token出错,直接清除数据,跳转到登录页.
.catch((error) => {
console.log("handleTokenRefresh error", error);
// Token 刷新失败,清除用户数据并跳转到登录
ElNotification({
title: "提示",
message: "您的会话已过期,请重新登录",
type: "info",
});
useUserStoreHook()
.clearUserData()
.then(() => {
router.push("/login");
});
})
.finally(() => {
isRefreshing = false;
});
}
完整代码
javascript
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axios'
import qs from 'qs'
import { ElNotification } from 'element-plus'
import { useUserStore } from '@/stores/modules/user' //用户信息的store
import { getToken } from '@/utils/auth'
import { ResultEnum } from '@/enums/ResultEnum'
import router from '@/router'
// 创建 axios 实例 ,针对网络请求的封装
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
paramsSerializer: (params) => qs.stringify(params),
})
// 请求拦截器,如果有token,就添加
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = getToken() //获取访问token
if (config.headers.Authorization !== 'no-auth' && accessToken) {
config.headers.Authorization = accessToken
}
else {
delete config.headers.Authorization
}
return config
},
(error) => {
return Promise.reject(error)
},
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
// 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等
// 这种类型不能return response.data
if (response.config.responseType === 'blob') {
return response
}
// json数据,成功状态
const { code, data, msg } = response.data
if (code === ResultEnum.SUCCESS) {
return data
}
// @ts-ignore
ElMessage.error(msg || '系统出错')
return Promise.reject(new Error(msg || 'Error'))
},
// 响应错误拦截器,处理不同的错误状态
(error: any) => {
// 非 2xx 状态码处理 401、403、500 等
const { config, response } = error
if (response) {
const { code, msg } = response.data
// 访问令牌无效或过期,重新获取一次,但是可能刷新令牌也过期
if (code === ResultEnum.ACCESS_TOKEN_INVALID) {
console.log('token过期,重新获取')
return handleTokenRefresh(config)
// return 'AccessTokenInvalid'
}
// 刷新令牌无效或过期,应该是跳转登录页面
else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
return Promise.reject(new Error(msg || 'Error'))
} else {
// @ts-ignore
ElMessage.error(msg || '系统出错')
}
}
return Promise.reject(error.message)
},
)
// 导出 axios 实例
export default service
// 刷新 Token 的锁,表示是否正在刷新token
let isRefreshing = false
// 因 Token 过期导致失败的请求队列
let requestsQueue: Array<() => void> = []
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
return new Promise((resolve) => {
const requestCallback = () => {
config.headers.Authorization = getToken()
resolve(service(config))
}
requestsQueue.push(requestCallback)
if (!isRefreshing) {
isRefreshing = true
// 刷新 Token
useUserStore()
.refreshToken()
.then(() => {
// Token 刷新成功,执行请求队列
requestsQueue.forEach((callback) => callback())
requestsQueue = []
})
.catch((error: any) => {
console.log('handleTokenRefresh error', error)
// Token 刷新失败,清除用户数据并跳转到登录
ElNotification({
title: '提示',
message: '您的会话已过期,请重新登录',
type: 'info',
})
useUserStore()
.clearUserData()
.then(() => {
router.push('/login')
})
})
.finally(() => {
isRefreshing = false
})
}
})
}