Axios 的面向对象封装和拦截器的灵活设计
一、Axios 核心层基础封装
核心层是整个网络请求的基础,负责创建 Axios 实例、封装基础请求方法,为上层提供统一的调用入口,同时预留扩展能力。
1.1 安装依赖
首先安装 Axios,本次使用的版本为 1.13.2,推荐使用 pnpm 包管理工具:
pnpm add axios1.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 中的接口,实现列表查询、详情查看、删除功能,同时处理加载状态和错误状态,验证整个封装链路的可用性。
<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 调用和数据解析
统一的异常捕获,错误信息展示在页面,提升用户体验
删除后重新获取列表,保证数据的实时性
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!