欢迎光临
我们一直在努力

VUE3 中的 Axios 二次封装与请求策略

VUE3 中的 Axios 二次封装与请求策略

  • 前言
  • 为什么要封装 Axios?
    • 没有封装的代码长什么样?
    • 封装之后的代码长什么样?
    • 封装的核心价值
  • 从零开始构建我们的请求层
    • 第一层:基础配置
    • 第二层:拦截器
    • 第三层:Loading 状态自动化
  • 实战技巧 – 解决常见痛点
    • 场景1:请求取消,告别重复请求
    • 场景2:自动重试,提升用户体验
    • 场景3:数据缓存,减少不必要的请求
  • TypeScript 加持 – 让代码更可靠
    • 自定义类型系统
    • 创建类型安全的API模块
    • 在组件中使用
  • 封装的度 – 如何把握封装分寸?
    • 封装层次图
    • 封装原则
      • 原则一:够用即可
      • 原则二:可配置性
      • 原则三:渐进增强
    • 封装的检查清单
    • 完整目录结构
    • 最终建议
  • 结语

前言

在现代前端应用中,网络请求是不可或缺的一部分。Axios 作为最流行的 HTTP 客户端,以其简洁的 API 和强大的功能赢得了开发者的青睐。然而,直接在每个组件中使用 Axios 会导致大量的代码冗余、错误处理混乱、难以维护等问题。

因此,我们需要对 Axios 进行二次封装,其核心价值在于:统一处理、集中配置、复用逻辑,把复杂的事情变得简单,把重复的事情变得自动化。。

本文将从零开始,深入探讨如何构建一个健壮、易用、类型安全的请求层,涵盖拦截器、请求取消、重试机制、缓存策略等高级特性。

为什么要封装 Axios?

没有封装的代码长什么样?

在本文开篇之前,我们先来看一个没有封装的 Axios 请求是什么样的:

// 用户页面
async function getUser() {
try {
const res = await axios.get('http://localhost:3000/api/users', {
headers: { token: localStorage.getItem('token') },
timeout: 10000
})
user.value = res.data
} catch (err) {
if (err.response?.status === 401) {
router.push('/login')
}
console.error('获取用户失败', err)
}
}

// 商品页面
async function getProduct() {
try {
const res = await axios.get('http://localhost:3000/api/products', {
headers: { token: localStorage.getItem('token') },
timeout: 10000
})
product.value = res.data
} catch (err) {
if (err.response?.status === 401) {
router.push('/login')
}
console.error('获取商品失败', err)
}
}

这段代码有哪些问题呢?

  • 每个请求都需要重复配置 headers、timeout 等重复配置项
  • 每个请求都要重复获取和处理 token :localStorage.getItem('token')
  • 每个请求都要写 try/catch 等错误处理
  • 当需要修改请求配置时,与之相关的所有文件都要修改

封装之后的代码长什么样?

二次封装后,所有的重复配置都只需要写一次:

// request.js
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})

// 使用
import request from './request'
request.get('/users')
request.get('/products')

封装的核心价值

  • 统一配置:一次配置,到处使用
  • 统一处理:token 自动添加、错误统一处理
  • 复用逻辑:loading状态、重试机制等都可复用
  • 易于维护 :修改一处,生效全局

从零开始构建我们的请求层

第一层:基础配置

创建一个 request.js 文件,这是所有请求的基础:

// request.js
import axios from 'axios'

// 1. 创建axios实例
const request = axios.create({
// 基础URL – 通过环境变量区分开发/生产
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',

// 超时时间 – 10秒后自动断开
timeout: 10000,

// 请求头 – 默认配置
headers: {
'Content-Type': 'application/json'
}
})

export default request

第二层:拦截器

拦截器就像机场的安检通道,每个请求和其响应都要经过检查:请求拦截器/响应拦截器:

// request.js
import { useUserStore } from '@/stores/user'

// 请求拦截器 – 请求发出前的处理
request.interceptors.request.use(
(config) => {
// 1. 获取用户token
const userStore = useUserStore()

// 2. 如果用户已登录,自动添加token
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}

// 3. GET请求添加时间戳,防止浏览器缓存
if (config.method === 'get') {
config.params = {
config.params,
_t: Date.now()
}
}

return config
},
(error) => {
// 请求配置出错时的处理
return Promise.reject(error)
}
)

// 响应拦截器 – 收到响应后的处理
request.interceptors.response.use(
(response) => {
// 直接返回数据部分,简化使用
return response.data
},
(error) => {
// 统一的错误处理
if (error.response) {
// 服务器返回了错误状态码
switch (error.response.status) {
case 401: // 未授权
const userStore = useUserStore()
userStore.logout() // 清除用户信息
router.push('/login') // 跳转到登录页
break
case 403: // 禁止访问
ElMessage.error('没有权限执行此操作')
break
case 404: // 资源不存在
ElMessage.error('请求的资源不存在')
break
case 500: // 服务器错误
ElMessage.error('服务器开小差了,请稍后再试')
break
default:
ElMessage.error(error.response.data?.message || '请求失败')
}
} else if (error.request) {
// 请求发出去了,但没有收到响应
ElMessage.error('网络连接失败,请检查网络设置')
} else {
// 请求配置出错
ElMessage.error('请求配置错误')
}

return Promise.reject(error)
}
)

第三层:Loading 状态自动化

当我们在发送请求时,手动控制 loading 状态会很麻烦,可以让拦截器帮我们自动处理:

// stores/loading.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useLoadingStore = defineStore('loading', () => {
const count = ref(0) // 当前正在进行的请求数
const isLoading = computed(() => count.value > 0) // 是否显示loading

function add() {
count.value++
}

function remove() {
if (count.value > 0) {
count.value
}
}

return { isLoading, add, remove }
})

在 request.js中,我们就可以使用上述 lodaing :

// request.js – 修改拦截器
import { useLoadingStore } from '@/stores/loading'

request.interceptors.request.use((config) => {
// 如果不是手动禁用了loading
if (!config.headers?.disableLoading) {
const loadingStore = useLoadingStore()
loadingStore.add()
}
return config
})

request.interceptors.response.use(
(response) => {
if (!response.config.headers?.disableLoading) {
const loadingStore = useLoadingStore()
loadingStore.remove()
}
return response
},
(error) => {
if (!error.config?.headers?.disableLoading) {
const loadingStore = useLoadingStore()
loadingStore.remove()
}
return Promise.reject(error)
}
)

在组件中使用:

<template>
<div>
<!– 自动显示/隐藏loading –>
<div v-if="loadingStore.isLoading" class="loading">加载中…</div>
<div v-else>
<!– 页面内容 –>
</div>
</div>
</template>

<script setup>
import { useLoadingStore } from '@/stores/loading'

const loadingStore = useLoadingStore()

// 发起请求会自动显示loading
async function fetchData() {
await request.get('/users')
}
</script>

实战技巧 – 解决常见痛点

场景1:请求取消,告别重复请求

当用户在使用搜索功能时,首先在搜索框输入"手机"发送搜索请求,此时请求还没返回;又将输入变成了"手机号",重新发送一次请求。此时应该取消第一个请求,只保留最新的一次请求:

// utils/CancelRequest.js
class CancelRequest {
constructor() {
// 存储所有pending状态的请求
this.pendingMap = new Map()
}

// 生成请求的唯一标识
getRequestKey(config) {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

// 添加请求到pending列表
addPending(config) {
const key = this.getRequestKey(config)

// 如果已有相同的请求,取消它
if (this.pendingMap.has(key)) {
const abort = this.pendingMap.get(key)
abort() // 取消请求
this.pendingMap.delete(key)
}

// 创建新的AbortController
const controller = new AbortController()
config.signal = controller.signal

// 保存取消函数
this.pendingMap.set(key, () => controller.abort())
}

// 请求完成后,从pending列表移除
removePending(config) {
const key = this.getRequestKey(config)
if (this.pendingMap.has(key)) {
this.pendingMap.delete(key)
}
}

// 取消所有pending请求(这在页面切换时很有用)
cancelAll() {
this.pendingMap.forEach(cancel => cancel())
this.pendingMap.clear()
}
}

export const cancelRequest = new CancelRequest()

在拦截器中使用:

// request.js
import { cancelRequest } from './utils/CancelRequest'

request.interceptors.request.use((config) => {
// 如果没有禁用取消功能
if (!config.headers?.disableCancel) {
cancelRequest.addPending(config)
}
return config
})

request.interceptors.response.use(
(response) => {
cancelRequest.removePending(response.config)
return response
},
(error) => {
// 如果是手动取消的请求,不抛出错误
if (axios.isCancel(error)) {
console.log('请求已取消')
return new Promise(() => {}) // 返回pending的Promise
}

if (error.config) {
cancelRequest.removePending(error.config)
}
return Promise.reject(error)
}
)

// 路由切换时,取消所有请求
router.beforeEach((to, from, next) => {
cancelRequest.cancelAll()
next()
})

场景2:自动重试,提升用户体验

当网络不稳定时,我们需要自动重试功能,让用户无感知地完成操作,而不是简单地返回一句“网络异常,请稍后重试”:

// utils/retry.js
/**
* 带重试功能的请求
* @param {Function} requestFn 请求函数
* @param {Object} options 配置选项
*/

export async function retryRequest(requestFn, options = {}) {
const {
retries = 3, // 最大重试次数
delay = 1000, // 初始延迟(毫秒)
factor = 2, // 延迟增长倍数
maxDelay = 30000, // 最大延迟
retryCondition = (error) => {
// 默认重试条件:网络错误 或 5xx服务器错误
return !error.response || error.response.status >= 500
}
} = options

let attempt = 0

while (attempt <= retries) {
try {
return await requestFn()
} catch (error) {
attempt++

// 最后一次尝试失败,抛出错误
if (attempt > retries) {
throw error
}

// 检查是否应该重试
if (!retryCondition(error)) {
throw error
}

// 计算等待时间(指数退避)
const waitTime = Math.min(delay * Math.pow(factor, attempt 1), maxDelay)

console.log(`请求失败,${waitTime}ms后第${attempt}次重试…`)

// 等待后继续循环
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}

// 使用示例
async function fetchImportantData() {
return retryRequest(
() => request.get('/important-data'),
{
retries: 5,
delay: 2000,
onRetry: (attempt, error) => {
// 可以在这里记录日志或通知用户
console.log(`${attempt}次重试`, error)
}
}
)
}

场景3:数据缓存,减少不必要的请求

当用户频繁查看某个商品详情时,每次都要发送一次请求,这样既浪费资源,又慢,因此我们可以将数据缓存起来:

// utils/cache.js
class RequestCache {
constructor() {
this.cache = new Map()
}

/**
* 设置缓存
* @param {string} key 缓存键
* @param {any} data 缓存数据
* @param {number} ttl 过期时间(毫秒)
*/

set(key, data, ttl = 60000) {
this.cache.set(key, {
data,
expire: Date.now() + ttl
})
}

/**
* 获取缓存
* @param {string} key 缓存键
*/

get(key) {
const item = this.cache.get(key)

// 没有缓存
if (!item) return null

// 检查是否过期
if (Date.now() > item.expire) {
this.cache.delete(key)
return null
}

return item.data
}

// 清除特定缓存
delete(key) {
this.cache.delete(key)
}

// 清除所有缓存
clear() {
this.cache.clear()
}
}

export const requestCache = new RequestCache()

// 封装带缓存的请求
async function requestWithCache(url, options = {}) {
const { cacheTTL = 60000, restOptions } = options

// 只有GET请求才使用缓存
if (restOptions.method && restOptions.method !== 'GET') {
return request(url, restOptions)
}

// 生成缓存键
const cacheKey = `${url}:${JSON.stringify(restOptions.params)}`

// 检查缓存
const cached = requestCache.get(cacheKey)
if (cached) {
console.log('使用缓存数据:', cacheKey)
return cached
}

// 发起真实请求
const data = await request(url, restOptions)

// 存入缓存
requestCache.set(cacheKey, data, cacheTTL)

return data
}

TypeScript 加持 – 让代码更可靠

自定义类型系统

// types/api.d.ts
// 通用响应格式
export interface ApiResponse<T = any> {
code: number // 业务状态码
message: string // 提示信息
data: T // 实际数据
timestamp?: number // 时间戳
}

// 分页参数
export interface PaginationParams {
page: number // 当前页码
pageSize: number // 每页条数
sort?: string // 排序字段
order?: 'asc' | 'desc' // 排序方式
}

// 分页结果
export interface PaginatedResult<T> {
list: T[] // 数据列表
total: number // 总条数
page: number // 当前页码
pageSize: number // 每页条数
totalPages: number // 总页数
}

// 扩展的请求配置
export interface RequestConfig {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
data?: any
params?: any
headers?: Record<string, string>

// 自定义选项
disableLoading?: boolean // 是否禁用loading
disableCancel?: boolean // 是否禁用自动取消
cacheTTL?: number // 缓存时间(毫秒)
retries?: number // 重试次数
}

创建类型安全的API模块

// api/user.ts
import request from '@/request'
import type { PaginationParams, PaginatedResult } from '@/types/api'

// 用户类型定义
export interface User {
id: number
name: string
email: string
avatar: string
role: 'admin' | 'user'
status: 'active' | 'inactive'
createdAt: string
updatedAt: string
}

export interface CreateUserDto {
name: string
email: string
password: string
role?: 'admin' | 'user'
}

export interface UpdateUserDto extends Partial<CreateUserDto> {
status?: 'active' | 'inactive'
}

export interface UserListParams extends PaginationParams {
keyword?: string
role?: string
status?: string
}

// 用户API模块
export const userApi = {
// 获取用户列表
getList: (params: UserListParams) =>
request.get<PaginatedResult<User>>('/users', { params }),

// 获取单个用户
getDetail: (id: number) =>
request.get<User>(`/users/${id}`),

// 创建用户
create: (data: CreateUserDto) =>
request.post<User>('/users', data),

// 更新用户
update: (id: number, data: UpdateUserDto) =>
request.put<User>(`/users/${id}`, data),

// 删除用户
delete: (id: number) =>
request.delete(`/users/${id}`),

// 修改状态
updateStatus: (id: number, status: User['status']) =>
request.patch(`/users/${id}/status`, { status })
}

在组件中使用

<script setup lang="ts">
import { ref } from 'vue'
import { userApi } from '@/api/user'
import type { User, UserListParams } from '@/api/user'

const users = ref<User[]>([])
const loading = ref(false)

const params = ref<UserListParams>({
page: 1,
pageSize: 10,
keyword: ''
})

async function loadUsers() {
loading.value = true
try {
const result = await userApi.getList(params.value)
users.value = result.list
} finally {
loading.value = false
}
}

// 完全的类型提示和自动补全!
async function handleCreate() {
const newUser = await userApi.create({
name: '张三',
email: 'zhangsan@example.com',
password: '123456',
role: 'user'
})
users.value.push(newUser)
}
</script>

封装的度 – 如何把握封装分寸?

封装层次图

#mermaid-svg-7aKPWqgk5b16TZ9a{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7aKPWqgk5b16TZ9a .error-icon{fill:#552222;}#mermaid-svg-7aKPWqgk5b16TZ9a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7aKPWqgk5b16TZ9a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7aKPWqgk5b16TZ9a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7aKPWqgk5b16TZ9a .marker.cross{stroke:#333333;}#mermaid-svg-7aKPWqgk5b16TZ9a svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7aKPWqgk5b16TZ9a p{margin:0;}#mermaid-svg-7aKPWqgk5b16TZ9a .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a .cluster-label text{fill:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a .cluster-label span{color:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a .cluster-label span p{background-color:transparent;}#mermaid-svg-7aKPWqgk5b16TZ9a .label text,#mermaid-svg-7aKPWqgk5b16TZ9a span{fill:#333;color:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a .node rect,#mermaid-svg-7aKPWqgk5b16TZ9a .node circle,#mermaid-svg-7aKPWqgk5b16TZ9a .node ellipse,#mermaid-svg-7aKPWqgk5b16TZ9a .node polygon,#mermaid-svg-7aKPWqgk5b16TZ9a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7aKPWqgk5b16TZ9a .rough-node .label text,#mermaid-svg-7aKPWqgk5b16TZ9a .node .label text,#mermaid-svg-7aKPWqgk5b16TZ9a .image-shape .label,#mermaid-svg-7aKPWqgk5b16TZ9a .icon-shape .label{text-anchor:middle;}#mermaid-svg-7aKPWqgk5b16TZ9a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7aKPWqgk5b16TZ9a .rough-node .label,#mermaid-svg-7aKPWqgk5b16TZ9a .node .label,#mermaid-svg-7aKPWqgk5b16TZ9a .image-shape .label,#mermaid-svg-7aKPWqgk5b16TZ9a .icon-shape .label{text-align:center;}#mermaid-svg-7aKPWqgk5b16TZ9a .node.clickable{cursor:pointer;}#mermaid-svg-7aKPWqgk5b16TZ9a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7aKPWqgk5b16TZ9a .arrowheadPath{fill:#333333;}#mermaid-svg-7aKPWqgk5b16TZ9a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7aKPWqgk5b16TZ9a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7aKPWqgk5b16TZ9a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7aKPWqgk5b16TZ9a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7aKPWqgk5b16TZ9a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7aKPWqgk5b16TZ9a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7aKPWqgk5b16TZ9a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7aKPWqgk5b16TZ9a .cluster text{fill:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a .cluster span{color:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7aKPWqgk5b16TZ9a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7aKPWqgk5b16TZ9a rect.text{fill:none;stroke-width:0;}#mermaid-svg-7aKPWqgk5b16TZ9a .icon-shape,#mermaid-svg-7aKPWqgk5b16TZ9a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7aKPWqgk5b16TZ9a .icon-shape p,#mermaid-svg-7aKPWqgk5b16TZ9a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7aKPWqgk5b16TZ9a .icon-shape rect,#mermaid-svg-7aKPWqgk5b16TZ9a .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7aKPWqgk5b16TZ9a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7aKPWqgk5b16TZ9a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7aKPWqgk5b16TZ9a :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

基础层

API层

业务层

业务组件

API模块

请求实例

拦截器

工具函数

封装原则

原则一:够用即可

不要过度设计,根据项目规模选择合适的封装程度:

// ✅ 小型项目:简单封装就够了
const request = axios.create({ baseURL: '/api' })

// ✅ 中型项目:添加拦截器、类型定义
request.interceptors.response.use(/* 错误处理 */)

// ✅ 大型项目:完整的缓存、重试、监控机制

原则二:可配置性

提供出口,让特殊场景可以绕过封装:

// 通过配置项控制
await request.get('/important-data', {
headers: {
disableLoading: true, // 不显示loading
disableCancel: true, // 不自动取消
disableRetry: true // 不重试
}
})

原则三:渐进增强

从简单开始,逐步完善:

// 第一阶段:基础封装
export const api = {
getUser: () => request.get('/user')
}

// 第二阶段:添加类型
export const api = {
getUser: (): Promise<User> => request.get('/user')
}

// 第三阶段:添加高级特性
export const api = {
getUser: () => retryRequest(
() => requestWithCache('/user'),
{ retries: 3 }
)
}

封装的检查清单

检查项是否必需说明
基础配置 baseURL、超时、请求头
错误处理 统一错误提示、状态码处理
Token管理 自动附加、过期处理
Loading状态 推荐 提升用户体验
TypeScript 推荐 类型安全、开发体验
请求取消 看场景 搜索、标签切换等
数据缓存 看场景 频繁访问的静态数据
自动重试 看场景 网络不稳定时

完整目录结构

src/
├── api/
│ ├── index.ts # API统一出口
│ ├── user.ts # 用户模块
│ ├── product.ts # 商品模块
│ └── order.ts # 订单模块
├── utils/
│ ├── request.ts # 请求核心
│ ├── cache.ts # 缓存工具
│ ├── retry.ts # 重试工具
│ └── cancel.ts # 取消工具
├── types/
│ └── api.d.ts # 类型定义
└── stores/
└── loading.ts # loading状态

最终建议

Axios 封装没有标准答案,关键在于根据项目规模和团队习惯找到平衡点:

  • 小型项目:简单的拦截器 + 类型定义就够了
  • 中型项目:需要请求取消、错误统一处理
  • 大型项目:完整的缓存、重试、监控机制

结语

封装不是为了炫技,而是为了让代码更简单,让开发更高效。一个好的封装应该让 90% 的场景变得简单,同时给 10% 的特殊场景留出出口。希望这篇文章能帮助我们构建适合自己的请求层。记住,最好的封装是让使用它的人感受不到封装的存在。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

赞(0)
未经允许不得转载:171主机测评 » VUE3 中的 Axios 二次封装与请求策略
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址