返回 导航

Vue.js

hangge.com

Vue.js 3 - 封装Axios+TypeScript网络请求类教程(拦截器、类型定义、Loading)

作者:hangge | 2026-10-29 09:42
            在实际开发中,我们会对网络请求进行二次封装,以提高代码的可复用性和可维护性。通过封装 axios,我们可以统一处理所有的请求、响应及异常,避免在每个组件中重复编写相同的逻辑。 本文将详细介绍如何在 Vue3 + TypeScript 项目中对 axios 进行封装,包括创建请求类、添加拦截器、类型定义以及 Loading 加载效果等。

一、为什么需要封装 axios

1,封装的优势

(1)在实际项目开发中,直接使用 axios 会存在以下问题:
  • 每次请求都需要重复设置 baseURL、timeout 等配置
  • 需要在每个请求中手动处理错误和异常
  • 请求头中添加 Token 等认证信息需要重复编写
  • 响应数据的类型定义分散,不利于维护

(2)通过封装 axios,我们可以实现以下目标:
  • 统一配置:所有请求共享相同的 baseURL、timeout 等基础配置
  • 统一拦截:在请求拦截器中统一添加 Token,在响应拦截器中统一处理错误
  • 类型安全:通过 TypeScript 类型定义,确保请求和响应的数据类型正确
  • 代码复用:封装后只需调用封装的方法,避免重复代码

2,封装的三层架构

(1)axios 的封装可以分为三层:
  • 全局层:所有请求实例共享的拦截器和配置
  • 实例层:单个请求实例特有的拦截器和配置
  • 请求层:单个请求特有的配置和处理
(2)分层封装的好处是可以在不同层级添加不同的处理逻辑,例如全局层处理通用错误,实例层处理特定业务的 Token,请求层处理单个请求的特殊需求。

二、HttpClient 基础封装

1,准备工作

(1)首先在项目中创建 service 目录,用于存放 axios 封装相关的文件:
提示:建议将网络请求相关的代码统一放在 service 目录下,便于管理和维护。
src/
└── service/
    ├── index.ts          // 导出请求实例
    ├── request/
    │   ├── index.ts      // HttpClient 类实现
    │   ├── config.ts      // 公共配置
    │   └── type.ts        // 类型定义
    └── api/               // API 接口定义

(2)接着执行如下命令在项目中安装 axios:
npm install axios --save

2,定义类型文件

在 service/request/type.ts 文件中定义类型:
提示:通过继承 AxiosRequestConfig 接口,可以扩展自己的配置属性,如拦截器和 Loading 配置。
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
// 定义拦截器的类型,T 是响应结果(res.data)的类型
export interface HttpClientInterceptors<T = any> {
  requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: AxiosResponse<T>) => AxiosResponse<T> | Promise<AxiosResponse<T>>
  responseInterceptorCatch?: (error: any) => any
}
// 继承 AxiosRequestConfig,扩展自己的类型
export interface HttpClientConfig<T = any> extends AxiosRequestConfig {
  interceptors?: HttpClientInterceptors<T>
  showLoading?: boolean  // 是否显示 Loading
}

3,创建 HttpClient 类

在 service/request/index.ts 文件中定义 HttpClient 类。该类的代码主要做了三件事情:
  • 在构造器中创建 axios 实例,并赋值给 instance 变量
  • 编写 request 请求函数,其中 T 指定了响应结果 res.data 的数据类型
  • 在 request 函数中调用 instance.request 发起网络请求,并最终返回一个 Promise 对象
import axios from 'axios'
import type { AxiosInstance, AxiosResponse } from 'axios'
import type { HttpClientConfig } from './type'

class HttpClient {
  // 声明 axios 实例
  instance: AxiosInstance

  constructor(config: HttpClientConfig) {
    // 创建 axios 实例
    this.instance = axios.create(config)
  }

  // request 方法,T 用于指定响应结果 res.data 的类型
  request<T>(config: HttpClientConfig): Promise<AxiosResponse<T>> {
    return new Promise((resolve, reject) => {
      this.instance
        .request<T>(config)
        .then((res) => {
          // 将结果 resolve 返回出去
          resolve(res)
        })
        .catch((err) => {
          reject(err)
          return err
        })
    })
  }
}

export default HttpClient

4,创建请求实例

(1)在 service/request/config.ts 文件中编写公共配置:
// 基础 URL
const BASE_URL = 'http://httpbin.org'

// 超时时间
const TIME_OUT = 10000

export { BASE_URL, TIME_OUT }

(2)在 service/index.ts 文件中创建 HttpClient 实例并导出:
import HttpClient from './request'
import { BASE_URL, TIME_OUT } from './request/config'

// 创建 HttpClient 实例
const httpClient = new HttpClient({
  baseURL: BASE_URL,
  timeout: TIME_OUT
})

export default httpClient

6,使用请求实例

(1)在 src/main.ts 中添加如下代码验证请求功能:
提示:通过泛型 <IResponseData> 指定响应数据的类型,TypeScript 会自动推导出 res.data 的类型,提供更好的类型安全性。
import httpClient from './service'

// 定义响应 res.data 对象的类型
interface IResponseData {
  args: any
  headers: any
  origin: string
  url: string
}

// 发起网络请求
httpClient
  .request<IResponseData>({
    url: '/get',
    method: 'get'
  })
  .then((res) => {
    // TypeScript 会自动推导出 res.data 的类型为 IResponseData
    console.log(res.data)
  })

(2)运行项目,控制台能正常获取到数据说明封装成功。

三、全局拦截器封装

1,添加全局拦截器

(1)修改 HttpClient 类,在构造函数中添加全局拦截器。同时,在响应拦截器中返回 res.data 而不是 res,这样调用方可以直接获取到响应数据,无需再通过 res.data 访问。
注意:由于响应拦截器返回 res.data,request 函数的返回值类型需要改为 Promise<T>。
import axios from 'axios'
import type { AxiosInstance, AxiosResponse } from 'axios'
import type { HttpClientConfig } from './type'
  
class HttpClient {
  // 声明 axios 实例
  instance: AxiosInstance
  
  constructor(config: HttpClientConfig) {
    // 创建 axios 实例
    this.instance = axios.create(config)
 
    // 添加全局请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        console.log('全局拦截器: 请求成功拦截')
        return config
      },
      (err) => {
        console.log('全局拦截器: 请求失败拦截')
        return err
      }
    )
  
    // 添加全局响应拦截器
    this.instance.interceptors.response.use(
      (res) => {
        console.log('全局拦截器: 响应成功拦截')
        return res.data  // 返回 res.data 而不是 res
      },
      (err) => {
        console.log('全局拦截器: 响应失败拦截')
        // 根据 HTTP Error Code 显示不同的错误信息
        const status = err.response?.status
        switch (status) {
          case 401:
            // Token 过期,跳转登录
            console.log('登录已过期')
            // router.push('/login')
            break
          case 403:
            console.log('没有权限访问')
            break
          case 404:
            console.log('请求资源不存在')
            break
          case 500:
            console.log('服务器内部错误')
            break
          default:
            console.log('网络请求失败')
        }
        return err
      }
    )
  }
  
  // request 函数返回值类型改为 Promise<T>
  request<T>(config: HttpClientConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      this.instance
        .request<T, T>(config)
        .then((res) => {
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }
}
  
export default HttpClient

(2)拦截器的执行顺序如下:
  • 请求拦截器:先添加的后执行(从后往前)
  • 响应拦截器:先添加的先执行(从前往后)
提示:理解拦截器的执行顺序非常重要,特别是在同时使用全局拦截器和实例拦截器时,需要注意它们之间的顺序关系。

2,使用请求实例

(1)在 src/main.ts 中添加如下代码发起请求验证拦截器。
import httpClient from './service'
 
// 发起网络请求
httpClient
  .request({
    url: '/get',
    method: 'get'
  })
  .then((data) => {
    console.log(data)
  })

(2)启动项目,可以看到控制控制台输出如下,说明拦截器配置成功。

四、单个实例拦截器封装

1,为什么需要实例拦截器

全局拦截器适用于所有请求实例,但有时我们需要为特定的请求实例添加独特的拦截逻辑,例如:
  • 某些业务模块需要特殊的请求头处理
  • 不同模块的错误处理方式不同
  • 某些请求需要添加特定的 Token

2,实现实例拦截器

(1)修改 HttpClient 类,支持实例级别的拦截器配置:
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { HttpClientInterceptors, HttpClientConfig } from './type'
  
class HttpClient<T = any> {
  instance: AxiosInstance
  interceptors?: HttpClientInterceptors<T>
  
  constructor(config: HttpClientConfig<T>) {
    this.instance = axios.create(config)
  
    // 从 config 中取出实例的拦截器
    this.interceptors = config.interceptors
  
    // 将实例拦截器添加到 axios 实例
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )
  
    // 添加全局拦截器(所有实例共享)
    this.instance.interceptors.request.use(
      (config) => {
        console.log('全局拦截器: 请求成功拦截')
        return config
      },
      (err) => {
        console.log('全局拦截器: 请求失败拦截')
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        console.log('全局拦截器: 响应成功拦截')
        return res.data
      },
      (err) => {
        console.log('全局拦截器: 响应失败拦截')
        const status = err.response?.status
        switch (status) {
          case 401:
            // Token 过期,跳转登录
            console.log('登录已过期')
            // router.push('/login')
            break
          case 403:
            console.log('没有权限访问')
            break
          case 404:
            console.log('请求资源不存在')
            break
          case 500:
            console.log('服务器内部错误')
            break
          default:
            console.log('网络请求失败')
        }
        return err
      }
    )
  }
  
  request<T = any>(config: HttpClientConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      this.instance
        .request<T, T>(config)
        .then((res) => {
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }
}
  
export default HttpClient

(2)当同时配置了全局拦截器和实例拦截器,执行顺序如下:
  • 请求阶段:实例拦截器 → 全局拦截器
  • 响应阶段:全局拦截器 → 实例拦截器

3,为实例添加拦截器

修改 src\service\index.ts 文件,在创建实例时配置拦截器:
提示:实例拦截器在添加时会覆盖 config 中的拦截器配置,请求拦截器是后添加的先执行,响应拦截器是先添加的先执行。
import HttpClient from './request'
import { BASE_URL, TIME_OUT } from './request/config'

const httpClient = new HttpClient({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  // 为单个实例添加拦截器
  interceptors: {
    requestInterceptor: (config) => {
      const token = 'xxx'
      if (token) {
        // 统一为 header 添加 Authorization 属性
        config.headers!.Authorization = `Bearer ${token}`
      }
      console.log('实例拦截器: 请求成功拦截')
      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('实例拦截器: 请求失败拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('实例拦截器: 响应成功拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('实例拦截器: 响应失败拦截')
      return err
    }
  }
})

export default httpClient

4,测试拦截器

(1)在 src/main.ts 中添加如下代码发起请求验证拦截器。
import httpClient from './service'
 
// 发起网络请求
httpClient
  .request({
    url: '/get',
    method: 'get'
  })
  .then((data) => {
    console.log(data)
  })

(2)启动项目,可以看到控制台输出如下:

五、GET/POST 等请求方法封装

1,封装 GET/POST 等方法

为 HttpClient 类添加 get、post、delete、patch 方法:
提示:每个请求方法最终调用的都是 request 方法,只是自动设置了对应的 HTTP 方法名,简化了调用方式。
class HttpClient<T = any> {
  // ... 其他代码

  // GET 请求
  get<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  // POST 请求
  post<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  // DELETE 请求
  delete<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  // PATCH 请求
  patch<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}

export default HttpClient

2,使用封装的方法

(1)在 src/main.ts 中添加如下代码使用封装后的请求方法。
// GET 请求
httpClient.get<IResponseData>({
  url: '/get'
}).then((data) => {
  console.log(data)  // data 类型为 IResponseData
})

// POST 请求
httpClient.post<IResult>({
  url: '/post',
  data: { name: 'test' }
}).then((data) => {
  console.log(data)  // data 类型为 IResult
})
(2)启动项目,可以看到控制台输出如下:

六、Loading 加载效果

1,添加 Loading 功能

修改 HttpClient 类,添加 Loading 功能。这里我们使用 Element Plus 的 Loading 组件,在请求过程中显示加载效果。
注意:Loading 是单例模式,多次调用也不会创建多个,需要在响应后手动关闭。
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { HttpClientInterceptors, HttpClientConfig } from './type'
// 导入 Element Plus Loading
import 'element-plus/es/components/loading/style/css'
import { ElLoading } from 'element-plus'
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
  
const DEFAULT_LOADING = true
   
class HttpClient<T = any> {
  instance: AxiosInstance
  interceptors?: HttpClientInterceptors<T>
  showLoading?: boolean  // 是否显示 Loading
  loading?: LoadingInstance  // Loading 实例
   
  constructor(config: HttpClientConfig<T>) {
    this.instance = axios.create(config)
    this.showLoading = config.showLoading ?? DEFAULT_LOADING
   
    // 从 config 中取出实例的拦截器
    this.interceptors = config.interceptors
   
    // 将实例拦截器添加到 axios 实例
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )
   
    // 添加全局拦截器(所有实例共享)
    this.instance.interceptors.request.use(
      (config) => {
        console.log('全局拦截器: 请求成功拦截')
        // 请求拦截器中显示 Loading
        if (this.showLoading) {
          this.loading = ElLoading.service({
            lock: true,
            text: 'Loading...',
            background: 'rgba(0, 0, 0, 0.2)',
            fullscreen: true
          })
        }
        return config
      },
      (err) => {
        console.log('全局拦截器: 请求失败拦截')
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        console.log('全局拦截器: 响应成功拦截')
        // 响应拦截器中关闭 Loading
        this.loading?.close()
        return res.data
      },
      (err) => {
        console.log('全局拦截器: 响应失败拦截')
        // 响应拦截器中关闭 Loading
        this.loading?.close()
        const status = err.response?.status
        switch (status) {
          case 401:
            // Token 过期,跳转登录
            console.log('登录已过期')
            // router.push('/login')
            break
          case 403:
            console.log('没有权限访问')
            break
          case 404:
            console.log('请求资源不存在')
            break
          case 500:
            console.log('服务器内部错误')
            break
          default:
            console.log('网络请求失败')
        }
        return err
      }
    )
  }
   
  request<T = any>(config: HttpClientConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      // 判断单个请求是否需要显示 Loading
      if (config.showLoading === false) {
        this.showLoading = config.showLoading
      }
 
      this.instance
        .request<T, T>(config)
        .then((res) => {
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
        .finally(() => {
          // 恢复默认值,不影响下一个请求
          this.showLoading = DEFAULT_LOADING
        })
    })
  }
 
    // GET 请求
  get<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }
  
  // POST 请求
  post<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }
  
  // DELETE 请求
  delete<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }
  
  // PATCH 请求
  patch<T = any>(config: HttpClientConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}
   
export default HttpClient

2,设置默认显示 Loading

(1)修改 src\service\index.ts 文件,创建实例时默认显示 Loading。
import HttpClient from './request'
import { BASE_URL, TIME_OUT } from './request/config'
 
const httpClient = new HttpClient({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  showLoading: true,  // 默认显示 Loading
  // 为单个实例添加拦截器
  interceptors: {
    requestInterceptor: (config) => {
      const token = 'xxx'
      if (token) {
        // 统一为 header 添加 Authorization 属性
        config.headers!.Authorization = `Bearer ${token}`
      }
      console.log('实例拦截器: 请求成功拦截')
      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('实例拦截器: 请求失败拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('实例拦截器: 响应成功拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('实例拦截器: 响应失败拦截')
      return err
    }
  }
})
 
export default httpClient

(2)这样使用该实例的请求都会自动显示 Loading 效果。

3,单独控制 Loading 显示

我们可以在单个请求中控制 Loading 的显示,例如个别不需要显示 Loading 的请求可以通过如下方式关闭。
// 单个请求关闭 Loading
httpClient.get({
  url: '/get',
  showLoading: false  // 该请求不显示 Loading
})

附:完整的项目文件组织结构与代码

1,文件组织建议

    下面文件结构适配中大型 Vue 前端项目,遵循分层解耦、业务模块化、类型统一管理原则,区分核心请求底层、业务 API、全局类型,结构清晰且易于维护、扩展和多人协作。
  • service/request:底层请求基础封装,全局唯一,业务零侵入,所有接口统一复用该底层能力
  • service/api:纯业务接口层,按业务模块拆分,只负责定义接口地址、请求方式、参数,不做数据处理、页面逻辑
  • service/index.ts:统一导出入口,简化页面引入方式,无需逐个引入业务 API 文件
  • types:统一管理所有 TS 类型,区分通用 API 类型和业务专属类型,实现类型复用、全局统一约束
src/
├── service/                # 全局请求服务层(统一处理接口请求、拦截器、配置)
│   ├── index.ts            # 统一导出请求实例、所有业务API,供全局引入
│   ├── request/            # 底层请求核心封装(基础能力,不写业务代码)
│   │   ├── index.ts       # HttpClient 请求核心类(封装get/post/put/delete、拦截器、错误处理)
│   │   ├── config.ts      # 请求公共配置(接口基础地址、超时时间、请求头、白名单等)
│   │   └── type.ts        # 底层请求通用类型(请求参数、响应通用结构体、拦截器类型)
│   └── api/                # 业务模块API层(按业务拆分,纯接口请求,无业务逻辑)
│       ├── user.ts        # 用户模块相关所有接口
│       ├── product.ts     # 商品模块相关所有接口
│       ├── order.ts       # 订单模块相关所有接口
│       └── index.ts       # 统一聚合导出所有业务API
├── types/                  # 全局通用类型声明文件夹
│   ├── api.d.ts           # API 全局通用响应类型、基础泛型定义
│   ├── user.d.ts          # 用户业务专属类型
│   └── product.d.ts       # 商品业务专属类型
└── utils/                  # 公共工具方法(可选,配合请求使用)
    └── http-error.ts      # 请求错误统一处理、提示封装

2,底层请求通用类型

(1)底层请求配置(service/request/config.ts)
// 基础 URL
const BASE_URL = process.env.VUE_APP_API_BASE_URL || '/api'
// 超时时间
const TIME_OUT = 10000
// 无需token的接口白名单
const NO_TOKEN_WHITE_LIST = ['/user/login', '/user/register']
// 导出配置
export { BASE_URL, TIME_OUT, NO_TOKEN_WHITE_LIST }

(2)底层请求通用类型(service/request/type.ts)
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
// 定义拦截器的类型,T 是响应结果(res.data)的类型
export interface HttpClientInterceptors<T = any> {
  requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: AxiosResponse<T>) => AxiosResponse<T> | Promise<AxiosResponse<T>>
  responseInterceptorCatch?: (error: any) => any
}
// 继承 AxiosRequestConfig,扩展自己的类型
export interface HttpClientConfig<T = any> extends AxiosRequestConfig {
  interceptors?: HttpClientInterceptors<T>
  showLoading?: boolean  // 是否显示 Loading
}

// 后端统一响应通用结构体
export interface HttpResponse<T = any> {
  code: number
  data: T
  message: string
  success: boolean
}

(3)核心请求类封装(service/request/index.ts)
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { HttpClientInterceptors, HttpClientConfig, HttpResponse } from './type'
// 导入 Element Plus Loading
import 'element-plus/es/components/loading/style/css'
import { ElLoading } from 'element-plus'
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
  
const DEFAULT_LOADING = true
   
class HttpClient<T = any> {
  instance: AxiosInstance
  interceptors?: HttpClientInterceptors<T>
  showLoading?: boolean  // 是否显示 Loading
  loading?: LoadingInstance  // Loading 实例
   
  constructor(config: HttpClientConfig<T>) {
    this.instance = axios.create(config)
    this.showLoading = config.showLoading ?? DEFAULT_LOADING
   
    // 从 config 中取出实例的拦截器
    this.interceptors = config.interceptors
   
    // 将实例拦截器添加到 axios 实例
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )
   
    // 添加全局拦截器(所有实例共享)
    this.instance.interceptors.request.use(
      (config) => {
        console.log('全局拦截器: 请求成功拦截')
        // 请求拦截器中显示 Loading
        if (this.showLoading) {
          this.loading = ElLoading.service({
            lock: true,
            text: 'Loading...',
            background: 'rgba(0, 0, 0, 0.2)',
            fullscreen: true
          })
        }
        return config
      },
      (err) => {
        console.log('全局拦截器: 请求失败拦截')
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        console.log('全局拦截器: 响应成功拦截')
        // 响应拦截器中关闭 Loading
        this.loading?.close()
        return res.data
      },
      (err) => {
        console.log('全局拦截器: 响应失败拦截')
        // 响应拦截器中关闭 Loading
        this.loading?.close()
        const status = err.response?.status
        switch (status) {
          case 401:
            // Token 过期,跳转登录
            console.log('登录已过期')
            // router.push('/login')
            break
          case 403:
            console.log('没有权限访问')
            break
          case 404:
            console.log('请求资源不存在')
            break
          case 500:
            console.log('服务器内部错误')
            break
          default:
            console.log('网络请求失败')
        }
        // 统一捕获 400/401/403/500 错误,不抛 runtime 异常
        return Promise.resolve({
          code: err.response?.status || 500,
          data: null,
          message: err.message || '请求参数错误',
          success: false
        })
      }
    )
  }
   
  request<T = any>(config: HttpClientConfig): Promise<HttpResponse<T>> {
    return new Promise((resolve, reject) => {
      // 判断单个请求是否需要显示 Loading
      if (config.showLoading === false) {
        this.showLoading = config.showLoading
      }
 
      this.instance
        .request<T, HttpResponse<T>>(config)
        .then((res) => {
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
        .finally(() => {
          // 恢复默认值,不影响下一个请求
          this.showLoading = DEFAULT_LOADING
        })
    })
  }
 
  // GET 请求
  get<T = any>(config: HttpClientConfig<T>): Promise<HttpResponse<T>> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  // POST 请求
  post<T = any>(config: HttpClientConfig<T>): Promise<HttpResponse<T>> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  // DELETE 请求
  delete<T = any>(config: HttpClientConfig<T>): Promise<HttpResponse<T>> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  // PATCH 请求
  patch<T = any>(config: HttpClientConfig<T>): Promise<HttpResponse<T>> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}
   
export default HttpClient

(4)统一导出请求实例、所有业务 API,供全局引入(service/index.ts)
import HttpClient from './request'
import { BASE_URL, TIME_OUT, NO_TOKEN_WHITE_LIST } from './request/config'
 
const httpClient = new HttpClient({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  showLoading: true,  // 默认显示 Loading
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  },
  // 为单个实例添加拦截器
  interceptors: {
    requestInterceptor: (config) => {
      console.log('实例拦截器: 请求成功拦截')
      // 白名单接口不携带token
      if (!NO_TOKEN_WHITE_LIST.includes(config.url || '')) {
        const token = localStorage.getItem('token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
      }
      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('实例拦截器: 请求失败拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('实例拦截器: 响应成功拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('实例拦截器: 响应失败拦截')
      // 统一错误处理
      return Promise.reject(err)
    }
  }
})
 
export default httpClient

// 统一导出所有业务API
export * from './api'

3,将 API 接口按业务模块拆分

(1)用户模块完整 API(service/api/user.ts)
import httpClient from '../index'
 
// 用户信息基础类型
export interface IUserInfo {
  id: number
  name: string
  email: string
}
 
// 获取用户信息
export function getUserInfo(id: number) {
  return httpClient.get<IUserInfo>({
    url: `/user/${id}`
  })
}

// 更新用户信息
export function updateUserInfo(data: IUserInfo) {
  return httpClient.post<IUserInfo>({
    url: '/user/update',
    data
  })
}

(2)商品模块完整 API(service/api/product.ts)
import httpClient from '../index'

// 商品基础信息类型
export interface IProductInfo {
  id: number
  title: string
  price: number
  stock: number
  categoryId: number
  status: 0 | 1 // 0-下架 1-上架
}

// 商品列表查询参数
export interface IProductListParams {
  page: number
  pageSize: number
  categoryId?: number
  keyword?: string
}

// 商品列表响应类型
export interface IProductListRes {
  list: IProductInfo[]
  total: number
  page: number
  pageSize: number
}

// 获取商品详情
export function getProductDetail(id: number) {
  return httpClient.get<IProductInfo>({
    url: `/product/${id}`
  })
}

// 获取商品分页列表
export function getProductList(params: IProductListParams) {
  return httpClient.get<IProductListRes>({
    url: '/product/list',
    params
  })
}

// 新增商品
export function addProduct(data: Omit<IProductInfo, 'id'>) {
  return httpClient.post<IProductInfo>({
    url: '/product/add',
    data
  })
}

// 编辑商品
export function updateProduct(data: IProductInfo) {
  return httpClient.post<IProductInfo>({
    url: '/product/update',
    data
  })
}

(3)业务 API 聚合导出(service/api/index.ts)
export * from './user'
export * from './product'

4,使用示例

(1)这里我使用 mock 工具模拟两个后端接口的返回数据。首先是获取用户信息接口 /user/{id},其返回数据如下:
{
  "code": 200,
  "data": {
    "id": 1001,
    "name": "测试管理员",
    "email": "admin@test.com",
    "phone": "13800138000",
    "avatar": "https://xxx.com/avatar.png",
    "createTime": "2026-01-01 10:00:00",
    "status": 1
  },
  "message": "查询成功",
  "success": true
}

(2)接着是获取商品分页列表接口 /product/list,其返回数据如下:
{
  "code": 200,
  "data": {
    "list": [
      {
        "id": 10001,
        "title": "高清无线蓝牙耳机",
        "price": 199.9,
        "stock": 999,
        "cover": "https://xxx.com/product1.png",
        "categoryId": 10,
        "status": 1
      },
      {
        "id": 10002,
        "title": "超薄移动电源20000mAh",
        "price": 89.9,
        "stock": 520,
        "cover": "https://xxx.com/product2.png",
        "categoryId": 11,
        "status": 1
      }
    ],
    "total": 56,
    "page": 1,
    "pageSize": 10
  },
  "message": "查询成功",
  "success": true
}

(3)最后我们在页面中引入相应的 API 进行调用即可。
import { getUserInfo, getProductList } from '@/service'

// 调用用户接口
const fetchUser = async () => {
  const res = await getUserInfo(1001)
  if (res.success) {
    console.log('用户信息', res.data)
  }
}

// 调用商品接口
const fetchProductList = async () => {
  const res = await getProductList({ page: 1, pageSize: 10 })
  if (res.success) {
    console.log('商品列表', res.data.list)
  }
}

(4)启动项目发起请求,可以看到控制台输出内容如下:
评论

全部评论(0)

回到顶部