Vue 3 项目中的 HTTP 请求管理:封装 Axios 实现全局 HTTP 方法
准备工作:下载axios依赖,在项目src目录创建utils文件夹,创建http.js, 目录结构:src/utils/http.js
npm install axios
1. 创建 Axios 实例
什么是 Axios 实例?
Axios 是一个基于 Promise 的 HTTP 库,它允许我们发送请求并接收响应。在封装 Axios 时,我们首先需要创建一个 Axios 实例,便于对所有请求应用统一配置。
代码解析
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router'; // 根据实际项目调整路径
/**
* 创建axios实例
*/
const http = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/', // 配置基础路径
timeout: 10000, // 超时时间
headers: {
'Content-Type': 'application/json', // 默认的请求内容类型
},
});
解释
-
baseURL:是请求的基础 URL。process.env.VUE_APP_BASE_API 是我们项目中通过 .env 文件设置的环境变量,默认值是 ‘/’。它指的是请求的基础路径。
-
timeout:设置请求的超时时间。这里设置为 10 秒,意味着如果请求在 10 秒内没有响应,将会中断该请求并抛出错误。
-
headers:设置请求头,这里我们默认使用 ‘application/json’ 类型,告诉服务器请求体的数据格式是 JSON。
通过这种方式,我们能够在项目中统一配置 Axios 请求的基础参数,减少代码重复。
2. 请求拦截器
什么是请求拦截器?
请求拦截器是 Axios 提供的一种功能,它允许我们在请求发送前对请求进行修改或者添加一些额外的逻辑。通常在请求拦截器中我们可以进行身份验证、设置请求头、取消请求等操作。
代码解析
/**
* 请求拦截器
*/
http.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
const isLoginRequest = config.url.includes('/login');
// 如果没有 Token 且不是登录请求,跳转到登录页
if (!token && !isLoginRequest) {
ElMessage.error('登录已过期,请重新登录');
router.push('/login');
return Promise.reject(new Error('未授权,请登录'));
}
// 如果有 Token,添加到请求头
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 添加取消请求功能
const source = axios.CancelToken.source();
config.cancelToken = source.token;
cancelTokens.set(config.url, source); // 存储取消令牌
return config;
},
(error) => {
ElMessage.error('请求发送失败');
return Promise.reject(error);
}
);
解释
-
Token 验证:在每个请求中,我们从 localStorage 中获取 Token,若 Token 不存在且请求的 URL 不包含 /login,则说明用户未登录,跳转到登录页并返回一个拒绝的 Promise。
-
设置请求头:如果存在 Token,则在请求头中添加 Authorization 字段,这样后端就能识别用户身份,进行权限验证。
-
取消请求:使用 Axios 提供的 CancelToken 来为每个请求添加取消功能,防止多个相同的请求并发执行,特别是在路由切换时取消未完成的请求。
通过请求拦截器,我们能够统一处理请求中的 Token 校验和请求取消,减少重复代码并提高系统的可维护性。
3. 响应拦截器
什么是响应拦截器?
响应拦截器是 Axios 提供的另一个功能,它允许我们在收到响应后对响应数据进行统一处理。响应拦截器主要用于检查响应状态、处理错误信息等。
代码解析
/**
* 响应拦截器
*/
http.interceptors.response.use(
(response) => {
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)) {
console.warn('请求被取消:', error.message);
return Promise.reject(error);
}
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
ElMessage.error('未授权或登录过期,请重新登录');
router.push('/login');
break;
case 403:
ElMessage.error('没有权限访问');
break;
case 404:
ElMessage.error('请求接口不存在');
break;
default:
ElMessage.error(data.message || '服务器错误');
break;
}
} else {
ElMessage.error(error.message || '网络错误');
}
return Promise.reject(error);
}
);
解释
-
业务状态码检查:后端返回的数据通常会包含一个 code 字段,用于表示请求的执行状态。我们根据返回的 code 来判断请求是否成功。如果 code !== 200,则说明请求失败,展示错误信息并拒绝 Promise。
-
错误处理:如果响应中返回了错误(如 401 未授权,403 无权限,404 接口不存在等),我们会根据不同的状态码展示对应的错误信息。
-
请求被取消:通过 axios.isCancel(error) 检测错误是否为请求被取消。如果是请求取消错误,则输出警告并返回。
通过响应拦截器,我们统一处理了各种错误情况,使得前端能够及时反馈给用户。
4. 请求重试机制
为什么需要请求重试?
在一些网络不稳定或者服务器偶尔超时的情况下,我们希望请求能够自动重试,而不是直接失败。我们通过封装请求重试逻辑来实现这一点。
代码解析
/**
* 封装重试逻辑
*/
const retryRequest = async (error, retryCount) => {
const config = error.config;
if (!config || retryCount <= 0) {
return Promise.reject(error);
}
ElMessage.info(`请求重试中... 剩余尝试次数:${retryCount}`);
return new Promise((resolve) => {
setTimeout(() => resolve(http(config)), 1000); // 延迟 1 秒后重试
}).catch((err) => retryRequest(err, retryCount - 1));
};
解释
-
重试次数:如果请求失败且重试次数大于 0,我们会等待 1 秒后再次发起请求。
-
递归重试:每次请求失败时,我们会递归调用 retryRequest 函数,直到达到最大重试次数为止。
通过这个机制,能够提高请求的可靠性,尤其是在网络波动时。
5. 封装 HTTP 请求方法
我们将常见的 HTTP 请求方法封装为一个对象,简化了代码结构,方便在各个组件中调用。
代码解析
/**
* 封装HTTP方法
*/
const httpMethods = {
get: (url, params, options = {}) => http.get(url, { params, ...options }),
post: (url, data, options = {}) => http.post(url, data, { ...options }),
put: (url, data, options = {}) => http.put(url, data, { ...options }),
patch: (url, data, options = {}) => http.patch(url, data, { ...options }),
delete: (url, params, options = {}) => http.delete(url, { params, ...options }),
options: (url, options = {}) => http.options(url, { ...options }),
head: (url, options = {}) => http.head(url, { ...options }),
// 文件下载
download: async (url, options = {}) => {
try {
const response = await http.get(url, {
responseType: 'blob',
...options,
});
const disposition = response.headers['content-disposition'];
let filename = 'downloaded-file';
if (disposition) {
const match = disposition.match(/filename\*?=(?:UTF-8'')?([^;\n]*)/);
if (match) {
filename = decodeURIComponent(match[1].replace(/["']/g, ''));
}
}
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
link.click();
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success('文件下载成功');
} catch (error) {
ElMessage.error('文件下载失败');
console.error(error);
throw error;
}
},
// 取消请求
cancel: (url) => {
const source = cancelTokens.get(url);
if (source) {
source.cancel(`取消请求:${url}`);
cancelTokens.delete(url);
}
},
// 请求方法支持重试
retryableRequest: async (method, url, data, options = {}, retryCount = 1) => {
try {
const response = await httpMethods[method](url, data, options);
return response;
} catch (error) {
if (retryCount > 0) {
return retryRequest(error, retryCount);
}
throw error;
}
},
};
解释
-
封装 HTTP 方法:我们通过 httpMethods 对象封装了常见的 GET, POST, PUT, PATCH, DELETE 请求方法,简化了每个请求的调用。
-
文件下载:download 方法支持从服务器下载文件,处理了响应头中的文件名和 Blob 数据。
-
请求取消:通过 cancel 方法可以取消指定 URL 的请求。
支持重试的请求:retryableRequest 方法允许我们在请求失败时自动重试。
6. http各种情况下使用
以下是针对你提到的各个场景的测试用例,每个 demo 都是如何使用封装后的 HTTP 请求工具的方法。
1. 重试机制的 Demo
在请求失败时,自动重试请求,最多重试 3 次。
代码示例:
import httpMethods from '@/utils/http';
export default {
async mounted() {
try {
const response = await httpMethods.retryableRequest('get', '/api/data', null, {}, 3);
console.log('请求成功:', response);
} catch (error) {
console.error('请求失败:', error);
}
},
};
解释:
- 使用 retryableRequest 方法,第二个参数为 URL,第三个参数为请求参数(这里为 null),最后一个参数为重试次数。
- 如果请求失败且重试次数大于 0,会进行重试。
2. 文件下载的 Demo
通过封装的 download 方法从后端下载文件,并处理文件的保存。
代码示例:
import httpMethods from '@/utils/http';
export default {
methods: {
async downloadFile() {
try {
const url = '/api/file/download';
await httpMethods.download(url);
} catch (error) {
console.error('文件下载失败:', error);
}
},
},
};
解释:
调用 httpMethods.download 方法,传入文件下载的接口 URL 即可自动处理文件下载,
注: 可根据前后端定义类型进行适当调整http中的下载方案。
3. Form 表单提交的 Demo
使用封装的 postForm 方法提交表单数据,处理表单的提交逻辑。
代码示例:
import httpMethods from '@/utils/http';
export default {
data() {
return {
formData: {
username: '',
password: '',
},
};
},
methods: {
async submitForm() {
try {
const response = await httpMethods.postForm('/api/user/login', this.formData);
console.log('登录成功:', response);
} catch (error) {
console.error('登录失败:', error);
}
},
},
};
解释:
使用 postForm 方法提交表单数据。该方法会使用 application/x-www-form-urlencoded 格式发送请求。
4. GET、POST、PUT、PATCH、DELETE 的 Demo
我们可以根据不同的 HTTP 请求类型,使用封装好的方法进行 API 调用。
代码示例:
import httpMethods from '@/utils/http';
export default {
methods: {
async getData() {
try {
const response = await httpMethods.get('/api/data', { id: 123 });
console.log('GET 请求成功:', response);
} catch (error) {
console.error('GET 请求失败:', error);
}
},
async postData() {
try {
const response = await httpMethods.post('/api/data', { name: 'Test' });
console.log('POST 请求成功:', response);
} catch (error) {
console.error('POST 请求失败:', error);
}
},
async putData() {
try {
const response = await httpMethods.put('/api/data/123', { name: 'Updated' });
console.log('PUT 请求成功:', response);
} catch (error) {
console.error('PUT 请求失败:', error);
}
},
async patchData() {
try {
const response = await httpMethods.patch('/api/data/123', { name: 'Patched' });
console.log('PATCH 请求成功:', response);
} catch (error) {
console.error('PATCH 请求失败:', error);
}
},
async deleteData() {
try {
const response = await httpMethods.delete('/api/data/123');
console.log('DELETE 请求成功:', response);
} catch (error) {
console.error('DELETE 请求失败:', error);
}
},
},
};
解释:
-
GET 请求:用于获取资源,通过 httpMethods.get 方法传入 URL 和参数。
-
POST 请求:用于提交数据,通过 httpMethods.post 方法传入 URL 和数据。
-
PUT 请求:用于更新资源,通过 httpMethods.put 方法传入 URL 和数据。
-
PATCH 请求:部分更新资源,使用 httpMethods.patch。
-
DELETE 请求:删除资源,通过 httpMethods.delete 方法。
每个方法的参数分别为: -
GET 请求:URL 和查询参数(params)。
-
POST、PUT、PATCH 请求:URL 和请求数据(data)。
-
DELETE 请求:URL 和查询参数(params)。
7. 总结
通过封装 Axios,我们在 Vue 3 项目中实现了以下功能:
- Token 验证与请求拦截:确保每个请求都有合法的 Token,避免用户未授权访问。
- 响应拦截与错误处理:根据不同的响应状态码提供相应的错误提示。
- 请求重试机制:在网络不稳定时自动重试请求,确保请求成功。
- 文件下载支持:支持下载文件并自动处理文件名和内容。
- 请求取消与性能优化:避免重复请求,提升性能。
通过这种方式,HTTP 请求的处理更加集中化、模块化,使得前端代码更加简洁且易于维护。
完整的http.js代码:
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router'; // 根据实际项目调整路径
/**
* 创建axios实例
*/
const http = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/', // 配置基础路径
timeout: 10000, // 超时时间
headers: {
'Content-Type': 'application/json',
},
});
/**
* 请求取消 Token Map
*/
const cancelTokens = new Map();
/**
* 请求拦截器
*/
http.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
const isLoginRequest = config.url.includes('/login') || config.params?.type === 'login';
// 如果没有 Token 且不是登录请求,跳转到登录页
if (!token && !isLoginRequest) {
ElMessage.error('登录已过期,请重新登录');
router.push('/login');
return Promise.reject(new Error('未授权,请登录'));
}
// 如果有 Token 且不是登录请求,添加到请求头
if (token && !isLoginRequest) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 添加取消请求功能
const source = axios.CancelToken.source();
config.cancelToken = source.token;
cancelTokens.set(config.url, source);
return config;
},
(error) => {
ElMessage.error('请求发送失败');
return Promise.reject(error);
}
);
/**
* 响应拦截器
*/
http.interceptors.response.use(
(response) => {
// 请求成功后移除对应的取消 Token
cancelTokens.delete(response.config.url);
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)) {
console.warn('请求被取消:', error.message);
return Promise.reject(error);
}
// 响应失败时,清理对应的取消 Token
cancelTokens.delete(error.config?.url);
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
ElMessage.error('未授权或登录过期,请重新登录');
localStorage.removeItem('token');
router.push('/login');
break;
case 403:
ElMessage.error('没有权限访问');
break;
case 404:
ElMessage.error('请求接口不存在');
break;
default:
ElMessage.error(data.message || '服务器错误');
break;
}
} else {
ElMessage.error(error.message || '网络错误');
}
return Promise.reject(error);
}
);
/**
* 封装重试逻辑
*/
const retryRequest = async (error, retryCount, showError = true) => {
const config = error.config;
if (!config || retryCount <= 0) {
// 最后一次重试失败,提示错误
if (showError) {
ElMessage.error(error.message || '网络请求失败');
}
return Promise.reject(error);
}
return new Promise((resolve) => {
setTimeout(() => resolve(http(config)), 1000); // 延迟 1 秒后重试
}).catch((err) => retryRequest(err, retryCount - 1, retryCount === 1));
};
/**
* 路由切换时取消未完成的请求
*/
router.beforeEach((to, from, next) => {
if (from.fullPath !== to.fullPath) {
cancelTokens.forEach((source, url) => {
source.cancel(`路由切换取消请求:${url}`);
});
cancelTokens.clear();
}
next();
});
/**
* 封装HTTP方法
*/
const httpMethods = {
get: (url, params, options = {}) => http.get(url, { params, ...options }),
post: (url, data, options = {}) => http.post(url, data, { ...options }),
put: (url, data, options = {}) => http.put(url, data, { ...options }),
patch: (url, data, options = {}) => http.patch(url, data, { ...options }),
delete: (url, params, options = {}) =>
http.delete(url, { params, ...options }),
options: (url, options = {}) => http.options(url, { ...options }),
head: (url, options = {}) => http.head(url, { ...options }),
// 表单请求
postForm: (url, data, options = {}) => http.postForm(url, data, { ...options }),
putForm: (url, data, options = {}) => http.putForm(url, data, { ...options }),
patchForm: (url, data, options = {}) =>
http.patchForm(url, data, { ...options }),
// 文件下载
download: async (url, options = {}) => {
try {
const response = await http.get(url, {
responseType: 'blob',
...options,
});
const disposition = response.headers['content-disposition'];
let filename = 'downloaded-file';
if (disposition) {
const match = disposition.match(/filename\*?=(?:UTF-8'')?([^;\n]*)/);
if (match) {
filename = decodeURIComponent(match[1].replace(/["']/g, ''));
}
}
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
link.click();
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success('文件下载成功');
} catch (error) {
ElMessage.error('文件下载失败');
console.error(error);
throw error;
}
},
// 取消请求
cancel: (url) => {
const source = cancelTokens.get(url);
if (source) {
source.cancel(`取消请求:${url}`);
cancelTokens.delete(url);
}
},
// 请求方法支持重试
retryableRequest: async (method, url, data, options = {}, retryCount = 1) => {
try {
const response = await httpMethods[method](url, data, options);
return response;
} catch (error) {
return retryRequest(error, retryCount);
}
},
};
export default httpMethods;