后台管理系统的技术栈:vite、vue3、pinia、vue-router、naive-ui。
用在系统中的请求组件,用到了 axios,lodash,pinia 这几个库,解决了几个开发中的痛点:
1.token 过期需要更新 token,当token正在换取时,并发请求过多导致重复换取新 token 问题
2.重复请求过滤
3.切换组件时取消还未完成的请求
直接上代码
import axios from 'axios';
import _ from 'lodash';
// 使用pinia管理状态
import pinia from './pinia';
// 公共loading
import { useLoadingStore } from '../store/loading';
// 用户token
import { useUserStore } from '../store/user';
// 请求队列
import { useRequestStore } from '../store/request';
const userStore = useUserStore(pinia),
requestStore = useRequestStore(pinia),
loadingStore = useLoadingStore(pinia);
// 接口地址
const apiHost = 'https://xxx.com';
// 请求根目录,使用了环境变量配置文件,打生产包时会使用.env.production文件中的VITE_API_BASEURL地址
let baseURL = import.meta.env.MODE === 'production' ? import.meta.env.VITE_API_BASEURL : apiHost;
// 初始化axios实例
let http = axios.create({
baseURL,
timeout: 60000,
headers: {
'Content-Type': 'application/json;charset=UTF-8;'
}
});
// 并发请求数量,处理多个并发请求时,多次调用loading问题
let requestCount = 0;
// loading操作
const ShowLoading = () => {
if (requestCount === 0) {
$uiMsg.loading('加载中');
}
requestCount++;
};
const HideLoading = () => {
requestCount--;
if (requestCount <= 0) {
requestCount = 0;
$uiMsg.removeMessage();
}
};
// 根据url和参数生成标识key
const GenerateKey = config => {
let { method, url, params, data } = config;
return [method, url, params, typeof data === 'object' ? JSON.stringify(data) : data].join('&');
};
// 阻止重复请求
const StopRepeatRequest = (config, cancel, errorMessage) => {
const errorMsg = errorMessage || '';
for (let i = 0; i < requestStore.list.length; i++) {
if (requestStore.list[i].key === GenerateKey(config)) {
cancel(errorMsg);
return;
}
}
requestStore.Add({
key: GenerateKey(config),
cancel
});
};
// 请求完成后删除重复队列
const DelRequest = config => {
for (let i = 0; i < requestStore.list.length; i++) {
if (requestStore.list[i].key === GenerateKey(config)) {
requestStore.Del(i);
break;
}
}
};
http.interceptors.request.use(config => {
let cancel;
config.cancelToken = new axios.CancelToken(c => {
cancel = c;
});
StopRepeatRequest(config, cancel, `${config.url} 请求被中断`);
if (userStore.token) {
config.headers.Authorization = 'Bearer ' + userStore.token;
}
return config;
});
// 处理token过期时重复换取token问题
let isRefreshToken = false,
retryRequest = [];
http.interceptors.response.use(
response => {
loadingStore.Hide();
DelRequest(response.config);
if (response.config.url.indexOf('/token') !== -1 && response.data.code !== 200) {
// 换token时报错,退出登录
$uiMsg.error('登录已过期,请重新登录', () => {
userStore.LoginOut();
});
}
return response.data;
},
error => {
loadingStore.Hide();
HideLoading();
if (error.response && error.response.status === 401) {
// token已过期,开始换取新token
const config = error.response.config;
// 删除队列
DelRequest(config);
if (!isRefreshToken) {
// 处于未换取token状态
isRefreshToken = true;
return http
.post('/token', userStore.token)
.then(res => {
if (!res.body) return;
userStore.LoginIn(res.body);
config.headers['Authorization'] = 'Bearer ' + res.body.token;
// 已经刷新token,执行等候队列中的请求
retryRequest.forEach(async cb => await cb('Bearer ' + res.body.token));
// 执行完清空队列
retryRequest = [];
isRefreshToken = false;
return http(config);
})
.catch(() => {
isRefreshToken = false;
})
.finally(() => {
isRefreshToken = false;
});
} else {
// 正在刷新token,返回一个未执行resolve的promise
return new Promise(resolve => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
retryRequest.push(token => {
config.headers['Authorization'] = token;
return resolve(http(config));
});
});
}
} else {
if (error.code !== 'ERR_CANCELED') {
// 错误拦截,排除自行取消接口的错误类型
$uiMsg.error(`网络错误,请稍后再试,错误代码:${error.response && error.response.status ? error.response.status : ''}`);
}
return Promise.reject(error);
}
}
);
/* 封装get请求
url:请求地址
params:请求参数
config:请求配置(请求开始时是否展示loading,请求完成后是否隐藏loading,请求错误时是否显示错误信息)
*/
const get = (url, params, config = { showLoading: true, hideLoading: true, showError: true }) => {
if (config.showLoading) {
ShowLoading();
loadingStore.Show();
}
return new Promise((resolve, reject) => {
// 当请求参数为对象时,过滤掉值为null的参数
let _params = Object.prototype.toString.call(params) === '[object Object]' ? _.pickBy(params, item => !_.isNil(item)) : params;
http
.get(url, _params)
.then(res => {
(config.hideLoading || config.hideLoading === undefined) && HideLoading();
if (res?.code === 200 || url.indexOf('/api/oss/sign') !== -1) {
resolve(res);
} else {
if ((config.showError || config.showError === undefined) && res.msg) {
$uiMsg.error(res.msg, () => {
reject(res);
});
} else {
reject(res);
}
}
})
.catch(error => {
if (error.code !== 'ERR_CANCELED') {
reject(error);
}
});
});
};
// 封装post请求,参数同get,filterNull:是否需要过滤掉值为null的参数
const post = (url, params, config = { showLoading: true, hideLoading: true, showError: true, filterNull: true }) => {
if (config.showLoading) {
ShowLoading();
loadingStore.Show();
}
let _filterNull = config.filterNull === undefined ? true : config.filterNull;
return new Promise((resolve, reject) => {
let _params = params;
if (_filterNull) {
_params = Object.prototype.toString.call(params) === '[object Object]' ? _.pickBy(params, item => !_.isNil(item)) : params;
}
http
.post(url, _params)
.then(res => {
(config.hideLoading || config.hideLoading === undefined) && HideLoading();
if (res?.code === 200) {
resolve(res);
} else {
if ((config.showError || config.showError === undefined) && res.msg) {
$uiMsg.error(res.msg, () => {
reject(res);
});
} else {
reject(res);
}
}
})
.catch(error => {
if (error.code !== 'ERR_CANCELED') {
reject(error);
}
});
});
};
export { get, post };
补上另外几个组件的代码
pinia.js
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
loading.js 公共loading状态store
import { defineStore } from 'pinia';
export const useLoadingStore = defineStore('loadingStore', {
state: () => ({
isLoading: false
}),
actions: {
Show() {
this.isLoading = true;
},
Hide() {
this.isLoading = false;
}
}
});
user.js 用户信息store
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: JSON.parse(localStorage.getItem('userInfo')),
token: JSON.parse(localStorage.getItem('token'))
}),
actions: {
LoginIn(data) {
this.token = data.token;
this.userInfo = data;
localStorage.setItem('userInfo', JSON.stringify(data));
localStorage.setItem('token', JSON.stringify(data.token));
localStorage.setItem('expire', data.expire);
},
LoginOut() {
localStorage.removeItem('userInfo');
localStorage.removeItem('token');
localStorage.removeItem('expire');
location.href = '/';
}
}
});
request.js 请求队列store
import { defineStore } from 'pinia';
export const useRequestStore = defineStore('requestStore', {
state: () => ({
list: []
}),
actions: {
Add(data) {
this.list.push(data);
},
Del(index) {
this.list.splice(index, 1);
}
}
});
router.js
import { useRequestStore } from '../store/request';
import pinia from './pinia';
const requestStore = useRequestStore(pinia);
router.beforeEach((to, from, next) => {
// 切换router时,取消pending中的请求
if (requestStore.list.length) {
requestStore.list.forEach((item, index) => {
item.cancel();
requestStore.Del(index);
});
}
})