Axios 在 Vue3 项目中的封装与使用
在 Vue3 项目开发过程中,与后端进行数据交互是必不可少的环节,而 Axios 作为一款强大的 HTTP 请求库被广泛应用。为了更好地管理请求、处理响应以及统一配置,对 Axios 进行合理封装是十分必要的。以下将详细介绍 Axios 在 Vue3 项目中的封装及使用方法。
一、安装
首先,通过 npm 命令安装 Axios:
npm i axios二、封装步骤
1. 创建相关文件
在 utils 目录下新建 request 文件夹,并在其中创建 index.ts、request.ts 和 status.ts 这三个关键文件,每个文件各司其职,共同完成 Axios 的封装工作。
2. status.ts 文件 - 状态码封装
status.ts 文件主要负责对常见的 HTTP 状态码进行信息封装,方便在请求出现异常时,根据状态码返回友好的提示信息给用户。示例代码如下:
export const ErrMessage = (status: number | string): string => {
let message: string = '';
switch (status) {
case 400:
message = '请求错误!请您稍后重试';
break;
case 401:
message = '未授权!请您重新登录';
break;
case 403:
message = '当前账号无访问权限!';
break;
case 404:
message = '访问的资源不存在!请您稍后重试';
break;
case 405:
message = '请求方式错误!请您稍后重试';
break;
case 408:
message = '请求超时!请您稍后重试';
break;
case 500:
message = '服务异常!请您稍后重试';
break;
case 501:
message = '不支持此请求!请您稍后重试';
break;
case 502:
message = '网关错误!请您稍后重试';
break;
case 503:
message = '服务不可用!请您稍后重试';
break;
case 504:
message = '网关超时!请您稍后重试';
break;
default:
message = '请求失败!请您稍后重试';
}
return message;
};需要注意的是,在此过程中,ESLint 可能会报 switch 前面的空格错误,这时需要修改 .eslintrc.cjs 里的 indent 规则来解决该问题,修改后的规则如下:
rules: {
// Switch语句 https://zh-hans.eslint.org/docs/latest/rules/indent#switchcase
indent: ['error', 2, { SwitchCase: 1 }]
}3. request.ts 文件 - Axios 核心封装
request.ts 文件承担着对 Axios 的核心封装工作,涉及定义请求和响应的数据类型、扩展配置类型、拦截器设置以及常用请求方法的封装等多个方面。
类型定义:
首先定义了一系列与请求和响应相关的类型,确保在整个请求流程中类型的准确性和完整性。// 自定义请求返回数据的类型 interface Data<T> { data: T; code: string; success: boolean; } // 扩展 InternalAxiosRequestConfig,让每个请求都可以控制是否要 loading interface RequestInternalAxiosRequestConfig extends InternalAxiosRequestConfig { showLoading?: boolean; } // 拦截器 interface InterceptorHooks { requestInterceptor?: (config: RequestInternalAxiosRequestConfig) => RequestInternalAxiosRequestConfig; requestInterceptorCatch?: (error: any) => any; responseInterceptor?: (response: AxiosResponse) => AxiosResponse; responseInterceptorCatch?: (error: any) => any; } // 扩展 AxiosRequestConfig,showLoading 给实例默认增加 loading,interceptorHooks 拦截 interface RequestConfig extends AxiosRequestConfig { showLoading?: boolean; interceptorHooks?: InterceptorHooks; }Request 类实现:
创建Request类,在构造函数中初始化配置并创建 Axios 实例,同时设置拦截器来处理请求和响应过程中的通用逻辑。class Request { config: RequestConfig; instance: AxiosInstance; loading?: boolean; // 用 loading 指代加载动画状态 constructor(options: RequestConfig) { this.config = options; this.instance = axios.create(options); this.setupInterceptor(); } // 类型参数的作用,T 决定 AxiosResponse 实例中 data 的类型 request<T = any>(config: RequestConfig): Promise<T> { return new Promise((resolve, reject) => { this.instance .request<any, Data<T>>(config) .then((res) => { resolve(res.data); }) .catch((err) => { reject(err); }); }); } // 封装常用方法 get<T = any>(url: string, params?: object, _object = {}) : Promise<T> { return this.request({ url, params,..._object, method: 'GET' }); } post<T = any>(url: string, params?: object, _object = {}) : Promise<T> { return this.request({ url, params,..._object, method: 'POST' }); } delete<T = any>(url: string, params?: object, _object = {}) : Promise<T> { return this.request({ url, params,..._object, method: 'DELETE' }); } patch<T = any>(url: string, params?: object, _object = {}) : Promise<T> { return this.request({ url, params,..._object, method: 'PATCH' }); } put<T = any>(url: string, params?: object, _object = {}) : Promise<T> { return this.request({ url, params,..._object, method: 'PUT' }); } // 自定义拦截器 https://axios-http.com/zh/docs/interceptors setupInterceptor(): void { /** * 通用拦截 */ this.instance.interceptors.request.use((config: RequestInternalAxiosRequestConfig) => { if (config.showLoading) { // 加载 loading 动画 this.loading = true; } return config; }); // 响应后关闭 loading this.instance.interceptors.response.use( (res) => { if (this.loading) this.loading = false; return res; }, (err) => { const { response, message } = err; if (this.loading) this.loading = false; // 根据不同状态码,返回不同信息 const messageStr = response? ErrMessage(response.status) : message || '请求失败,请重试'; window.alert(messageStr); return Promise.reject(err); } ); /** * 使用通用实例里的拦截,两个拦截都会生效,返回值以后一个执行的为准 */ // 请求拦截 this.instance.interceptors.request.use( this.config?.interceptorHooks?.requestInterceptor, this.config?.interceptorHooks?.requestInterceptorCatch ); // 响应拦截 this.instance.interceptors.response.use( this.config?.interceptorHooks?.responseInterceptor, this.config?.interceptorHooks?.responseInterceptorCatch ); } } export default Request;
4. index.ts 文件 - 创建 Request 实例
index.ts 文件主要用于创建 Request 实例,根据不同的业务需求,可以创建多个实例,比如当需要请求多个不同域名的接口时就很有用。示例代码如下:
/**
* 创建实例,可以多个,当你需要请求多个不同域名的接口时
*/
import Request from './request';
import { getToken } from '@/utils/auth';
const defRequest = new Request({
// 这里用 Easy Mock 模拟了真实接口
baseURL: 'https://mock.mengxuegu.com/mock/65421527a6dde808a695e96d/official/',
timeout: 5000,
showLoading: true,
interceptorHooks: {
requestInterceptor: (config) => {
const token = getToken();
if (token) {
config.headers.Authorization = token;
}
return config;
},
requestInterceptorCatch: (err) => {
return err;
},
responseInterceptor: (res) => {
return res.data;
},
responseInterceptorCatch: (err) => {
return Promise.reject(err);
}
}
});
// 创建其他示例,然后导出
// const otherRequest = new Request({...})
export { defRequest };三、使用方法
1. 创建 API 文件
在 src 目录下新建 api 文件夹,并创建 login.ts 文件用于定义具体的登录接口请求函数。
2. login.ts 文件
在 login.ts 文件中,导入封装好的 Request 实例,并基于它定义登录接口的请求函数,示例如下:
import { defRequest } from '../utils/request';
export const loginApi = (params: any) => {
// 设置 showLoading,timeout 会覆盖 index.ts 里的默认值
return defRequest.post<any>('/login', params, { showLoading: false, timeout: 1000 });
};3. 修改组件使用 API
以 login.vue 组件为例,展示如何在组件中调用接口请求函数,并处理响应结果以及更新相关状态。
<script setup lang="ts">
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useUserStore } from '@store/user';
import { loginApi } from '@/api/login';
defineOptions({
name: 'V-login'
});
const userStore = useUserStore();
const { userInfo, token } = storeToRefs(userStore);
let userName = ref(userInfo.value.name);
let userToken = ref(token);
const updateUserName = () => {
userStore.setUserInfo({
name: userName.value
});
};
const updateUserToken = () => {
userStore.setToken(userToken.value);
};
const login = () => {
loginApi({
name: userName.value
})
.then((res) => {
userName.value = res.name;
userToken.value = res.token;
updateUserToken();
})
.catch((err) => {
console.log(err);
});
};
</script>
<template>
<div>login page</div>
name:
<input type="text" v-model="userName" @input="updateUserName" />
<br />
token:
<input type="text" v-model="userToken" />
<hr />
<button @click="login">login</button>
</template>
<style scoped></style>在组件中,点击 login 按钮,即可触发登录接口请求,看到相应的请求效果。
四、相关说明
1. 关于 InternalAxiosRequestConfig 的使用
Axios 源码有所修改,拦截器传入和返回的参数不再是 AxiosRequestConfig,而是 InternalAxiosRequestConfig 类型。若想深入了解其背后原理,可以查看这篇博文:https://blog.csdn.net/huangfengnt/article/details/131490913。
2. Request 类里的 config 参数
在 Request 类的构造函数中,this.config 会接收所有实例参数,所以在通用实例拦截里使用的是 this.config?.xxx。而通用拦截里使用 config.showLoading,而非 this.config.showLoading,这是为了在实际的 api/login.ts 等具体请求文件里可以再次传入 showLoading 参数,以满足单个请求对于是否显示加载动画的特殊要求。在 config.showLoading 之前可以通过 console.log(this.config, config) 打印这两个 config 查看具体内容。如果在 login.ts 里不传入 showLoading,那么 config.showLoading 会去获取通用实例(即 request/index.ts 里传入的配置)中的 showLoading 值。当然,如果项目不需要全局加载动画,整个 loading 相关逻辑也都可以移除。
3. request/index.ts 和 api/login.ts 参数差异
request/index.ts:可以创建多个实例,一般以baseURL来判断是否需要多个实例,它里面的参数是针对当前baseURL下所有请求的通用参数,拦截规则也是如此,用于统一管理同域名下请求的通用配置。api/login.ts:是具体的请求文件,其大部分参数是请求的url和具体的请求传参。当同一个baseURL下的某些请求有特殊要求时,就可以添加相应参数进行定制化配置。总的来说,request/index.ts侧重于对相同baseURL的请求进行整体封装,而request/request.ts则是对所有请求进行更底层、通用的封装。
4. 优化相关
由于本示例中使用的 Easy Mock 接口支持跨域,所以没有配置代理。但在正常开发接口时,如果接口存在跨域问题,还需要修改 vite.config.ts 里的 proxy 配置。之前的教程里已有代理配置相关说明,此处便不再赘述。另外,baseURL 还可以放在环境变量里,方便区分开发环境和生产环境,以适配不同环境下的接口地址需求。同时,文中示例里的 loading 相关逻辑只是为了提供一种思路,实际项目中可根据具体情况决定是否保留以及如何优化。
通过对 Axios 的合理封装与正确使用,能够在 Vue3 项目中更高效、规范地进行接口请求与数据交互,提升项目整体的开发效率与可维护性。
