Vue3 网络请求优雅封装:从分层架构设计到 Mock 服务落地

企业级项目必备的网络请求基础方案,为后续封装铺路
在 Vue3 企业级项目开发中,网络请求层的设计是基础且核心的环节,一个高内聚、低耦合、可复用的网络请求架构,能大幅提升项目的开发效率和可维护性。我原本计划将网络请求、国际化这类通用能力,抽离为基于 pnpm 的 monorepo 多包微前端工程中的共享库(ts lib),让基座和子应用能统一复用,但如果将通用模板作为单体工程开发,把网络请求直接集成其中也完全适用。
接下来我会以系列文章的形式,把 Vue3 网络请求的优雅封装讲透彻,涵盖架构设计、Mock 服务搭建、Axios 与拦截器封装、取消请求 / 请求防重、请求重试以及 useRequest 封装等核心内容。本文作为系列第一篇,先从整体的分层架构设计入手,同时完成 Mock 服务的搭建,为后续的功能实现打好基础。
技术选型:Axios 还是 Alova?
做网络请求封装,首先绕不开请求库的选择。对于 Vue/React 开发者来说,Axios 无疑是最熟悉的请求库,生态成熟、使用简单,也是企业级项目中的主流选择。
这里顺带提及一下 Alova,这款请求库功能十分强大,在状态管理、请求缓存等方面有显著优势,其适配器设计等思想也非常值得学习,推荐大家有空深入研究。但目前我仅在个人实践中尝试过 Alova,尚未在实际项目中落地,因此本系列还是基于经典的 Axios 展开封装,保证方案的实用性和落地性。
前期准备:标准化目录与分层设计
在开始具体编码前,先搭建标准化的目录结构,这是实现分层架构的基础,也能让后续的代码组织更清晰。本次的目录结构从项目根路径开始设计,核心遵循通用逻辑与业务逻辑解耦、功能模块独立的原则。
1. 标准化目录结构
wumeng-vue3-app-template/ |- mock/ # 独立 Mock 服务目录,与 src 平级,存放接口模拟文件 | |- demo.ts # 示例 Mock 接口文件 |- src/ | |- http/ # 网络请求核心目录 | | |- core/ # 通用请求封装层,与业务无关,可抽离为独立 lib | | | |- http-client.ts # Axios 实例核心封装 | | | |- interceptors.ts # 拦截器相关封装 | | | |- index.ts # 通用能力统一导出 | | | |- types.ts # 通用类型定义 | | |- index.ts # 项目配置层,组装 core 能力,自定义项目配置 | |- services/ # 业务请求层(原 api 层),与具体业务绑定 | | |- base-service.ts # 通用 CRUD 封装,业务模块可继承
2. 四层核心设计思路
整个网络请求架构分为四层,每层职责清晰、低耦合,符合企业级项目的可复用、可扩展、可维护原则,四层的调用关系为业务请求层 → 项目配置层 → 通用请求封装层 → Axios 原生库。
- Mock 服务层:项目根目录下的 mock 目录,独立于业务代码,专门用于模拟后端接口,替代真实服务端完成前端联调测试,也可根据项目需求替换为 SpringBoot、NestJS 等真实后端服务。
- 通用请求封装层:src/http/core/目录是整个封装的核心,对 Axios 进行底层封装,包含拦截器、取消请求、请求防重、请求重试等通用功能,且每个功能独立成类。该目录的代码与具体业务完全解耦,原则上可抽离为独立的工具库,在多个项目中复用,也是后续微前端共享库的核心内容。
- 项目配置层:src/http/index.ts 作为项目的自定义配置入口,通过组装 core 层的通用能力,配置项目专属的请求参数(如超时时长、基础请求路径)、拦截器处理逻辑、错误处理方式等,最终导出适配当前项目的 Axios 实例和 CRUD 函数,为业务层提供调用入口。
- 业务请求层:src/services/目录对应传统开发中的 api 目录,与具体业务绑定,负责定义各业务模块的接口。这里抽离了 base-service.ts 实现通用的 CRUD 方法,各业务模块的 service 可直接继承,减少重复代码。
四层的层级依赖关系可概括为:
+---------------------+ | service 层 (业务 API 定义) | +---------------------+ ↑ | 调用 +---------------------+ | 项目配置层 (项目自定义) | +---------------------+ ↑ | 实例化 +---------------------+ | 通用请求封装层 (通用能力) | +---------------------+ ↑ | 依赖 +---------------------+ | Axios 原生库 | +---------------------+
3. 通用类型定义
与后端交互的过程中,统一的类型定义能保证 TypeScript 的类型安全,也能让前后端的交互格式更规范。在 src/http/core/types.ts 中定义三个基础通用类型,覆盖通用响应、分页请求、分页响应三大高频场景,且支持泛型适配不同业务数据。
/**
* 通用响应结构,适配大多数企业级项目的接口返回格式
*/
export interface ApiResp<T = any> {
code: number
message: string
data: T
}
/**
* 分页请求结构,标准化分页入参
*/
export interface PageReq {
pageNum: number
pageSize: number
}
/**
* 分页响应数据结构,适配通用响应的 data 字段
*/
export interface PageData<T> {
list: T[]
total: number
}
在 src/http/core/index.ts 中统一导出所有类型,方便其他模块按需引入:
export * from './types'
关于通用响应结构的设计争议
在企业级项目中,通用响应结构的设计有两种主流思路,各有优劣,可根据团队开发习惯和项目需求选择:
- 思路一:无论请求成功 / 失败,均返回 code/data/message 标准结构。该方式将 HTTP 请求错误(如 404、500)与业务错误(如参数错误、权限不足)解耦,前端解析时需先判断 HTTP 状态码,再判断业务 code 码,适合大型项目的精细化错误处理。
- 思路二:请求成功直接返回业务数据,业务失败时才返回标准结构。该方式将两类错误合并处理,前端只需判断 HTTP 状态码为 200 即视为业务成功,开发更简洁,适合中小型项目或快速开发场景。
我个人更倾向于第二种思路,但结合多年的团队开发实践,大部分开发者更习惯第一种思路,因此本系列也基于第一种思路展开封装。
快速落地:基于 MockJS+Vite 搭建 Mock 服务
在后端接口尚未开发完成时,Mock 服务能模拟接口返回数据,让前端开发脱离后端依赖,实现前后端并行开发。本次基于 MockJS 生成模拟数据,结合 vite-plugin-mock 插件快速搭建 Mock 服务,该插件支持热更新、TypeScript 友好,是 Vite 项目的主流 Mock 方案。
1. 安装依赖
使用 pnpm 安装核心依赖,其中@types/mockjs 为 MockJS 提供 TypeScript 类型支持,依赖版本选择生产环境验证过的稳定版本:
pnpm add mockjs @types/mockjs vite-plugin-mock -D # 版本说明:mockjs@1.1.0、vite-plugin-mock@3.0.2
2. 配置 Vite 插件
在项目根目录的 vite.config.ts 中引入并配置 vite-plugin-mock,指定 Mock 文件目录,开启 Mock 服务:
// 省略其他导入
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
// 其他插件(如 vue())
viteMockServe({
mockPath: './mock', // 指定 Mock 文件的存放目录
enable: true, // 开启 Mock 服务
}),
],
// 其他 Vite 配置
})
3. 编写 Mock 接口实现 CRUD
在 mock 目录下创建 demo.ts,基于 MockJS 生成模拟数据,实现一套标准的 CRUD Mock 接口,适配前文定义的 ApiResp 通用响应结构,同时支持分页、关键词搜索等高频功能。
import Mock from 'mockjs'
import { ApiResp } from '../src/http/core'
import { MockMethod } from 'vite-plugin-mock'
// 生成 100 条模拟测试数据
const demoList = Mock.mock({
'list|100': [
{
'id|+1': 1,
title: '@ctitle(5, 10)', // 随机生成中文标题
content: '@cparagraph(1, 3)', // 随机生成中文段落
author: '@name', // 随机生成姓名
status: '@boolean', // 随机生成布尔值
createdAt: '@datetime', // 随机生成时间
updatedAt: '@datetime',
},
],
}).list
// 封装成功响应
function success<T>(data: T): ApiResp<T> {
return {
code: 0,
message: 'success',
data,
}
}
// 封装失败响应
function error(message: string, code: number = 500): ApiResp<null> {
return {
code,
message,
data: null,
}
}
// 定义 Mock 接口
const demoMock: MockMethod[] = [
// 分页查询列表,支持关键词搜索
{
url: '/api/demo',
method: 'get',
timeout: 1000, // 模拟网络延迟
response: ({ query }) => {
const pageNum = parseInt(query.pageNum) || 1
const pageSize = parseInt(query.pageSize) || 10
const keyword = query.keyword || ''
// 关键词过滤
let filteredList = demoList
if (keyword) {
filteredList = demoList.filter(
(item) =>
item.title.includes(keyword) ||
item.content.includes(keyword) ||
item.author.includes(keyword)
)
}
// 分页处理
const start = (pageNum - 1) * pageSize
const end = start + pageSize
const list = filteredList.slice(start, end)
return success({ list, total: filteredList.length })
},
},
// 根据 ID 查询详情
{
url: '/api/demo/:id',
method: 'get',
timeout: 3000,
response: ({ query }) => {
const item = demoList.find((item) => item.id === parseInt(query.id))
return item ? success(item) : error('Item not found')
},
},
// 新增数据
{
url: '/api/demo',
method: 'post',
response: ({ body }) => {
const newItem = {
id: demoList.length + 1,
...body,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
demoList.push(newItem)
return success(newItem)
},
},
// 根据 ID 修改数据
{
url: '/api/demo/:id',
method: 'put',
response: ({ query, body }) => {
const index = demoList.findIndex((item) => item.id === parseInt(query.id))
if (index !== -1) {
demoList[index] = {
...demoList[index],
...body,
updatedAt: new Date().toISOString(),
}
return success(demoList[index])
}
return error('Item not found')
},
},
// 根据 ID 删除数据
{
url: '/api/demo/:id',
method: 'delete',
response: ({ query }) => {
const index = demoList.findIndex((item) => item.id === parseInt(query.id))
if (index !== -1) {
demoList.splice(index, 1)
return success(null)
}
return error('Item not found')
},
},
]
export default demoMock
4. 验证 Mock 服务
启动 Vite 项目,在浏览器中访问 http://localhost:5173/api/demo,若能看到包含 code/message/data 的 JSON 数据,且 data 中包含分页的 list 和 total 字段,说明 Mock 服务搭建成功。
写在最后
本文完成了 Vue3 网络请求封装的分层架构设计和 Mock 服务落地,这是后续所有网络请求功能实现的基础,核心是通过分层解耦让通用请求能力与业务代码分离,保证代码的可复用性和可维护性。
下一篇文章,我将进入核心的编码环节,讲解 Axios 的面向对象化封装,以及请求拦截器、响应拦截器的实现,同时会处理请求头、错误统一处理等企业级项目中的高频需求。
文章来源公众号:AI 懒人码农
以上关于Vue3 网络请求优雅封装:从分层架构设计到 Mock 服务落地的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » Vue3 网络请求优雅封装:从分层架构设计到 Mock 服务落地
微信
支付宝