在Vue 3项目开发中,处理网络请求是不可避免的核心环节。虽然Axios本身已经提供了强大的API,但如果直接在组件内部使用原生的Axios方法,会导致代码高度耦合。一旦后端接口结构发生变动,或者我们需要统一替换请求头中的鉴权Token,就不得不修改散落在各个组件中的请求代码。为了解决这个痛点,我们需要在工具库层面对Axios进行深度封装,通过创建独立的实例并配置拦截器,将公共逻辑抽离出来,让组件只关注具体的业务数据。

创建独立的Axios实例与基础配置
要构建一个高复用的请求工具库,第一步是创建一个专属的Axios实例。这样做的好处是我们可以将通用的配置集中管理,比如基础路径、超时时间等,避免每次调用都重复设置。通过实例化,我们还能隔离不同业务模块的请求配置,比如用户模块和订单模块可以拥有不同的超时策略。
在Vue 3结合TypeScript的项目中,我们通常会定义一个接口来约束配置项。下面这段代码展示了如何创建一个带有自定义配置的Axios实例。我们设置了基础的请求超时时间,并预留了Token的注入点。通过这种方式,后续所有的请求都会基于这个实例发出,确保了配置的一致性。
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
// 定义扩展配置接口,可以携带自定义字段
export interface CustomRequestConfig extends AxiosRequestConfig {
retryCount?: number; // 请求重试次数
skipError?: boolean; // 是否跳过全局错误提示
}
// 创建Axios实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000, // 设置10秒超时
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
export default service;
在上述配置中,我们利用了Vite的环境变量来动态获取基础路径。这种做法使得我们在切换开发、测试和生产环境时无需修改代码,只需调整对应的env文件即可。同时,自定义的CustomRequestConfig接口允许我们在发起请求时传入额外的控制参数,为后续的拦截器逻辑提供了灵活的扩展能力。
深入理解与应用请求拦截器
请求拦截器是整个封装方案中的核心枢纽之一。它会在请求真正发出之前被执行,这为我们提供了一个统一修改请求配置的绝佳时机。最常见的应用场景就是在请求头中注入鉴权Token,以及控制全局的Loading状态。如果不使用拦截器,我们就得在每次调用接口时手动拼接Token,这显然是低效且容易遗漏的。
在编写请求拦截器时,我们需要注意Token的获取时机和存储位置。在Vue 3中,通常会将Token存储在Pinia或LocalStorage中。拦截器内部可以直接读取这些状态,并将其附加到请求头中。此外,为了防止并发请求时Loading闪烁,我们可以引入一个请求计数器,只有当计数器从0变为1时才显示Loading,当所有请求都结束时才隐藏它。
import service from './request';
import { useUserStore } from '@/store/user';
import { showLoading, hideLoading } from '@/utils/loading';
let loadingCount = 0;
// 请求拦截器
service.interceptors.request.use(
(config) => {
const customConfig = config as CustomRequestConfig;
// 注入Token逻辑
const userStore = useUserStore();
if (userStore.token) {
config.headers['Authorization'] = `Bearer ${userStore.token}`;
}
// Loading控制逻辑
loadingCount++;
if (loadingCount === 1) {
showLoading();
}
return config;
},
(error) => {
// 请求错误处理
return Promise.reject(error);
}
);
上述代码展示了请求拦截器的标准实现。我们通过类型断言获取了自定义配置,然后从Pinia中读取Token并注入到请求头。Loading的控制逻辑采用了计数器模式,完美解决了多个接口并发时的状态同步问题。需要注意的是,在拦截器中调用Pinia之前,必须确保Pinia实例已经被挂载,否则会抛出未激活的错误。
响应拦截器的数据处理与错误兜底
如果说请求拦截器是请求的起点,那么响应拦截器就是请求的终点。它的主要职责是对后端返回的数据进行预处理,剥离掉多余的网络层信息,只将业务数据暴露给组件。同时,它还承担着全局错误兜底的重任,比如处理网络断开、服务器内部错误以及业务逻辑异常等情况。
在处理响应数据时,我们需要与后端约定好统一的数据结构。通常一个规范的接口返回会包含状态码、提示信息和数据体。响应拦截器会根据状态码判断请求是否成功,如果成功则直接返回数据体,如果失败则抛出包含错误信息的Promise。对于特定的错误码(如401未授权),我们还可以在这里统一触发登出逻辑和路由跳转。
import { Message } from '@arco-design/web-vue';
import router from '@/router';
// 响应拦截器
service.interceptors.response.use(
(response) => {
const { data } = response;
const customConfig = response.config as CustomRequestConfig;
// 隐藏Loading
loadingCount--;
if (loadingCount === 0) {
hideLoading();
}
// 根据后端约定的业务状态码处理
if (data.code === 200 || data.code === 0) {
return data.data; // 直接返回业务数据
}
// 处理Token失效
if (data.code === 401) {
// 清除用户信息并跳转登录页
router.push('/login');
return Promise.reject(new Error('登录状态已过期,请重新登录'));
}
// 全局错误提示
if (!customConfig.skipError) {
Message.error(data.message || '请求失败');
}
return Promise.reject(new Error(data.message || 'Error'));
},
(error) => {
// 处理HTTP状态码错误
loadingCount--;
if (loadingCount === 0) {
hideLoading();
}
let errorMessage = '网络异常,请稍后重试';
if (error.response) {
switch (error.response.status) {
case 404:
errorMessage = '请求资源不存在';
break;
case 500:
errorMessage = '服务器内部错误';
break;
}
}
Message.error(errorMessage);
return Promise.reject(error);
}
);
这段代码完整地展示了响应拦截器的处理流程。在成功回调中,我们不仅处理了业务状态码,还结合了之前定义的skipError字段,允许某些特殊请求自行处理错误提示。在错误回调中,我们针对HTTP状态码进行了分类处理,并统一关闭了Loading。这种将错误处理收敛在工具库内部的做法,极大地减轻了组件层的负担。
在Vue 3组件中调用封装后的请求方法
完成了实例创建和拦截器配置后,我们的工具库已经具备了强大的基础能力。但在Vue 3的Composition API中,为了更好地利用响应式系统,我们通常会进一步封装一个useRequest钩子,或者将具体的接口请求方法封装成独立的函数模块。这样不仅让调用更加简洁,也有利于代码的按模块拆分。
在组件内部,我们不再需要关心Axios的任何配置细节,只需要像调用普通函数一样调用接口方法即可。配合Vue 3的setup语法糖,我们可以非常优雅地处理异步数据加载、加载状态以及错误捕获。下面是一个在组件中调用封装好的请求方法的示例。
<template>
<div>
<button @click="fetchUserInfo">获取用户信息</button>
<p v-if="loading">加载中...</p>
<p v-else>{{ userInfo.name }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { getUserInfo } from '@/api/user';
const userInfo = ref<any>({});
const loading = ref(false);
const fetchUserInfo = async () => {
loading.value = true;
try {
// 直接拿到业务数据,无需处理外层包裹
const data = await getUserInfo({ id: 123 });
userInfo.value = data;
} catch (error) {
console.error('获取用户信息失败', error);
} finally {
loading.value = false;
}
};
</script>
从上述组件代码可以看出,由于我们在响应拦截器中已经剥离了外层结构,组件中拿到的直接就是可用的业务数据。同时,全局的Loading和错误提示已经由拦截器接管,组件内部只需要关注局部的loading状态和具体的业务逻辑即可。这种清晰的职责划分,使得代码的可读性和可测试性都得到了显著提升。
总结来说,Vue 3中Axios的封装不仅仅是写几个工具函数,更是一次系统性的架构设计。通过实例隔离配置,通过请求拦截器统一注入鉴权,通过响应拦截器集中处理数据与错误,最终为组件层提供极简的调用体验。掌握这套封装思路,能够有效提升项目的工程质量。