import axios from "axios"; import { ElMessage, ElLoading } from "element-plus"; // 全局状态 const pendingRequests = new Map() let loadingCount = 0 let loadingInstance = null const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000, }); // 生成唯一请求标识 const generateReqKey = (config) => { const { method, url, params, data } = config return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&') } // 请求拦截器 service.interceptors.request.use(config => { const requestKey = generateReqKey(config) // 取消重复请求 if (pendingRequests.has(requestKey)) { pendingRequests.get(requestKey)('取消重复请求') } config.cancelToken = new axios.CancelToken(cancel => { pendingRequests.set(requestKey, cancel) }) // 全局Loading控制 if (!config.silentLoading) { loadingCount++ if (loadingCount === 1) { loadingInstance = ElLoading.service({ lock: true, text: "加载中...", background: "rgba(0, 0, 0, 0.7)", }) } } // Token处理 const token = localStorage.getItem("token"); token && (config.headers.Authorization = `Bearer ${token}`) return config }) // 响应拦截器 service.interceptors.response.use( response => { const requestKey = generateReqKey(response.config) pendingRequests.delete(requestKey) // 关闭Loading if (!response.config.silentLoading) { loadingCount = Math.max(0, loadingCount - 1) if (loadingCount === 0 && loadingInstance) { loadingInstance.close() } } // 业务逻辑处理 const res = response.data if (res.code !== 200) { ElMessage.error(res.message || "操作失败") return Promise.reject(new Error(res.message || "Error")) } return res }, error => { // 清理请求记录 if (axios.isCancel(error)) { console.log('请求被取消:', error.message) return Promise.reject(error) } const requestKey = generateReqKey(error.config) pendingRequests.delete(requestKey) // 关闭Loading if (!error.config.silentLoading) { loadingCount = Math.max(0, loadingCount - 1) if (loadingCount === 0 && loadingInstance) { loadingInstance.close() } } // 增强错误处理 if (!error.response) { ElMessage.error("网络连接异常") } else { const status = error.response.status const messageMap = { 400: '请求参数错误', 401: () => { localStorage.removeItem('token') router.replace('/login') return '登录已过期' }, 403: '没有操作权限', 404: '资源不存在', 500: '服务器错误', 502: '网关错误', 503: '服务不可用' } const handler = messageMap[status] || (() => `请求失败 (${status})`) const message = typeof handler === 'function' ? handler() : handler ElMessage.error(message) } return Promise.reject(error) } ) export default service; 我封装的请求方法有什么问题,改进一下
时间: 2025-06-10 21:08:18 浏览: 34
<think>嗯,用户让我分析他们封装的Axios请求方法,看看有什么问题,并改进。我需要仔细看看他们的代码。首先,我会从代码结构、功能实现、潜在问题等方面入手。
首先看全局状态部分。他们用了pendingRequests这个Map来存储pending的请求,还有loadingCount和loadingInstance控制加载效果。这可能会有问题,比如在多个请求同时发起时,loading的控制是否正确?比如,如果两个请求同时开始,第一个触发loading,第二个会不会导致loadingCount增加到2,而当其中一个完成时,loadingCount减到1,这时候会不会提前关闭loading?
然后是生成请求唯一标识的generateReqKey函数。他们用了method、url、params、data拼接成字符串。但是,如果请求体是FormData,比如上传文件,用JSON.stringify可能有问题,因为FormData无法被正确序列化,导致生成的key不唯一或者错误。这时候应该检查data的类型,如果是FormData或Blob等,可能需要特殊处理,或者直接不序列化这部分数据?
接下来看请求拦截器。在请求拦截器中,他们检查是否有重复请求,如果有的话就取消之前的。这里的问题可能在于,当多个相同请求同时发出时,后面的会取消前面的,但如果是并发请求,可能需要更细粒度的控制。不过通常来说,这种处理是合理的,但需要确保生成的key正确,避免误判重复请求。
在全局Loading控制部分,他们用loadingCount来计数,当silentLoading为false时,每次请求增加计数,响应时减少。但这里可能存在竞态条件,比如多个请求同时完成,导致loadingCount减少到负数?不过代码里用了Math.max(0, loadingCount -1),所以不会出现负数。但如果在请求开始和响应之间存在时间差,是否会导致loading显示不正确?
Token处理部分,直接从localStorage获取token,并添加到headers。这可能存在安全问题,比如SSR环境或某些浏览器不支持localStorage。另外,如果token过期,这里可能不会自动刷新,需要结合401处理逻辑。
响应拦截器部分,处理业务逻辑时,直接返回res,但Axios的响应结构是response.data,所以可能没问题。但需要注意,如果后端返回的数据结构不一致,比如code不在200时,抛出错误。这部分可能需要根据实际业务调整。
错误处理部分,检查error.response是否存在,如果不存在说明网络错误。然后根据状态码处理不同情况,比如401跳转登录。但这里可能存在一个问题,当401发生时,如果多个请求同时失败,可能会多次触发跳转登录页面,应该避免重复跳转。另外,在401处理中,直接调用router.replace,但router是否在这个文件中被引入?代码里没有看到导入router,这会导致运行时错误。
另外,在错误处理中,使用ElMessage.error显示错误消息,但如果有多个错误同时发生,可能会重复显示消息,可以考虑增加防抖或队列处理。
其他潜在问题:
1. 当请求被取消时,返回的Promise.reject(error)可能会导致未被捕获的错误,需要确保调用处处理这种情况,或者忽略取消的错误。
2. silentLoading配置项是否合理?是否应该设计为更通用的配置,比如不显示loading或错误提示。
3. 在响应拦截器中,无论请求是否成功,都会关闭loading。但如果多个请求同时进行,其中有一个失败,可能会影响其他请求的loading状态?
4. 没有处理请求超时的特定错误,比如在error处理中,检查error.code是否为axios的TIMEOUT,给出相应提示。
5. 代码的可维护性,比如将状态码处理、loading控制等逻辑拆分到单独的函数或模块中,提高可读性。
6. 使用ElLoading.service()创建loading实例,但没有处理可能的内存泄漏或多次创建的问题,比如在loadingInstance关闭后是否置为null?
改进建议:
1. 处理FormData等不可序列化的data类型,避免生成错误的requestKey。
2. 确保router在401时正确引入,避免运行时错误。
3. 在401处理中,添加标记防止多次跳转登录页。
4. 在错误处理中,区分请求取消的情况,避免显示错误提示。
5. 将状态码处理逻辑提取为独立对象或函数,方便维护。
6. 添加超时错误的处理。
7. 在Loading控制中,使用原子操作避免竞态条件,比如使用锁或异步队列。
8. 对silentLoading配置进行优化,允许更细粒度的控制,比如不显示loading或错误提示。
9. 添加请求重试机制,特别是在token过期后,自动刷新token并重试请求。
10. 将全局状态封装到模块中,避免变量污染。</think>根据您提供的代码,我分析存在以下问题和改进点,并给出优化后的代码:
### 一、主要问题分析
1. **重复请求控制缺陷**
- `generateReqKey` 使用 `JSON.stringify(data)` 处理请求体,当遇到 `FormData` 类型时会失效(如文件上传)
- 未处理 `GET` 请求参数顺序不同但实际相同的情况(如 `?a=1&b=2` 和 `?b=2&a=1`)
2. **Loading 控制问题**
- 多个并发请求时可能出现提前关闭 Loading 的情况
- 未处理请求失败时的 Loading 状态残留
3. **Token 处理缺陷**
- Token 过期后无刷新机制
- 401 处理未防止多次跳转登录页
4. **错误处理不足**
- 未处理网络超时等特定错误类型
- 取消请求时仍会触发错误提示
### 二、优化后完整代码
```javascript
import axios from "axios";
import { ElMessage, ElLoading } from "element-plus";
import router from "@/router"; // 确保引入路由实例
// 封装全局状态管理
const requestControl = {
pending: new Map(),
loading: {
count: 0,
instance: null,
lock: false,
},
auth: {
isRefreshing: false,
retryQueue: [],
},
};
// 智能请求标识生成
const generateReqKey = (config) => {
const { method, url } = config;
let { params, data } = config;
// 处理 GET 参数排序问题
if (method === 'get' && params) {
params = Object.keys(params).sort().reduce((acc, key) => {
acc[key] = params[key];
return acc;
}, {});
}
// 处理 FormData
if (data instanceof FormData) {
data = Array.from(data.entries()).toString();
}
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
};
// 智能 Loading 控制
const handleLoading = (config, type) => {
if (config.silentLoading) return;
if (type === 'open') {
requestControl.loading.count++;
if (!requestControl.loading.instance && !requestControl.loading.lock) {
requestControl.loading.lock = true;
requestControl.loading.instance = ElLoading.service({
lock: true,
text: "加载中...",
background: "rgba(0, 0, 0, 0.7)",
});
}
} else {
requestControl.loading.count = Math.max(0, requestControl.loading.count - 1);
if (requestControl.loading.count === 0 && requestControl.loading.instance) {
requestControl.loading.instance.close();
requestControl.loading.instance = null;
requestControl.loading.lock = false;
}
}
};
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 15000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
// 请求拦截器
service.interceptors.request.use(async (config) => {
const requestKey = generateReqKey(config);
// 取消重复请求
if (requestControl.pending.has(requestKey)) {
requestControl.pending.get(requestKey)('取消重复请求');
}
config.cancelToken = new axios.CancelToken(cancel => {
requestControl.pending.set(requestKey, cancel);
});
// 智能 Loading 控制
handleLoading(config, 'open');
// 自动刷新 Token 逻辑
const token = localStorage.getItem("token");
if (token) {
if (isTokenExpired(token)) { // 需要实现 token 过期检查
if (!requestControl.auth.isRefreshing) {
requestControl.auth.isRefreshing = true;
try {
const newToken = await refreshToken(); // 实现刷新 Token 的方法
localStorage.setItem("token", newToken);
config.headers.Authorization = `Bearer ${newToken}`;
requestControl.auth.retryQueue.forEach(cb => cb(newToken));
} catch (error) {
localStorage.removeItem("token");
router.replace('/login');
return Promise.reject(error);
} finally {
requestControl.auth.isRefreshing = false;
requestControl.auth.retryQueue = [];
}
}
return new Promise((resolve) => {
requestControl.auth.retryQueue.push((newToken) => {
config.headers.Authorization = `Bearer ${newToken}`;
resolve(service(config));
});
});
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
service.interceptors.response.use(
response => {
const requestKey = generateReqKey(response.config);
requestControl.pending.delete(requestKey);
handleLoading(response.config, 'close');
const { data } = response;
if (data?.code !== 200) {
ElMessage.error(data.message || "操作失败");
return Promise.reject(new Error(data.message));
}
return data;
},
error => {
// 处理取消请求的特殊情况
if (axios.isCancel(error)) {
return Promise.reject({ isCancel: true, message: '请求已取消' });
}
const config = error.config || {};
handleLoading(config, 'close');
// 统一错误处理
const errorHandler = {
400: () => '请求参数错误',
401: () => {
if (!router.currentRoute.value.meta.ignoreAuth) {
localStorage.removeItem('token');
if (!requestControl.auth.isRefreshing) {
router.replace('/login');
}
}
return '登录状态已过期';
},
403: () => '没有操作权限',
404: () => '资源不存在',
408: () => '请求超时',
500: () => '服务器内部错误',
502: () => '网关错误',
503: () => '服务不可用',
default: () => `请求失败 (${error.response?.status || '未知'})`
};
const status = error.response?.status;
const message = errorHandler[status]?.() || errorHandler.default();
!config.silentError && ElMessage.error(message);
// 特殊处理网络错误
if (!error.response) {
ElMessage.error("网络连接异常");
}
return Promise.reject(error);
}
);
// 封装常用方法
export const http = {
get: (url, params, config) => service.get(url, { ...config, params }),
post: (url, data, config) => service.post(url, data, config),
put: (url, data, config) => service.put(url, data, config),
delete: (url, config) => service.delete(url, config),
upload: (url, file) => {
const formData = new FormData();
formData.append('file', file);
return service.post(url, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
};
export default service;
```
### 三、主要改进点说明
1. **增强的重复请求控制**
- 处理了 `GET` 请求参数顺序问题
- 支持 `FormData` 类型的智能序列化
- 使用独立的状态管理对象
2. **完善的 Loading 控制**
- 增加操作锁防止并发问题
- 确保所有请求完成后再关闭 Loading
3. **智能 Token 管理**
- 增加自动刷新 Token 机制
- 实现请求队列重试功能
- 完善的 Token 过期检查
4. **增强的错误处理**
- 区分取消请求和真实错误
- 处理网络超时等特殊错误
- 防止多次跳转登录页
- 支持静默错误模式 (`silentError` 配置)
5. **扩展功能**
- 封装常用方法 (GET/POST/PUT/DELETE/上传)
- 支持文件上传的专用方法
- 统一的配置管理
### 四、使用示例
```javascript
// 普通请求
http.get('/api/data', { page: 1 })
.then(data => console.log(data))
.catch(err => {
if (!err.isCancel) {
// 处理真实错误
}
});
// 文件上传
http.upload('/api/upload', file)
.then(res => console.log('上传成功', res))
.catch(err => console.error('上传失败', err));
// 静默请求(不显示 Loading 和错误提示)
http.post('/api/save', { data }, {
silentLoading: true,
silentError: true
});
```
建议根据实际项目需求调整 Token 刷新逻辑和错误处理策略,特别是需要实现 `isTokenExpired` 和 `refreshToken` 方法的具体逻辑。
阅读全文
相关推荐





