前后端联调这四个字,干前端的人听到都会心头一紧。后端说接口下周给,下周变成下下周;页面早写好了,可一接数据全是问题。被坑了几次之后,我现在做 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-mock | Vite 插件在构建层拦截请求 | 配置直观、支持 mockjs 语法 | 依赖插件自身维护,部分版本兼容性差,动态逻辑受限 |
| axios 拦截器 Mock | 在 request 封装层判断并返回假数据 | 依赖少、完全可控、可模拟任意场景 | 需要自己写一套匹配和注册逻辑 |
| MSW | Service 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: Record<string, string | undefined> // URL 查询参数 params: Record<string, string> // 动态路由参数,如 :id body: any // 请求体 headers: Record<string, 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.development:
VITE_USE_MOCK=true VITE_API_BASE_URL=/api.env.production:
VITE_USE_MOCK=false VITE_API_BASE_URL=/apiVue3 里通过import.meta.env.VITE_USE_MOCK读取。注意这里读出来的一定是字符串"true"或"false",不能直接当成布尔值用。
之所以不把开关写到代码里,是因为团队开发时每个人的环境不同。有的人后端服务没起来,需要 Mock;有的人后端已经起了,要联调新接口。用环境变量,每个人只要改自己本地的.env.development即可,不用动公共代码。
2.3 Vite 项目里的基础配置
搭建项目这一步相信大多数人都熟了,我简单带一句:
npm create vite@latest 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 request<T = any>(config: AxiosRequestConfig): Promise<T> { const useMock = import.meta.env.VITE_USE_MOCK === 'true' if (useMock) { return mockRequest(config) as Promise<T> } return service.request<any, 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: Record<string, 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: Record<string, 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): Promise<any> { return new Promise((resolve, reject) => { const method = (config.method || 'get').toLowerCase() const { path, query } = parseUrl(config.url || '', config.baseURL) const headers = (config.headers || {}) as Record<string, 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/000&text=${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 工具直接生成固定长度的数组,导致最后一页和空状态永远测不到,联调时一翻页就露馅。
再看登录态校验。管理后台项目必定有 Token,Mock 阶段怎么模拟?做法是:登录接口返回一个假的 Token,其他接口判断请求头里有没有带,没带就返回 401。
function getToken(headers: Record<string, 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改成false,Token 的存取逻辑完全不用动。
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/login | userName, password | { code, message, data: { token } } | 0成功/401 | 完成 |
| 用户列表 | GET | /api/user/list | page, pageSize, keyword | { code, message, data: { list, total } } | 0成功 | Mock 中 |
| 用户详情 | GET | /api/user/:id | id | { 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: Record<string, 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 request<any, { 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 === 'true' |
| Mock 不生效 | 请求绕过了 request 封装 | 统一走api/modules层 |
| Mock 不生效 | 规则 URL 不匹配 | mockRequest打印 method/path,核对 cleanPath |
| 突然报跨域 | Mock 关闭后请求打到真实后端 | 配置server.proxy,changeOrigin: true |
| 页面报 null 错误 | 后端返回 null | 做 normalize 兜底,并约定返回默认值 |
| 分页不出数据 | 响应体结构和预期不一致 | 在 api 层做字段适配,页面只认统一结构 |
| 登录后仍被判定未登录 | Token 读取兼容有问题 | 检查 AxiosHeaders 的取值兼容 |
| 某个接口联调完还想走 Mock | 规则未关闭 | 给规则加enabled: false |
| 请求永远转圈 | Mock 随机延迟太高 | 调整delay,或临时改为固定 200ms |
这张表是我这些年排查联调问题时总结的高频原因。你可以把它直接贴到团队项目 README 的“常见问题”里,新人碰到问题先查表,能省不少答疑时间。
这些年项目做下来,我对 Mock 最大的感受是,它不是糊弄数据的临时手段,而是一个前端项目里值得认真维护的基础设施。联调完成也不代表 Mock 规则要立刻删掉,留着它,下次后端环境宕了、评审需要演示、或者新同事要接手开发时,都能用得上。这些不起眼的设计,反而能在关键时刻救大急。