ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue3项目前后端联调实战:从Mock数据搭建到真实接口切换

Vue3项目前后端联调实战:从Mock数据搭建到真实接口切换 前后端联调这四个字干前端的人听到都会心头一紧。后端说接口下周给下周变成下下周页面早写好了可一接数据全是问题。被坑了几次之后我现在做 Vue3 项目第一件事就是把 Mock 数据搭起来。这篇文章就围绕 Vue3 项目里的 Mock 数据与联调展开把我实际用过的方案、踩过的坑、以及从 Mock 切到真实接口时的处理顺序都整理出来。这篇文章适合两类人看一类是刚接触 Vue3想弄明白 Mock 到底该怎么落地的新手另一类是已经在写业务但每次都被“接口没给”“字段对不上”折腾到加班的老手。我会尽量讲得具体代码可以直接抄思路可以搬到你自己的项目里。1. 为什么要做 Mock联调之前的“并行开发”1.1 前端开发真正卡在哪儿很多人以为前端开发卡在后端接口没完成所以只能等着。其实卡住你的不是接口没给而是没有一套能模拟接口行为的假数据层。页面写死了const list [...]刷新后数据就复位增删改查全都验不了写一个临时接口文件路径一换就要改一堆代码最怕的是后端接口终于给了结果字段名和你拍脑袋写的完全不一样翻工量直接翻倍。Mock 数据解决的正是这个核心问题让前端在接口还没就绪时依然能按照业务逻辑完整走通整个功能流程。列表页能分页、详情页能编辑、表单能提交、登录能拿 Token、无权限能报 401这些都不依赖后端是否完成。我做了几个项目后总结下来前端真正能高质量完成任务的前提不是前端技术多强而是 Mock 层有多接近真实接口。Mock 越像真后端联调时出的幺蛾子就越少。1.2 Mock 的本质是接口契约不是造假数据这里必须先纠正一个认知Mock 不是随便造几个数据让页面不报错就完事了。接口文档里会写清楚字段名、字段类型、嵌套结构、状态码、错误提示而这些内容都应该原样落在 Mock 数据里。Mock 层本质上是一份“可运行的接口契约”它把前端和后端共同认可的口径固定下来。比如后端返回用户信息{ code: 0, message: ok, data: { id: 1001, userName: admin, role: admin } }那么前端不管等不等得到真接口都要按这个结构开发调用request以后拿到code判断业务成功取data.userName渲染用户名。只有这样联调时才会变成“核对细节”而不是“重写整个页面”。我在项目里经常跟后端说Mock 数据就是初步的接口约定你们实现接口时如果发现字段设计不合理提前一天告诉我不要在联调当天突然改。这句话很管用因为它把 Mock 变成了双方都认的基线而不是前端自娱自乐。1.3 主流 Mock 方案对比我为什么选了拦截器方案Vue3 项目里可用的 Mock 方案不少我把实际用过的拉出来对比一下。方案实现思路优点缺点本地 JSON 文件页面直接 import 或请求本地 json最简单、零依赖无法模拟延迟、状态码、动态参数交互反馈缺失vite-plugin-mockVite 插件在构建层拦截请求配置直观、支持 mockjs 语法依赖插件自身维护部分版本兼容性差动态逻辑受限axios 拦截器 Mock在 request 封装层判断并返回假数据依赖少、完全可控、可模拟任意场景需要自己写一套匹配和注册逻辑MSWService Worker 拦截真实请求最接近真实环境、能拦截图片等资源学习成本高项目初期重部分环境有兼容问题最终我选的是“axios 拦截器 手写 Mock 规则”这套方案主要是看中三个点。第一它不依赖额外插件升级 Vite 或 Vue 版本时少一个变量。第二它可以完全掌控请求上下文比如根据请求头里的 Token 动态决定返回 401 还是正常数据这在 mock 权限流程时非常有用。第三Mock 和真实接口共用同一个request方法切开关只改一个环境变量联调时不容易出现“Mock 通、真实接口不通”的诡异问题。2. 搭建 Vue3 Mock 环境目录、开关与依赖准备2.1 Mock 目录这样组织项目大了也不乱Mock 看似只是临时数据但一旦业务模块多了写得不规范就是灾难。我现在的目录结构是固定的src/ ├── api/ # 接口请求层与后端交互的唯一入口 │ ├── modules/ │ │ ├── user.ts │ │ └── list.ts │ └── request.ts # axios 封装 Mock 开关判断 ├── mock/ # Mock 数据与规则 │ ├── index.ts # 注册所有 Mock 规则向外提供 mockRequest │ ├── types.ts # Mock 规则、上下文类型定义 │ ├── utils.ts # URL 解析、Token 校验、响应封装等公共函数 │ └── modules/ # 按业务模块拆分 │ ├── user.ts │ └── list.ts有人可能会说Mock 反正是临时用的何必拆这么细。但项目只要超过三个模块找一条接口规则就能让你翻几分钟。按模块拆开后后台上线前要删 Mock直接看mock/modules列表就行哪个模块联调完了就清哪个清清楚楚。types.ts里我定义了两个核心类型export interface MockContext { url: string // 去掉 baseURL 后的路径 method: string // get / post / put / delete query: Recordstring, string | undefined // URL 查询参数 params: Recordstring, string // 动态路由参数如 :id body: any // 请求体 headers: Recordstring, string // 请求头 } export interface MockRule { url: string // 示例/api/user/list method?: string // get / post delay?: number // 模拟延迟单位毫秒 enabled?: boolean // 可以让某条规则临时关闭 response: (ctx: MockContext) any }这里有个小约定MockRule.url要带上/api前缀和接口请求保持一致。这样找规则时能以接口文档里的路径直接搜索省去换算。2.2 用环境变量控制 Mock 开关Mock 最忌讳的写法是直接写死在代码里比如某条请求注释掉改成return假数据。一旦写死联调时想切回真实接口就要翻代码翻得多了还会漏改。我用环境变量控制具体是在项目根目录建两个文件.env.developmentVITE_USE_MOCKtrue VITE_API_BASE_URL/api.env.productionVITE_USE_MOCKfalse VITE_API_BASE_URL/apiVue3 里通过import.meta.env.VITE_USE_MOCK读取。注意这里读出来的一定是字符串true或false不能直接当成布尔值用。之所以不把开关写到代码里是因为团队开发时每个人的环境不同。有的人后端服务没起来需要 Mock有的人后端已经起了要联调新接口。用环境变量每个人只要改自己本地的.env.development即可不用动公共代码。2.3 Vite 项目里的基础配置搭建项目这一步相信大多数人都熟了我简单带一句npm create vitelatest my-vue-app -- --template vue-ts cd my-vue-app npm install axios不建议在 Mock 阶段引入过多依赖比如 mockjs。不是不能用而是 mockjs 生成的中文名、随机数格式未必贴合真实业务有时候生成的 userId 是120000197001010000这种诡异值联调时反而干扰判断。我用的是最简单的方式规则函数里手动构造结构化数据。看起来多写几行但你能精确控制每条数据长什么样什么时候返回空数组、什么时候返回code: 1都在掌控之中。3. 手写拦截器 Mock从 request 封装到规则注册3.1 在 request.ts 里预留 Mock 通道Mock 要和真实请求无缝切换最好的方式是把判断放在统一封装的request函数里而不是分散到各个页面。下面这个封装是我目前一直在用的// src/api/request.ts import axios, { AxiosInstance, AxiosRequestConfig } from axios import { mockRequest } from /mock const service: AxiosInstance axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) service.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( (response) response.data, (error) Promise.reject(error) ) export function requestT any(config: AxiosRequestConfig): PromiseT { const useMock import.meta.env.VITE_USE_MOCK true if (useMock) { return mockRequest(config) as PromiseT } return service.requestany, T(config) }关键点在于Mock 阶段走mockRequest真实阶段走service.request两者最后返回的数据形态必须一致。真实后端返回的是整个响应体{ code, message, data }所以 Mock 规则里返回的也必须是这个结构而不是只返回一个列表。很多半途接手项目的同学最容易在这里出错Mock 返回[{ id: 1 }]真实接口返回{ code: 0, data: [{ id: 1 }] }结果页面里res.data到底取哪一层完全对不上。3.2 写一个通用的 Mock 匹配器Mock 的核心是一个“按请求规则找假数据”的匹配器。它要做三件事解析请求 URL、匹配规则、返回数据。先看工具函数// src/mock/utils.ts export function parseUrl(url: string, baseURL?: string) { let raw url || if (baseURL raw.startsWith(baseURL)) { raw raw.slice(baseURL.length) } const [path, queryString ] raw.split(?) const query: Recordstring, string | undefined {} queryString.split().forEach((pair) { if (!pair) return const [key, value] pair.split() if (key) query[key] decodeURIComponent(value || ) }) return { path: path || /, query } } export function matchPath(pattern: string, path: string) { const patternParts pattern.split(/).filter(Boolean) const pathParts path.split(/).filter(Boolean) if (patternParts.length ! pathParts.length) return null const params: Recordstring, string {} for (let i 0; i patternParts.length; i) { const pp patternParts[i] const real pathParts[i] if (pp.startsWith(:)) { params[pp.slice(1)] decodeURIComponent(real) } else if (pp ! real) { return null } } return params }这里我见过有些同学直接引入path-to-regexp来做动态路由匹配完全没问题。我选择手写是因为 Mock 规则里用到的动态参数就:id这一种手写二十行足够不必为一个变量引入一个依赖。接着写mockRequest// src/mock/index.ts import type { AxiosRequestConfig } from axios import { userRules } from ./modules/user import { listRules } from ./modules/list import { matchPath, parseUrl } from ./utils import type { MockContext, MockRule } from ./types const rules: MockRule[] [...userRules, ...listRules] function cleanPath(p: string) { return p.replace(/^\/api/, ) || / } export function mockRequest(config: AxiosRequestConfig): Promiseany { return new Promise((resolve, reject) { const method (config.method || get).toLowerCase() const { path, query } parseUrl(config.url || , config.baseURL) const headers (config.headers || {}) as Recordstring, string const body config.data for (const rule of rules) { if (rule.enabled false) continue if (rule.method rule.method.toLowerCase() ! method) continue const params matchPath(cleanPath(rule.url), path) if (!params) continue const ctx: MockContext { url: path, method, query, params, body, headers } const delay rule.delay ?? Math.floor(Math.random() * 400) 200 setTimeout(() { try { resolve(rule.response(ctx)) } catch (error) { reject(error) } }, delay) return } reject(new Error([Mock] 未匹配到规则: ${method.toUpperCase()} ${path})) }) }这段代码看起来不多但解决了几个很实际的问题。第一cleanPath统一把规则 URL 里的/api去掉再匹配你在 Mock 规则里写不写/api都行。第二setTimeout模拟网络延迟页面里的 loading 效果能真实跑起来而不是一瞬间闪过去。第三如果某个请求没有匹配到任何规则直接报错。这样做能第一时间发现是不是漏配了 Mock而不是页面空白但控制台静悄悄。3.3 业务场景模拟分页、登录态和动态路由参数Mock 里最有价值的是把业务场景模拟出来而不是只给数据。先看分页列表。真实列表接口必然有page、pageSize、total下面这段规则能直接应对各种分页场景// src/mock/modules/list.ts import type { MockRule } from ../types const TOTAL 57 function buildList(page: number, pageSize: number) { const start (page - 1) * pageSize const realTotal Math.min(pageSize, Math.max(0, TOTAL - start)) return Array.from({ length: realTotal }, (_, i) { const id start i 1 return { id, name: 测试用户${id}, avatar: https://dummyimage.com/100x100/ccc/000text${id}, status: id % 3 0 ? disabled : active, createdAt: 2024-06-${String((id % 28) 1).padStart(2, 0)} } }) } export const listRules: MockRule[] [ { url: /api/user/list, method: get, response: ({ query }) { const page Number(query.page || 1) const pageSize Number(query.pageSize || 10) return { code: 0, message: ok, data: { list: buildList(page, pageSize), total: TOTAL, page, pageSize } } } }, { url: /api/user/:id, method: get, response: ({ params }) { const id Number(params.id) return { code: 0, message: ok, data: { id, name: 测试用户${id}, role: id % 2 0 ? admin : guest, remark: id % 5 0 ? : 普通用户 } } } } ]注意分页这里有个容易忽略的细节当page超出总页数时返回的列表应该是空数组而不是生成长度小于pageSize的数据。上面的buildList用TOTAL - start做了保护翻到第 6 页时 length 为 0前端可以验证空数据展示。很多 Mock 工具直接生成固定长度的数组导致最后一页和空状态永远测不到联调时一翻页就露馅。再看登录态校验。管理后台项目必定有 TokenMock 阶段怎么模拟做法是登录接口返回一个假的 Token其他接口判断请求头里有没有带没带就返回 401。function getToken(headers: Recordstring, string) { const auth headers?.Authorization || headers?.authorization || return auth.startsWith(Bearer ) ? auth.slice(7) : auth } export const userRules: MockRule[] [ { url: /api/login, method: post, response: () { return { code: 0, message: ok, data: { token: mock_token_${Date.now()} } } } }, { url: /api/user/info, method: get, response: ({ headers }) { if (!getToken(headers)) { return { code: 401, message: 未登录或登录已过期, data: null } } return { code: 0, message: ok, data: { name: 管理员, role: admin, permissions: [user:list, user:edit] } } } } ]这个思路必须和前端的 axios 请求拦截器配合登录成功把 Token 存进 localStorage后续请求自动带上。真实后端联调时只要把.env.development的VITE_USE_MOCK改成falseToken 的存取逻辑完全不用动。3.4 给 Mock 加上合理的网络延迟为什么要特意加延迟因为真实接口的网络耗时是天然的“loading 测试器”。如果 Mock 瞬间返回前端开发时根本看不到 loading 状态等到联调时接口慢了才发现表格会在 loading 和空白之间闪烁、按钮会被连点两次。我在mockRequest里默认用了200ms 到 600ms的随机延迟。个别接口可以覆盖比如上传接口延迟设 1500ms用来验证上传中不能关闭弹窗登录接口延迟设 800ms看看按钮 loading 文案是否正常。实现方式很简单规则里加delay字段即可{ url: /api/upload, method: post, delay: 1500, response: () ({ code: 0, message: ok, data: { url: /uploads/xxx.png } }) }有人会觉得 600 毫秒太慢影响开发体验。我的建议是开发阶段别关掉随机延迟至少保留一个 200ms 左右的底数这样所有异步状态都能被真实触发。真的嫌烦可以只在本地调试某个具体 bug 时临时把 delay 改成 0。4. Mock 切换到真实接口联调阶段的完整流程4.1 联调前对接口清单重点对齐三件事Mock 数据搭好不代表联调就一定会顺利。我经历过太多次“前一刻还跑得好好的切到真实接口就白屏”的情况大部分原因是 Mock 和后端接口的契约不一致。联调前我都会整理一张接口清单哪怕是用表格写在 issue 里也行。模块方法路径请求参数响应结构状态码联调状态登录POST/api/loginuserName, password{ code, message, data: { token } }0成功/401完成用户列表GET/api/user/listpage, pageSize, keyword{ code, message, data: { list, total } }0成功Mock 中用户详情GET/api/user/:idid{ code, message, data }0成功Mock 中核对时我会把重点放在三件事上。一是请求路径和方法。常见问题后端接口是/api/user/list还是/users分页参数是page还是current。二是响应体外层结构。code是后端错误码还是 HTTP 状态码很多后端会把业务错误通过 HTTP 200 code: 500返回这就要在响应拦截器里区分。三是字段类型。尤其id这种字段后端如果是Long类型且数字很大超过 JS 安全整数前端直接用会丢精度应该让后端返回字符串。4.2 Vite 代理配置为什么 Mock 时没跨域联调突然跨域Mock 阶段请求根本没发出去是在 axios 层直接返回假数据所以你不会遇到跨域问题。一旦把VITE_USE_MOCK改成false真实请求立刻发到后端服务器跨域问题就冒出来了。解决跨域最标准的方式是配置 Vite 开发服务器代理// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这里的target是后端服务地址rewrite把/api前缀去掉是因为很多后端接口本身没有/api这一层由前端网关统一加。我见过一个很典型的翻车现场前同事把代理配好之后Mock 阶段一切正常开始联调就被跨域问题卡了两天最后才发现是changeOrigin没写。这个属性必须为true否则后端收到请求时Host头还是localhost:5173后端服务一旦做了域名校验就会拒绝。4.3 字段类型不一致null、命名和空数组的处理经验联调时最容易炸的不是接口不通而是字段形态和 Mock 不一致。先看 null。后端数据库里某个字段是空的返回null而前端 Mock 里写的是{ avatar: }。页面渲染user.avatar.url时直接报错Cannot read properties of null。我的经验是前端别指望后端改先在自己代码里兜底。export function normalizeUser(raw: any): User { return { id: raw.id ?? , name: raw.name ?? --, avatar: raw.avatar ?? , role: raw.role ?? guest } }再来是命名风格。很多后端 Java 项目习惯create_time前端再用createdAt接一接就是undefined。两种处理方式一是联调前就约定后端起名按前端来二是实在改不了就在api/modules层做映射。我强烈建议不要写全局响应拦截器去把所有字段都转驼峰因为那会让接口数据结构变得“看起来一致实际上不可追踪”一旦排查问题你都不知道原始字段到底是什么。最后是空数组和 null 的区别。列表接口后端没事先初始化数据时可能返回data: null前端表格组件往往期望data.list是数组直接list.map就崩。我在request的类型定义中明确要求后端列表为空就返回[]不要返回null。这条规约写进接口文档联调会省很多事。4.4 接口完成一半时的混用策略真实联调过程中后端不是一口气把所有接口全部给完的。今天登录接口好了明天用户列表好了详情接口还要等两天。这时候如果把 Mock 整体关掉未完成的接口就全断了整体开着已联调的接口又没法走真实数据。我的做法是在 Mock 规则里加一个enabled字段把已完成联调的规则关闭{ url: /api/login, method: post, enabled: false, // 已经联调完成走真实接口 response: () ({ ... }) }这样mockRequest在遍历规则时会跳过它。如果请求没有匹配到任何启用的规则最终会把请求丢弃并抛错。但更完整一点的做法是在mockRequest里如果没有任何规则匹配就让请求继续走真实 axios而不是直接抛错。也就是说Mock 只拦截“需要拦截的接口”其余接口放行。我实际项目里两种都试过最终选的是“未匹配则抛错”。原因很简单放行模式容易让前端把“漏配 Mock”当成“后端还没好”等到联调时才发现某个请求一直在打真实接口中间状态完全失控。抛错模式虽然初期会在控制台看到一堆[Mock] 未匹配到规则但这些错误逼着你把规则补全反而更安全。5. 常见问题与排查技巧实录5.1 Mock 不生效先从这三个地方查很多人问我“Mock 配了为什么请求还是打到了后端”这种问题九成出在下面三处。第一环境变量读错了。import.meta.env.VITE_USE_MOCK读出来是字符串true你写if (import.meta.env.VITE_USE_MOCK)判断时它永远为真反过来写 false也永远不成立。统一用 true判断。第二请求没有走request函数。页面里直接用axios.get或者service.get绕过 Mock 是必然的。所有接口调用必须汇总到api/modules/*.ts由request统一出去。第三URL 匹配不上。Mock 规则写的是/api/user/list但parseUrl里已经把/api去掉了匹配器内部还需要cleanPath再处理一次。我建议在mockRequest里暂时加一行console.log(method, path)能看到真实匹配路径比瞎猜快得多。5.2 401 与 Token 在 Mock 阶段怎么处理之前提到 Mock 阶段要校验 Token这里说一个更具体的场景。登录接口模拟返回token mock_token_123前端把它存到 localStorage。然后请求用户信息时axios 请求拦截器加了Authorization: Bearer mock_token_123。这时 Mock 的getToken函数能不能读到取决于你传进去的headers是什么。我在实际代码里遇到过一个问题axios 的 headers 不是普通对象是AxiosHeaders实例访问headers.Authorization可能拿到的是一个数组而不是字符串。所以在getToken里我做了两层兼容function getToken(headers: Recordstring, any) { const auth headers?.Authorization || headers?.authorization || const strAuth Array.isArray(auth) ? auth.join( ) : String(auth) return strAuth.startsWith(Bearer ) ? strAuth.slice(7) : strAuth }另外当 Mock 返回code: 401时前端的全局响应拦截器应该统一跳转登录页。这个逻辑放在service.interceptors.response.use里对 Mock 和真实接口都生效service.interceptors.response.use( (response) { if (response.data?.code 401) { localStorage.removeItem(token) window.location.href /login } return response.data }, (error) Promise.reject(error) )5.3 字段对不上、分页错乱的排查思路联调最经典的坑之一就是分页错乱。后端给的响应体是{ status: 200, data: { records: [...], total: 100 } }前端按 Mock 里{ code, data: { list } }写结果data.list永远是 undefined。遇到这种问题我先打开 Network 面板看真实响应体然后只改api/modules/list.ts里的适配函数不让分页逻辑侵入到页面组件// 页面组件只关心 list / total export function getList(params: ListQuery) { return requestany, { data: { list: Item[]; total: number } }({ url: /api/user/list, method: get, params }).then((res) { // 兼容后端 response 结构差异 return { list: res.data?.records || res.data?.list || [], total: res.data?.total || 0 } }) }这种“适配层”策略能保证页面组件始终按照统一的list/total结构开发。后端改动字段名时你只改一个文件的映射而不是全局替换所有页面。5.4 快速排查速查表现象可能原因排查与解决Mock 不生效环境变量判断有问题检查import.meta.env.VITE_USE_MOCK trueMock 不生效请求绕过了 request 封装统一走api/modules层Mock 不生效规则 URL 不匹配mockRequest打印 method/path核对 cleanPath突然报跨域Mock 关闭后请求打到真实后端配置server.proxychangeOrigin: true页面报 null 错误后端返回 null做 normalize 兜底并约定返回默认值分页不出数据响应体结构和预期不一致在 api 层做字段适配页面只认统一结构登录后仍被判定未登录Token 读取兼容有问题检查 AxiosHeaders 的取值兼容某个接口联调完还想走 Mock规则未关闭给规则加enabled: false请求永远转圈Mock 随机延迟太高调整delay或临时改为固定 200ms这张表是我这些年排查联调问题时总结的高频原因。你可以把它直接贴到团队项目 README 的“常见问题”里新人碰到问题先查表能省不少答疑时间。这些年项目做下来我对 Mock 最大的感受是它不是糊弄数据的临时手段而是一个前端项目里值得认真维护的基础设施。联调完成也不代表 Mock 规则要立刻删掉留着它下次后端环境宕了、评审需要演示、或者新同事要接手开发时都能用得上。这些不起眼的设计反而能在关键时刻救大急。
返回列表