前端新手Vue3+Vite+Ts+Pinia+Sass项目指北系列文章 —— 第十章 请求配置 (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

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 

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();
  }
};
  1. 在登录页面点击登录,触发接口


总结

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

相关推荐
王码码203536 分钟前
Flutter for OpenHarmony:Flutter 三方库 algoliasearch 毫秒级云端搜索体验(云原生搜索引擎)
android·前端·git·flutter·搜索引擎·云原生·harmonyos
发现一只大呆瓜40 分钟前
深入浅出 AST:解密 Vite、Babel编译的底层“黑盒”
前端·面试·vite
天天鸭1 小时前
前端仔写了个 AI Agent,才发现大模型只干了 10% 的活
前端·python·ai编程
发现一只大呆瓜1 小时前
前端模块化:CommonJS、AMD、ES Module三大规范全解析
前端·面试·vite
IT_陈寒1 小时前
一文搞懂JavaScript的核心概念
前端·人工智能·后端
IT_陈寒1 小时前
Java开发者必看!5个提升开发效率的隐藏技巧,你用过几个?
前端·人工智能·后端
前端Hardy1 小时前
Wails v3 正式发布:用 Go 写桌面应用,体积仅 12MB,性能飙升 40%!
前端·javascript·go
Laurence2 小时前
Qt 前后端通信(QWebChannel Js / C++ 互操作):原理、示例、步骤解说
前端·javascript·c++·后端·交互·qwebchannel·互操作
Pu_Nine_92 小时前
JavaScript 字符串与数组核心方法详解
前端·javascript·ecmascript
码云数智-园园2 小时前
从输入 URL 到页面展示:一场精密的互联网交响乐
前端