Vue3企业级项目网络请求封装:分层架构与Mock服务搭建

更新日期: 2026-03-18 阅读: 28 标签: 请求

vue3企业级项目里,网络请求层的设计是个基础活,也是个核心活。一个好的请求架构,能让项目开发顺溜不少,后期维护也省心。

我打算把Vue3网络请求的优雅封装讲清楚,包括架构设计、Mock服务、Axios拦截器、取消请求、防重、重试、useRequest封装这些内容。


技术选型:用Axios还是Alova

做网络请求封装,先得选个请求库。

Axios大家最熟,生态成熟,用着简单,企业项目里用得最多,这次肯定选它。

顺便提一下Alova,这库功能挺强,状态管理、请求缓存都有优势,设计思路也值得学。但我只在个人项目里试过,没在生产落地过,所以系列还是用Axios,保证方案实用、能落地。


前期准备:标准化目录与分层设计

代码前先把目录结构搭好,这是分层架构的基础,后面代码也好组织。

1. 标准化目录结构

wumeng-vue3-app-template/
|- mock/        # Mock服务目录,和src平级,放接口模拟文件
|   |- demo.ts  # 示例Mock接口
|- src/
|   |- http/    # 网络请求核心目录
|   |   |- core/ # 通用请求封装层,和业务无关
|   |   |   |- 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直接继承,少写重复代码。

层级依赖关系长这样:

text
+---------------------+
| 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类型支持,版本选生产验证过的稳定版:

bash
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服务落地,这是后面所有功能的基础。核心思想是通过分层解耦,让通用请求能力和业务代码分开,保证代码好复用、好维护。

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

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

相关推荐

flutter之网络请求dio封装,拦截器的封装

flutter一直很火的网络请求插件dio,直接上代码,写成一个类,可以直接使用,包含请求的封装,拦截器的封装

ajax异步请求302分析

遇到这样一种情况,打开网页两个窗口a,b(都是已经登录授权的),在a页面中退出登录,然后在b页面执行增删改查,这个时候因为授权原因,b页面后端的请求肯定出现异常(对这个异常的处理,进行内部跳转处理),b页面中的ajax请求的回调中就会出现问题

nginx 301跳转https后post请求失效问题解决

强制把http请求跳转到https,结果发现App有部分的功能不能使用,因为App一共设置了4种请求方式,分别是GET,POST,DELETE和OPTIONS方式,设置301跳转后所有的请求方法都变成了GET方式,导致一些功能无法正常使用.

Js两个异步请求 同步合并数据

业务代码经常会有 两个不一样的请求,拿到数据后合并成新数组的操作。但是在异步请求中我们不知道哪个请求的回调更快返回,从而使代码的合并时间无法确定。这就需要在两个异步请求都完成后再做数据处理。

http请求过程的7个步骤

HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤:建立TCP连接、Web浏览器向Web服务器发送请求命令、Web浏览器发送请求头信息、 Web服务器应答

http请求的几种类型

http请求中的8种请求方法:opions 返回服务器针对特定资源所支持的HTML请求方法 ,Get 向特定资源发出请求,Post 向指定资源提交数据进行处理请求

node.js含有%百分号时,发送get请求时浏览器地址自动编码的问题

目前浏览器会对地址,进行编码,比如这个文件名:在发到后台时,会自动编码成:不过如果文件名中含有%百分号,编码过程则会出现问题,如

HTTP请求报文和响应报文

GET:请求获取Request—URL所标识的资源,POST:在Request—URL所标识的资源后附加资源,HEAD:请求获取由Request—URL所标识的资源的响应消息报头,PUT:请求服务器存储一个资源,由Request—URL作为其标识

ajax中options请求的理解

这个概念听着有点耳生,嗯是我自己这么说的。我们可以把浏览器自主发起的行为称之为“浏览器级行为”。之所以说options是一种浏览器级行为,是因为在某些情况下,普通的get或者post请求回首先自动发起一次options请求

HTTP请求的11个处理阶段

几乎所以有关Nginx书只要是讲深入点的就会讲到Nginx请求的11个处理阶段,要记住这些真是不易,人脑特别不擅长记住各种东西,只能做些索引罢了,能做到知道这个知识点在哪儿能找到不就行了,可是你去面试还是问这些理论,所以这里汇总下记录如下

点击更多...

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