Vue3+TS 进阶实战:Axios 核心层封装与拦截器设计

AI 概述
本文聚焦Vue3中Axios的面向对象封装与拦截器设计。首先完成Axios核心层基础封装,包括安装依赖、定义配置类型、封装基础类等;接着实现拦截器的优雅设计,提供默认实现并支持项目自定义;随后融合HttpClient与拦截器,在项目配置层进行个性化配置并导出全局实例;最后在业务层基于抽象类实现RESTful接口。通过实际页面调用验证了封装链路的可用性,为后续取消请求、请求防重、请求重试等增强功能的实现打下基础。
目录
文章目录隐藏
  1. 一、Axios 核心层基础封装
  2. 二、拦截器的优雅设计与实现
  3. 三、融合 HttpClient 与拦截器
  4. 四、项目配置层:自定义配置与全局实例导出
  5. 五、业务层封装:基于抽象类实现 RESTful 接口
  6. 六、实战测试:在 Vue3 页面中调用封装的接口
  7. 写在最后

Vue3+TS 进阶实战:Axios 核心层封装与拦截器设计

本篇继续 Vue3 通用模板的网络请求封装系列,上一篇我们完成了整体架构设计和 Mock 服务的搭建,本次核心聚焦 Axios 的面向对象封装和拦截器的灵活设计,同时带大家跳出 “把 TS 当有类型的 JS 用” 的误区,真正用 TS 实现类型化的网络请求层。

本系列的网络请求封装会采用渐进式实现,后续还会陆续讲解取消请求、请求防重、请求重试以及 useRequest 组合式函数封装,本次先完成最基础也是最核心的 Axios 核心层和拦截器封装,打好基础才能让后续的扩展更优雅。

一、Axios 核心层基础封装

核心层是整个网络请求的基础,负责创建 Axios 实例、封装基础请求方法,为上层提供统一的调用入口,同时预留扩展能力。

1.1 安装依赖

首先安装 Axios,本次使用的版本为 1.13.2,推荐使用 pnpm 包管理工具:

pnpm add axios

1.2 定义配置类型

基于 TS 的类型约束,我们先在 src/http/core/types.ts 中定义 HTTP 客户端的配置类型,让配置项具备强类型校验,后续可根据需求扩展:

/**
 * HTTP 请求客户端配置
 */
export interface HttpClientConfig {
  baseURL?: string // 请求基础路径
  timeout?: number // 超时时间
  headers?: Record<string, string> // 公共请求头
  // 后续扩展其他配置项:如拦截器、请求重试次数等
}

1.3 封装 HttpClient 基础类

在 src/http/core/http-client.ts 中创建 HttpClient 类,负责 Axios 实例的创建和基础请求方法的封装,采用面向对象的方式让后续扩展更灵活:

import { Env } from '@/utils/env.ts'
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
import type { HttpClientConfig } from './types.ts'

// HTTP 请求客户端的默认配置
const defaultConfig: HttpClientConfig = {
  baseURL: Env.get('VITE_API_BASE_URL', '/api'), // 从环境变量获取基础路径,默认/api
  timeout: 3000, // 默认 3 秒超时
  headers: {
    'Content-Type': 'application/json;charset=utf-8', // 默认 JSON 请求头
  },
}

/**
 * HttpClient 基础 HTTP 客户端类
 * 负责创建 Axios 实例和封装基础请求方法
 */
export class HttpClient {
  protected instance: AxiosInstance // Axios 实例
  protected config: HttpClientConfig // 合并后的配置

  /**
   * 构造函数:合并默认配置和自定义配置
   * @param config 自定义配置
   */
  constructor(config: HttpClientConfig = {}) {
    this.config = { ...defaultConfig, ...config }
    this.instance = this.createInstance()
  }

  /**
   * 私有方法:创建 Axios 实例
   * @returns AxiosInstance
   */
  private createInstance(): AxiosInstance {
    return axios.create({
      baseURL: this.config.baseURL,
      timeout: this.config.timeout,
      headers: this.config.headers,
    })
  }

  // 封装 GET 请求
  public get(url: string, config?: AxiosRequestConfig): Promise<any> {
    return this.instance.get(url, config)
  }

  // 封装 POST 请求
  public post(url: string, data?: any, config?: AxiosRequestConfig): Promise<any> {
    return this.instance.post(url, data, config)
  }

  // 封装 PUT 请求
  public put(url: string, data?: any, config?: AxiosRequestConfig): Promise<any> {
    return this.instance.put(url, data, config)
  }

  // 封装 DELETE 请求
  public delete(url: string, config?: AxiosRequestConfig): Promise<any> {
    return this.instance.delete(url, config)
  }

  // 封装 PATCH 请求
  public patch(url: string, data?: any, config?: AxiosRequestConfig): Promise<any> {
    return this.instance.patch(url, data, config)
  }

  /**
   * 公共方法:获取 Axios 原生实例
   * 用于文件上传、下载等个性化需求
   * @returns AxiosInstance
   */
  public getInstance(): AxiosInstance {
    return this.instance
  }
}

核心封装要点:

  1. 构造函数自动合并默认配置和自定义配置,无需手动重复设置;
  2. 抽离 createInstance 私有方法,统一创建 Axios 实例,便于后续修改实例配置;
  3. 封装 RESTful 风格的常用请求方法(GET/POST/PUT/DELETE/PATCH),上层直接调用;
  4. 提供 getInstance 方法获取 Axios 原生实例,满足文件上传、下载等个性化场景。

二、拦截器的优雅设计与实现

拦截器是 Axios 的核心特性,负责请求发送前和响应返回后的统一处理。本次设计的核心原则是:提供默认拦截器实现,同时支持项目自定义拦截器,满足不同项目的差异化需求。

拦截器分为请求拦截器和响应拦截器,两者各包含成功处理函数(onFulfilled)和失败处理函数(onRejected) ,共四个核心处理函数。

2.1 扩展拦截器配置类型

先在 src/http/core/types.ts 中追加拦截器的类型定义,并扩展到 HttpClientConfig 中,让拦截器配置具备强类型:

import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'

/**
 * 拦截器配置
 */
export interface InterceptorConfig {
  request?: {
    onFulfilled?: (config: AxiosRequestConfig) => AxiosRequestConfig | Promise<AxiosRequestConfig>
    onRejected?: (error: AxiosError) => any
  }
  response?: {
    onFulfilled?: (response: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>
    onRejected?: (error: AxiosError) => any
  }
}

/**
 * 扩展后的 HTTP 请求客户端配置
 */
export interface HttpClientConfig {
  baseURL?: string
  timeout?: number
  headers?: Record<string, string>
  interceptor?: InterceptorConfig // 新增拦截器配置
}

// 后续扩展分页相关类型(供业务层使用)
export interface PageReq {
  pageNum: number
  pageSize: number
}
export interface PageData<T> {
  list: T[]
  total: number
}

2.2 实现拦截器管理类

在 src/http/core/interceptors.ts 中实现拦截器的核心逻辑,分为两部分:默认拦截器函数实现和拦截器管理类,同时将默认函数导出,方便外部自定义时复用。

import { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, AxiosError } from 'axios'
import { Env } from '@/utils/env'
import type { InterceptorConfig } from './types.ts'

// ---------------------- 默认拦截器函数实现 ----------------------
/**
 * 默认请求拦截器-成功:开发环境打印请求日志
 * @param config Axios 请求配置
 * @returns 处理后的请求配置
 */
export const defaultRequestOnFulfilled = (config: AxiosRequestConfig) => {
  if (Env.isDev) {
    console.log('Request:', {
      url: config.url,
      method: config.method,
      params: config.params,
      data: config.data,
    })
  }
  return config
}

/**
 * 默认请求拦截器-失败:直接抛出错误
 * @param error 错误对象
 * @returns Promise.reject
 */
export const defaultRequestOnRejected = (error: AxiosError): any => {
  return Promise.reject(error)
}

/**
 * 默认响应拦截器-成功:统一解析响应数据,简化业务层调用
 * 核心:业务成功直接返回 data,无需业务层写 res.data.data;业务失败抛出错误
 * @param response Axios 响应对象
 * @returns 解析后的业务数据
 */
export const defaultResponseOnFulfilled = (response: AxiosResponse): any => {
  // 开发环境打印响应日志
  if (Env.isDev) {
    console.log('Response:', {
      url: response.config.url,
      status: response.status,
      data: response.data,
    })
  }
  const { data } = response
  // 解析标准 API 响应格式({ code, message, data })
  if (data && typeof data === 'object' && 'code' in data && 'message' in data) {
    const { code, message, data: responseData } = data
    if (code === 0 || code === 200) {
      return responseData // 业务成功,直接返回业务数据
    } else {
      throw new Error(message || '请求失败') // 业务失败,抛出错误供业务层捕获
    }
  }
  // 非标准格式,直接返回响应数据
  return data
}

/**
 * 默认响应拦截器-失败:统一处理网络错误、状态码错误
 * @param error 错误对象
 * @returns Promise.reject
 */
export const defaultResponseOnRejected = (error: AxiosError): any => {
  if (error.response) {
    // 服务器返回错误状态码(4xx/5xx)
    const status = error.response.status
    console.error('request error, status: ', status)
  } else if (error.request) {
    // 请求已发出,未收到服务器响应(网络错误)
    console.error('Network Error')
  } else {
    // 请求配置错误
    console.error('Request Config Error:', error.message)
  }
  return Promise.reject(error)
}

// ---------------------- 拦截器管理类 ----------------------
/**
 * 拦截器管理类:负责配置和管理请求/响应拦截器
 * 核心:自定义拦截器优先,无自定义则使用默认拦截器
 */
export class Interceptors {
  private config: InterceptorConfig

  constructor(config: InterceptorConfig = {}) {
    this.config = config
  }

  /**
   * 应用拦截器到 Axios 实例
   * @param instance Axios 实例
   */
  public applyInterceptors(instance: AxiosInstance): void {
    // 应用请求拦截器
    instance.interceptors.request.use(
      this.config.request?.onFulfilled ?? defaultRequestOnFulfilled,
      this.config.request?.onRejected ?? defaultRequestOnRejected
    )
    // 应用响应拦截器
    instance.interceptors.response.use(
      this.config.response?.onFulfilled || defaultResponseOnFulfilled,
      this.config.response?.onRejected || defaultResponseOnRejected
    )
  }
}

拦截器设计亮点:

  1. 默认 + 自定义结合:项目无自定义拦截器时使用默认实现,有自定义时自动覆盖,兼顾通用性和灵活性;
  2. 统一响应解析:默认响应拦截器自动解析标准 API 格式,业务层直接获取真实数据,避免重复写 res.data.data;
  3. 开发环境日志:仅在开发环境打印请求 / 响应日志,生产环境无冗余日志;
  4. 统一错误处理:响应失败时区分网络错误、状态码错误、配置错误,便于问题排查。

三、融合 HttpClient 与拦截器

目前 HttpClient 和 Interceptors 是两个独立的类,需要将两者融合,让 HttpClient 创建 Axios 实例后自动应用拦截器,同时抽离扩展方法,为后续添加更多拦截器预留空间。

修改 src/http/core/http-client.ts,整合拦截器逻辑:

// 新增导入拦截器相关
import { Interceptors } from './interceptors.ts'
import type { InterceptorConfig } from './types.ts'

// ... 保留原有默认配置和类型导入 ...

export class HttpClient {
  protected instance: AxiosInstance
  protected config: HttpClientConfig
  private interceptors: Interceptors // 新增:拦截器实例

  /**
   * 构造函数:合并配置 + 实例化拦截器 + 创建 Axios 实例 + 应用拦截器
   * @param config 客户端配置
   */
  constructor(config: HttpClientConfig = {}) {
    this.config = { ...defaultConfig, ...config }
    // 实例化拦截器:使用配置中的拦截器配置
    this.interceptors = new Interceptors(this.config.interceptor ?? {})
    this.instance = this.createInstance()
    // 应用拦截器
    this.setInterceptors()
  }

  // ... 保留 createInstance 和基础请求方法 ...

  /**
   * 私有方法:设置拦截器
   * 抽离为独立方法,便于后续添加更多拦截器(如取消请求、防重拦截器)
   */
  private setInterceptors() {
    this.interceptors.applyInterceptors(this.instance)
  }

  // ... 保留 getInstance 方法 ...
}

最后在 src/http/core/index.ts 中统一导出核心层的所有内容,简化上层导入:

export * from './types'
export * from './http-client'
export * from './interceptors'

四、项目配置层:自定义配置与全局实例导出

核心层实现了通用的封装,项目配置层则负责根据项目需求做个性化配置,并创建全局的 HttpClient 实例,供业务层直接调用,让核心层与项目业务解耦。

在 src/http/index.ts 中实现项目配置层,示例为请求头添加 token,可根据项目需求自定义响应拦截器(如结合 UI 组件展示错误提示):

import { HttpClient } from '@/http/core/http-client.ts'
import type { AxiosRequestConfig } from 'axios'

/**
 * 项目自定义请求拦截器-成功:在请求头中添加 token
 * 可根据项目需求修改(如添加 token、语言标识等)
 * @param config Axios 请求配置
 * @returns 处理后的配置
 */
const customRequestOnFulfilled = (config: AxiosRequestConfig) => {
  const { headers = {} } = config
  headers.token = 'xxxxxx' // 实际项目中从本地存储/状态管理中获取
  return config
}

// 创建并导出全局的 HttpClient 实例:api
export const api = new HttpClient({
  interceptor: {
    request: {
      onFulfilled: customRequestOnFulfilled, // 使用自定义请求拦截器
    },
    // 可自定义响应拦截器:response: { onFulfilled: xxx, onRejected: xxx }
  },
  // 也可自定义 baseURL、timeout 等:baseURL: '/api/v2', timeout: 5000
})

// 导出 Axios 原生实例,用于文件上传、下载等个性化需求
export const instance = api.getInstance()

// 若无任何自定义配置,直接创建即可:export const api = new HttpClient()

配置层核心作用:

  1. 所有项目个性化配置都集中在此,无需修改核心层代码,符合开闭原则;
  2. 对外暴露统一的 api 实例,业务层直接导入使用,无需重复创建 HttpClient;
  3. 导出 Axios 原生实例 instance,满足文件上传、下载等需要直接操作 Axios 的场景。

五、业务层封装:基于抽象类实现 RESTful 接口

业务层(也有项目称其为 api 层)负责封装具体的业务接口,本次基于 TS 抽象类实现 RESTful 风格的接口封装,抽离通用的 CRUD 方法,让业务接口的封装更高效、更统一。

5.1 抽离 BaseService 抽象类

在 src/services/base-service.ts 中创建抽象类 BaseService,封装通用的分页查询、详情、创建、更新、删除方法,子类只需实现资源前缀即可快速拥有全套 CRUD 方法:

import { api } from '@/http'
import type { PageData, PageReq } from '@/http/core/types.ts'

/**
 * 基础服务抽象类:封装 RESTful 风格的通用 CRUD 方法
 * @template T 业务实体类型
 * @template Q 分页查询参数类型(继承 PageReq)
 */
export abstract class BaseService<T, Q extends PageReq> {
  /**
   * 抽象方法:获取资源前缀(由子类实现)
   * 如:demo、user、product
   * @returns 资源前缀字符串
   */
  protected abstract getPrefix(): string

  // 分页查询列表
  public getList(params: Q): Promise<PageData<T>> {
    return api.get(`/${this.getPrefix()}`, { params })
  }

  // 根据 ID 获取详情
  public getDetail(id: number): Promise<T> {
    return api.get(`/${this.getPrefix()}/${id}`)
  }

  // 创建实体
  public create(data: Partial<T>): Promise<T> {
    return api.post(`/${this.getPrefix()}`, data)
  }

  // 根据 ID 更新实体
  public update(id: number, data: Partial<T>): Promise<T> {
    return api.put(`/${this.getPrefix()}/${id}`, data)
  }

  // 根据 ID 删除实体
  public delete(id: number): any {
    return api.delete(`/${this.getPrefix()}/${id}`)
  }
}

5.2 实现业务 Service 子类

以 Demo 业务为例,在 src/services/demo-service.ts 中实现 BaseService 的子类,只需定义实体类型、查询参数类型,并实现 getPrefix 方法,即可快速完成接口封装:

import { BaseService } from './base-service.ts'
import type { PageReq } from '@/http/core/types.ts'

/**
 * Demo 业务实体类型
 */
export interface Demo {
  id: number
  title: string
  content: string
  author: string
  status: boolean
  createdAt: string
  updatedAt: string
}

/**
 * Demo 分页查询参数:继承 PageReq,扩展 keyword 关键字
 */
export interface DemoListReq extends PageReq {
  keyword?: string
}

/**
 * Demo 服务类:实现基础服务抽象类
 */
export class DemoService extends BaseService<Demo, DemoListReq> {
  // 实现抽象方法:资源前缀为 demo
  protected getPrefix(): string {
    return 'demo'
  }

  // 可添加 Demo 业务的自定义方法(如启用/停用 Demo)
  // public enable(id: number): Promise<Demo> {
  //   return api.patch(`/demo/${id}/enable`)
  // }
}

// 创建并导出 Demo 服务实例
export const demoService = new DemoService()

业务层封装意义:

  1. 统一接口规范:基于 RESTful 风格封装,所有业务接口的调用方式保持一致;
  2. 减少重复代码:抽离通用 CRUD 方法,子类无需重复实现,提升开发效率;
  3. 强类型约束:通过 TS 泛型实现实体和查询参数的类型校验,避免传参错误;
  4. 便于维护:接口地址集中管理,后续修改接口路径只需修改资源前缀,无需逐个修改业务代码。

题外话:很多同学疑惑为什么要抽离这一层,直接在页面中调用 api 不更香吗?其实核心不是为了 “复用”,而是为了统一接口处理逻辑,让页面层专注于 UI 和业务逻辑,接口层专注于接口封装,符合单一职责原则。

六、实战测试:在 Vue3 页面中调用封装的接口

完成以上封装后,我们创建一个测试页面 src/pages/http-demo.vue,调用 demoService 中的接口,实现列表查询、详情查看、删除功能,同时处理加载状态和错误状态,验证整个封装链路的可用性。

<template>
  <div class="demo-page">
    <h1>Demo 列表</h1>
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    <!-- 错误状态 -->
    <div v-else-if="error" class="error">错误: {{ error.message }}</div>
    <!-- 列表展示 -->
    <div v-else class="demo-list">
      <ul>
        <li v-for="item in data" :key="item.id" class="demo-item">
          <span 
            class="demo-title" 
            @click="onTitleClick(item.id)"
          >{{ item.title }}</span>
          <button 
            class="delete-btn" 
            @click="deleteItem(item.id)"
          >删除</button>
        </li>
      </ul>
      <!-- 分页组件 -->
      <div class="pagination">
        <button 
          @click="fetchData({ pageNum: currentPage - 1 })"
          :disabled="currentPage === 1"
        >上一页</button>
        <span class="page-info">{{ currentPage }}/{{ totalPages }}</span>
        <button 
          @click="fetchData({ pageNum: currentPage + 1 })"
          :disabled="currentPage === totalPages"
        >下一页</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { Demo, DemoListReq } from '@/services/demo-service.ts'
import { demoService } from '@/services/demo-service.ts'

// 状态管理
const loading = ref(false) // 加载状态
const error = ref<Error | null>(null) // 错误状态
const data = ref<Demo[]>([]) // 列表数据
const currentPage = ref(1) // 当前页码
const pageSize = ref(10) // 每页条数
const total = ref(0) // 总条数

// 计算属性:总页数
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

/**
 * 获取列表数据
 * @param params 分页查询参数(可选)
 */
const fetchData = async (params?: Partial<DemoListReq>) => {
  loading.value = true
  error.value = null
  try {
    const response = await demoService.getList({
      pageNum: params?.pageNum || currentPage.value,
      pageSize: pageSize.value,
      ...params,
    })
    data.value = response.list
    total.value = response.total
    currentPage.value = params?.pageNum || currentPage.value
  } catch (err) {
    error.value = err as Error
  } finally {
    loading.value = false
  }
}

/**
 * 删除 Demo 项
 * @param id DemoID
 */
const deleteItem = async (id: number) => {
  await demoService.delete(id)
  await fetchData() // 删除后重新获取列表
}

/**
 * 点击标题查看详情
 * @param id DemoID
 */
const onTitleClick = async (id: number) => {
  const resp = await demoService.getDetail(id)
  console.log('Demo 详情:', resp)
}

// 组件挂载时初始化获取列表
onMounted(() => {
  fetchData()
})
</script>

<style scoped lang="scss">
.demo-page {
  padding: 20px;
  .loading, .error {
    margin: 20px 0;
    font-size: 16px;
  }
  .error {
    color: #f56c6c;
  }
  .demo-list {
    ul {
      list-style: none;
      padding: 0;
      .demo-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 10px 0;
        border-bottom: 1px solid #eee;
        .demo-title {
          font-size: 18px;
          font-weight: bold;
          color: #409eff;
          cursor: pointer;
        }
        .delete-btn {
          color: #f56c6c;
          border: none;
          background: transparent;
          cursor: pointer;
        }
      }
    }
    .pagination {
      margin-top: 20px;
      display: flex;
      align-items: center;
      gap: 10px;
      button {
        padding: 4px 12px;
        border: 1px solid #eee;
        background: #fff;
        cursor: pointer;
        &:disabled {
          cursor: not-allowed;
          color: #ccc;
        }
      }
      .page-info {
        font-size: 14px;
      }
    }
  }
}
</style>

页面核心逻辑:

  1. 使用 Vue3 的组合式 API 管理状态,分离加载、错误、数据状态;
  2. 调用 demoService 的封装方法,无需关心底层的 Axios 调用和数据解析;
  3. 统一的异常捕获,错误信息展示在页面,提升用户体验;
  4. 删除后重新获取列表,保证数据的实时性。

写在最后

本次我们完成了 Vue3+TS 环境下 Axios 网络请求的核心层封装、拦截器设计、项目配置层和业务层封装,并通过实际页面验证了整个封装链路的可用性,核心收获有三点:

  1. 采用面向对象的方式封装 Axios,让扩展更灵活,符合开闭原则;
  2. 设计默认 + 自定义的拦截器模式,兼顾通用性和项目差异化需求;
  3. 基于 TS 抽象类和泛型实现业务层封装,让接口更规范、类型更安全。

本次的封装是整个网络请求层的基础,后续我们会在这个基础上继续增强核心能力:实现取消请求、请求防重,解决项目中常见的重复请求、页面切换取消未完成请求等问题,再实现请求重试和 useRequest 组合式函数封装,让页面层的调用更简洁。

建议大家手动敲一遍代码,理解每个环节的封装思路,而不是直接复制粘贴,只有理解了底层逻辑,才能根据自己的项目需求做出合适的调整。

如果对本次封装有任何疑问、建议,或者有更好的封装思路,欢迎在评论区留言交流!

文章来源公众号:AI 懒人码农

以上关于Vue3+TS 进阶实战:Axios 核心层封装与拦截器设计的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

23

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » Vue3+TS 进阶实战:Axios 核心层封装与拦截器设计

发表回复