Axios 在 Vue3 项目中的封装与使用

在 Vue3 项目开发过程中,与后端进行数据交互是必不可少的环节,而 Axios 作为一款强大的 HTTP 请求库被广泛应用。为了更好地管理请求、处理响应以及统一配置,对 Axios 进行合理封装是十分必要的。以下将详细介绍 Axios 在 Vue3 项目中的封装及使用方法。

一、安装

首先,通过 npm 命令安装 Axios:

npm i axios

二、封装步骤

1. 创建相关文件

utils 目录下新建 request 文件夹,并在其中创建 index.tsrequest.tsstatus.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.tsapi/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 项目中更高效、规范地进行接口请求与数据交互,提升项目整体的开发效率与可维护性。

最后修改:2024 年 12 月 11 日
如果觉得我的文章对你有用,请随意赞赏