Skip to content

第十章 请求配置 (Axios请求和响应拦截)

前言

Vue项目中使用 axios 是常见的实践,因为 axios 是一个流行的基于PromiseHTTP 客户端,可用于浏览器和 Node.js 环境。它提供了许多优点,包括易用性、可靠性和在处理 HTTP 请求时的灵活性。

使用 axios 可以轻松地执行 HTTP 请求,包括 GETPOST 等,还可以简化对响应数据的处理。此外,axios 还提供了拦截器(interceptors)、取消请求、全局配置等功能,使其成为在 Vue 项目中进行 HTTP 通信的强大工具。

axios 相比其他 HTTP 客户端库具有几个优势:

    1. 基于 Promise:axios利用 Promise 对象来处理请求和响应,使得代码更加清晰、可读,并且更容易处理异步操作。
    1. 浏览器和Node.js兼容:axios 可以在浏览器和 Node.js 环境中使用,这使得它非常灵活,可以在各种环境中进行 HTTP 通信。
    1. 易用性:axiosAPI 设计非常直观和简单,使其易于学习和使用。它提供了丰富的配置选项和拦截器接口,以满足各种复杂的 HTTP 请求场景。
    1. 错误处理:axios 提供了丰富的错误处理机制,包括全局的错误处理和局部的错误处理,使得在处理HTTP请求过程中能够更好地应对各种异常情况。
    1. 拦截器支持:axios 允许你在发送请求或接收响应前对它们进行拦截和处理,这为添加公共头部、身份验证等操作提供了便利。

一、安装和配置

1、安装

bash
# npn 安装
npm install axios
# yarn 安装
yarn add axios

axios-install

2、配置

axios 不用想其他依赖库需要在 main.tsimport,只需要在文件中请求,就可以使用,不过为了更方便的在项目中使用,我们需要通过拦截器为 axios 做统一的封装,在个在下一节会说到。

在 src 目录在创建 api 文件夹,并创建 userApi.ts 文件

javascript
import axios from "axios";

// 登陆
export function userLogin(data = {}) {
  return axios.post({
    url: "/login",
    data,
  });
}

二、拦截器介绍和使用

1、拦截器介绍

axios 是一个基于 PromiseHTTP 客户端,可用于浏览器和 Node.js。它允许您在发送请求或接收响应时使用拦截器来执行特定的操作。axios 拦截器分为请求拦截器和响应拦截器。

请求拦截器允许您在发送请求之前对其进行修改或添加特定的配置。您可以通过请求拦截器来添加认证信息、设置请求头、转换请求数据等操作。

2、拦截器的使用

请求拦截器

javascript
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  config.headers.Authorization = 'Bearer token'
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

响应拦截器

javascript
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error)
})

三、axios 请求封装

在 utils 文件下创建 service 文件夹,新建四个文件,分别是:config.ts、http.ts、index.ts、types.ts

1、config.ts文件

该文件为 url 等相关配置

typescript
const TEST_BASE_URL = '/mock'
const API_BASE_URL = '/api'
const AUTH_BASE_URL = '/auth'

const TIME_OUT = 30 * 1000

export {
  TEST_BASE_URL,
  API_BASE_URL,
  AUTH_BASE_URL,
  TIME_OUT
}

2、types.ts文件

该文件为ts 的类型声明

typescript
import type { AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'

export interface BaseRequestInterceptors<T = AxiosResponse<any>> {
  requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

export interface BaseRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: BaseRequestInterceptors<T>
  loading?: boolean | string
  noToken?: boolean
}

3、http.ts文件

该文件将请求拦截和相应拦截以及相关逻辑全部封装

定义了一个基于 axios 封装的 BaseRequest 类,用于发送 http 请求。具体功能有:

  1. 创建 axios 实例,并允许用户自定义配置;
  2. 支持使用拦截器对请求和响应进行处理;
  3. 支持在请求中添加 token
  4. 支持在请求中显示 loading
  5. 提供一系列方法用于发送 get、post、put、delete、patch 请求;
  6. 对不同的 http 错误状态码进行处理,并在错误时显示对应的错误信息。
typescript
import axios from "axios";
import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
import { BaseRequestConfig, BaseRequestInterceptors } from "./types";

import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { LoadingInstance } from "element-plus/es/components/loading/src/loading";
import { getToken, logout } from "@/utils/tools/user";

const DEFAULT_LOADING = false;
let isRelogin = false;

class BaseRequest {
  // axios 实例
  instance: AxiosInstance;
  interceptors?: BaseRequestInterceptors;
  showLoading: boolean | string;
  loading?: LoadingInstance;

  constructor(config: BaseRequestConfig) {
    this.instance = axios.create(config);
    this.showLoading = config.loading ?? DEFAULT_LOADING;
    this.interceptors = config.interceptors;

    // 使用拦截器
    // 1. 从config中取出的拦截器是对应的实例的拦截器
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    );
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    );

    // 2.添加所有的实例都有的拦截器
    this.instance.interceptors.request.use(
      (config) => {
        if (config.params && config.params.responseType) {
          config.responseType = config.params.responseType;
          delete config.params.responseType;
        }
        if (
          (config.method === "post" || config.method === "put") &&
          config.params
        ) {
          if (config.params.formData) {
            config.data = config.params.formData;
          } else {
            config.data = { ...config.params };
          }
          delete config.params;
        }
        // console.log('所有的实例都有的拦截器: 请求成功拦截')
        if (this.showLoading) {
          const loadingText =
            typeof this.showLoading == "boolean" ? "加载中" : this.showLoading;
          this.loading = ElLoading.service({
            lock: true,
            text: loadingText + "...",
            background: "rgba(0, 0, 0, 0.5)",
          });
        }
        return config;
      },
      (err) => {
        console.log("所有的实例都有的拦截器: 请求失败拦截");
        return err;
      }
    );

    this.instance.interceptors.response.use(
      (res) => {
        // 将loading移除
        this.loading?.close();
        if (res.status !== 200) return res;
        return res.data;
      },
      (err) => {
        // console.log('所有的实例都有的拦截器: 响应失败拦截')
        // 将loading移除
        this.loading?.close();

        // 例子: 判断不同的HttpErrorCode显示不同的错误信息
        switch (err.response.status) {
          case 400:
            ElMessage.error("请求错误(400)");
            break;
          case 401:
            ElMessage.error("未授权,请重新登录(401)");
            logout();
            break;
          case 403:
            ElMessage.error("拒绝访问(403)");
            break;
          case 404:
            ElMessage.error("请求出错(404)");
            break;
          case 408:
            ElMessage.error("请求超时(408)");
            break;
          case 500:
            ElMessage.error("服务器错误(500)");
            break;
          case 501:
            ElMessage.error("服务未实现(501)");
            break;
          case 502:
            ElMessage.error("网络错误(502)");
            break;
          case 503:
            ElMessage.error("服务不可用(503)");
            break;
          case 504:
            ElMessage.error("网络超时(504)");
            break;
          case 505:
            ElMessage.error("HTTP版本不受支持(505)");
            break;
          default: {
            ElMessage.error(`连接出错(${err.response.status})!`);
          }
        }
        return err;
      }
    );
  }

  request<T = any>(config: BaseRequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 1.单个请求对请求config的处理
      if (config.interceptors?.requestInterceptor) {
        // 对config 进行转化
        config = config.interceptors.requestInterceptor(
          config as InternalAxiosRequestConfig
        );
      }
      // 携带token的拦截
      if (!config.noToken && getToken()) {
        config.headers = {
          Authorization: getToken(),
        };
      }
      // 2.判断是否需要显示loading
      if (config.loading) {
        this.showLoading = config.loading;
      }
      this.instance
        .request<any, T>(config)
        .then((res: any) => {
          // 1.单个请求对数据的处理
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res);
          }
          // 2.将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEFAULT_LOADING;
          if (res.status !== 200) {
            if (config.responseType == "blob") {
              resolve(res);
              return;
            }
            let errMsg = res.message || "请求失败";
            if (res.code == "501") errMsg = "数据中存在敏感词汇, 请修改!";
            if (res.status == 401) {
              if (!isRelogin) {
                isRelogin = true;
                // prettier-ignore
                errMsg = res.message || '登录状态已过期,您可以继续留在该页面,或者重新登录'
                ElMessageBox.confirm(errMsg, "系统提示", {
                  confirmButtonText: "重新登录",
                  cancelButtonText: "取消",
                  type: "warning",
                })
                  .then(() => {
                    isRelogin = false;
                    logout();
                  })
                  .catch(() => {
                    isRelogin = false;
                  });
              }
              reject(res);
              return;
            }
            ElMessage({
              message: errMsg,
              type: "error",
            });
            reject(res);
          } else {
            resolve(res);
          }
        })
        .catch((err) => {
          // 将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEFAULT_LOADING;
          reject(err);
          return err;
        });
    });
  }

  get<T = any>(config: BaseRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "GET" });
  }

  post<T = any>(config: BaseRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "POST" });
  }

  put<T = any>(config: BaseRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "PUT" });
  }

  delete<T = any>(config: BaseRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "DELETE" });
  }

  patch<T = any>(config: BaseRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: "PATCH" });
  }
}

export default BaseRequest;

4、index.ts文件

使用已经封装好的 BaseRequest

typescript
import BaseRequest from './http'
import { TIME_OUT } from './config'

const testRequest = new BaseRequest({
  timeout: TIME_OUT,
  interceptors: {
    requestInterceptor: config => {
      return config
    },
    requestInterceptorCatch: err => {
      // console.log('请求失败的拦截')
      return err
    },
    responseInterceptor: res => {
      // console.log('响应成功的拦截')
      return res
    },
    responseInterceptorCatch: err => {
      const errRes = err.response
      if (errRes) {
        return {
          status: errRes.status,
          message: errRes.data.message
        }
      }
      return err
    }
  }
})

export default testRequest

5、使用封装

修改 login 接口

typescript
import testRequest from "@/utils/service";
import { TEST_BASE_URL } from "@/utils/service/config";

// 登陆
export function userLogin(data = {}) {
  return testRequest.post({
    url: TEST_BASE_URL + "/login",
    data,
  });
}

四、mock 的使用

1、什么是 mockjs

Mock.js 是一个用于生成模拟数据的 JavaScript 库,它能帮助前端开发人员模拟后端接口数据,尤其适用于前后端分离开发中。通过 Mock.js,开发人员可以轻松地创建和配置虚拟接口,从而使前端能够在后端接口尚未完成或不易获取时进行开发和测试。

Mock.js 提供了丰富的数据模拟功能,包括生成随机的文本、数字、日期、布尔值、数组等等,还能模拟不同情况下的数据返回。使用 Mock.js 可以大大提高前端开发效率,促进前后端协作,是一个强大而实用的前端开发工具。

2、安装

bash
# yarn 安装
yarn add mockjs -D

# 这里要特别注意版本,如果版本过高会报错 require 找不到
yarn add vite-plugin-mock@2.9.6 -D

mock-install

3、配置

在根目录创建 mock 文件,并在其中创建 index.ts 文件,增加测试的请求

javascript
import { MockMethod } from "vite-plugin-mock";

const mockItems: MockMethod[] = [
  {
    url: "/mock/login",
    method: "post",
    response: () => {
      return {
        status: 200,
        data: {
          "access_token": "abc",
        },
        message: "success",
      };
    },
  },
];

export default mockItems;

在 vite.config.ts 文件中

javascript
import { viteMockServe } from "vite-plugin-mock";

plugins: [
      vue(),
      nodePolyfills(),
      setupExtend(),
      // 新增代码
      viteMockServe({
        mockPath: "./mock",
        localEnabled: command === "serve"
      }),
      // ...
]

4、界面测试 mock

1. 修改登录按钮逻辑

views文件下 login文件登录逻辑修改如下

typescript
const loginClick = () => {
  loading.value = true;
  const accessToken = getToken();
  if (!accessToken) {
    userLogin()
      .then((res) => {
        setToken(res.data.access_token);
      })
      .then(() => {
        userLoginFunc();
      })
      .catch((err) => {
        ElMessage.error(err);
        loading.value = false;
      });
  } else {
    userLoginFunc();
  }
};

2. 在登录页面点击登录,触发接口

test-01test-02

总结

本文介绍了在 Vue 项目中使用 axios 进行 HTTP 通信的方法。首先详细介绍了 axios 的安装和配置方法,包括 npmyarn 安装以及文件请求的简单示例。然后讲解了 axios 拦截器的作用和使用方法,分别介绍了请求拦截器和响应拦截器的使用。接着讲解了对 axios 请求的封装,包括 configtypeshttp 三个文件的具体内容,以及如何使用封装好的 BaseRequest 类进行请求。最后,介绍了 mockjs 的作用和安装配置方法,展示了 mock 的使用测试请求。上文中的配置代码可在 github 仓库中直接 copy,仓库路径:https://github.com/SmallTeddy/ProjectConstructionHub