Axios 的面向对象封装和拦截器的灵活设计

更新日期: 2026-03-24 阅读: 18 标签: 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
  }
}

核心封装要点

  • 构造函数自动合并默认配置和自定义配置,无需手动重复设置

  • 抽离 createInstance 私有方法,统一创建 Axios 实例,便于后续修改实例配置

  • 封装 RESTful 风格的常用请求方法(GET/POST/PUT/DELETE/PATCH),上层直接调用

  • 提供 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
    )
  }
}

拦截器设计亮点

  • 默认 + 自定义结合:项目无自定义拦截器时使用默认实现,有自定义时自动覆盖,兼顾通用性和灵活性

  • 统一响应解析:默认响应拦截器自动解析标准 API 格式,业务层直接获取真实数据,避免重复写 res.data.data

  • 开发环境日志:仅在开发环境打印请求/响应日志,生产环境无冗余日志

  • 统一错误处理:响应失败时区分网络错误、状态码错误、配置错误,便于问题排查


三、融合 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()

配置层核心作用

  • 所有项目个性化配置都集中在此,无需修改核心层代码,符合开闭原则

  • 对外暴露统一的 api 实例,业务层直接导入使用,无需重复创建 HttpClient

  • 导出 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()

业务层封装意义

  • 统一接口规范:基于 RESTful 风格封装,所有业务接口的调用方式保持一致

  • 减少重复代码:抽离通用 CRUD 方法,子类无需重复实现,提升开发效率

  • 强类型约束:通过 TS 泛型实现实体和查询参数的类型校验,避免传参错误

  • 便于维护:接口地址集中管理,后续修改接口路径只需修改资源前缀,无需逐个修改业务代码

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

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

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

vue
<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>

页面核心逻辑

  • 使用 Vue3 的组合式 API 管理状态,分离加载、错误、数据状态

  • 调用 demoService 的封装方法,无需关心底层的 Axios 调用和数据解析

  • 统一的异常捕获,错误信息展示在页面,提升用户体验

  • 删除后重新获取列表,保证数据的实时性

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13491

相关推荐

axios常见传参方式_axios中get/post/put/patch请求

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。axios中get/post/put/patch请求。传参格式为 formData ,传参格式为 query 形式 ,传参格式为 raw等

axios的特点与使用_解决处理axios兼容性问题

axios基于 Promise 的 HTTP 请求客户端,可同时在浏览器和 node.js 中使用。项目中发现,在安卓4.3及以下的手机不支持axios的使用,主要就是无法使用promise。加上以下polyfill就可以了。

vue中axios的使用与封装

分享下我自己的axios封装,axios是个很好用的插件,都是一些params对象,所以很方便做一些统一处理。当然首先是npm安装axios 很简单。在src下新建文件夹 service / index.js,接着上代码

vue axios不缓存get请求(防止返回304不更新数据)

最近做项目遇到一款浏览器,由于缓存了get请求,导致不管如何刷新,数据都不更新的问题。以下分享一下解决办法:解决思路就是给每一条get请求增加一个timestamp的参数,value为时间戳

axios处理Http请求的基本使用方法总汇

axios的功能特性:在浏览器中发送 XMLHttpRequests 请求,在 node.js 中发送 http请求,支持 Promise API,拦截请求和响应,转换请求和响应数据,自动转换 JSON 数据,客户端支持保护安全免受 XSRF 攻击

axios-mock-adapter_一个axios调试好用的工具

axios-mock-adapter可以用来拦截http请求,并模拟响应,使用起来也很简单,比如你想模拟下服务器返回个500错误,什么404找不到、403禁止访问、500服务器错误、503服务不可用、504网关超时等等,你都能模拟出来

Vue+Typescript中在Vue上挂载axios使用时报错

在vue项目开发过程中,为了方便在各个组件中调用axios,我们通常会在入口文件将axios挂载到vue原型身上,如下:这样的话,我们在各个组件中进行请求时

Vue项目中使用Axios封装http请求

使用axios可以统一做请求响应拦截,例如请求响应时我们拦截响应信息,判断状态码,从而弹出报错信息。请求超时的时候断开请求,还可以很方便地使用then或者catch来处理请求。

vue中axios请求的封装

发送请求模块目录,@/api/url中存放的是每个模块的URL,使用webpack提供的require.context将src/api/url下后缀为js的所有文件引入,并整理出一个对象。整合common.js & product.js,最终得到的对象如下:

使用axios请求,前端数字long类型精度问题解决方法

后台数据库定义的bigint类型(对应Long)的值太长会导致传递给前端的时候精度丢失,其原因是后端语言和js对字节码的解析长度不一样。前端js对Long类型支持的精度不够,导致后端使用的Long传到前端丢失精度。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!