环境
Vue3、soybean admin: “1.0.0”(native-ui: “2.38.0”)、pnpm: “8.5.3”、axios: “1.6.7”
实现
Soybean Admin 使用的 Axios 库,封装的请求方法中没有对请求失败做任何逻辑处理,目前需要实现:请求返回 50020 状态码,表示 token 失效或者过期,跳转登录页面,让用户重新登录
1. 找到补充逻辑的地方
因为这是常用功能,在二次开发各种后台管理模板都是这样,有需求第一步先找文档,Soybean Admin文档-系统请求中说到了, onBackendFail 是后端请求在业务上表示失败时调用的异步函数,例如:处理 token 过期
所以大概率是在 onBackendFail 里面实现功能
// 文件路径:src\service\request\index.ts
export const request = createFlatRequest<App.Service.Response>(
{
baseURL: isHttpProxy ? createProxyPattern() : baseURL,
},
{
async onRequest(config) {
const { headers } = config;
// set token
const token = localStg.get('token');
// const Authorization = token ? `Bearer ${token}` : null;
const Authorization = token ? `${token}` : null;
Object.assign(headers, { Authorization });
return config;
},
isBackendSuccess(response) {
// when the backend response success is "true", it means the request is success
return response?.data?.success === true;
},
async onBackendFail(_response) {
// when the backend response code is not "0000", it means the request is fail
// for example: the token is expired, refetch token and retry request
// 补充token失效逻辑
console.log("补充token失效逻辑");
},
transformBackendResponse(response) {
return response.data.result;
},
onError(error) {
// when the request is fail, you can show error message
let message = error.message;
// show backend error message
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.message || message;
}
window.$message?.error(message);
}
}
);
打印测试发现,接口请求失败确实会走这个方法。
你还会发现打印了两次,不着急,等功能完成后再解决(参考博客)
确定补充逻辑的地方后,下一步:
补充上状态码的判断,将 redirectLogin 单独出来
async onBackendFail(_response) {
// when the backend response code is not "0000", it means the request is fail
// for example: the token is expired, refetch token and retry request
// 补充token失效逻辑
if (_response.data.code === TOKEN_EXPIRED_CODE) {
await redirectLogin(); // token失效跳转到登录页
}
},
2. 写redirectLogin方法
首先我们看 onBackendFail 是在哪里执行的,写 redirectLogin 方法之前有没有需要注意的地方。createFlatRequest 调用的是 createCommonRequest 方法
看看 createCommonRequest 方法
// 文件路径:packages\axios\src\index.ts
function createCommonRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const opts = createDefaultOptions<ResponseData>(options);
const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
// config axios retry
const retryOptions = createRetryOptions(axiosConf);
axiosRetry(instance, retryOptions);
instance.interceptors.request.use(); // ...
instance.interceptors.response.use(
async response => {
if (opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}
const fail = await opts.onBackendFail(response, instance); // 就是这里!!!
if (fail) {
return fail;
}
// ...
},
async (error: AxiosError<ResponseData>) => {}
);
function cancelRequest(requestId: string) {} // ...
function cancelAllRequest() {} // ...
return {
instance,
opts,
cancelRequest,
cancelAllRequest
};
}
看起来很简单,没什么好注意的,只是注意要返回一个 Promise 对象,setTimeout 模拟一下异步就行了。
准备开始写方法,参考现有的跳转登录页功能,比如系统自带的【退出登录】功能,去看看有没有能抄的:
文件路径:src\layouts\modules\global-header\components\user-avatar.vue
<script setup lang="ts">
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
const authStore = useAuthStore();
const { routerPushByKey, toLogin } = useRouterPush();
function loginOrRegister() {
toLogin();
}
function logout() {
window.$dialog?.info({
title: $t('common.tip'),
content: $t('common.logoutConfirm'),
positiveText: $t('common.confirm'),
negativeText: $t('common.cancel'),
onPositiveClick: () => {
authStore.resetStore(); // 确认退出登录,会清空token, 然后跳转到登录页
}
});
}
</script>
<template></template>
<style scoped></style>
3. 遇到问题
照上面【退出登录】组件代码,补充 redirectLogin 方法,代码如下:
import { useAuthStore } from '@/store/modules/auth';
const authStore = useAuthStore();
export const request = createFlatRequest<App.Service.Response>(){}; // ...
// 跳转登录页面
function redirectLogin() {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
authStore.resetStore(); // 参考退出登录的逻辑
resolve();
}, 500);
})
}
但控制台报错:pinia.js?v=f2cc29a3:1340 Uncaught Error: [🍍]: “getActivePinia()” was called but there was no active Pinia. Are you trying to use a store before calling “app.use(pinia)”? See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help.
解决了好久还是不知道什么问题~
但从菠萝报错信息来看,是 store 的问题,那就不使用 authStore.resetStore 这个方法了,直接跳转登录页也是一样的
4. 另辟蹊径
那就直接跳转登录页了,也不清空 token,反正登录就会重新赋值的,最重要的是解决问题。
代码如下
import { useRouterPush } from '@/hooks/common/router';
const { toLogin } = useRouterPush();
// 跳转登录页面
function redirectLogin() {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
toLogin(); // 跳转到登录页
resolve();
}, 1200);
})
}
没有跳转到登录页,并且控制台警告+报错
漂黄: [Vue warn]: inject() can only be used inside setup() or functional components.
漂红:Uncaught ReferenceError: Cannot access ‘globalRouter’ before initialization
第一个警告好解决,看 useRouterPush 源码,需要将 inSetup 参数设置成 false
// 文件路径:src\hooks\common\router.ts
import { useRouter } from 'vue-router';
import type { RouteLocationRaw } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { router as globalRouter } from '@/router';
/**
* Router push
*
* Jump to the specified route, it can replace function router.push
*
* @param inSetup Whether is in vue script setup
*/
export function useRouterPush(inSetup = true) { ... }
设置完了还是报错:Uncaught ReferenceError: Cannot access ‘globalRouter’ before initialization
没有耐心了,报错全都是执行时机的问题,陷进屎坑了。
库好多方法都封装的很好,无可挑剔,很难排查执行时机问题。
用最原始的方法跳转登录页,不使用 Soybean Admin 提供的 authStore.resetStore、toLogin 方法。
5. 原始方法
网上搜索了:“Vue3 axios实现token失效跳转登录页面”,参考了网友的实现,决定使用最丑陋的route.push方式,开始导入router对象:
import { router } from '@/router';
...
// 跳转登录页面
function redirectLogin() {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
console.log(router); // 先测试
resolve();
}, 1200);
})
}
测试发现 router 能成功打印,没有报错。
突然就想把之前的 useAuthStore、useRouterPush 移到 redirectLogin 方法里面去执行,竟然不报错,可以成功跳转到登录页???!!!
6. 完善方法
千辛万苦,实现 token 过期或失效时跳转登录页的功能,最终的代码如下:
async onBackendFail(_response) {
// when the backend response code is not "0000", it means the request is fail
// for example: the token is expired, refetch token and retry request
// 补充token失效逻辑
if (_response.data.code === TOKEN_EXPIRED_CODE) {
await redirectLogin(); // token失效
}
}
function redirectLogin() {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
const authStore = useAuthStore();
authStore.resetStore(); // 参考退出登录的逻辑
resolve();
}, 500);
})
}
简单得要死!
最后分享一下,不需要后端配合,自测的方法:
欢迎评论指出问题!
总结
记录了这次Soybean Admin基础上实现Token过期跳转登录页功能的整个过程。
因为代码放错地方,导致出现了很多奇奇怪怪的问题,也算是踩坑过程了,主要还是因为不熟悉。
最后希望本博客能帮助到大家!