前端联调Mock系统:规则改写与断点拦截实战 📅 发布时间:2026/9/14 20:33:49 👁 浏览次数: 1. 这不是“造数据”而是前端联调的呼吸系统Mock 接口数据实操规则改写和断点拦截的联调——这标题里没一个生僻词但组合在一起就是前端工程师每天在真实项目里反复调试、反复推翻、反复重建的“呼吸节奏”。我带过十几支前端团队从电商秒杀到金融风控后台所有项目上线前最耗时、最易出错、最让后端同事皱眉的环节从来不是写页面而是接口还没好UI要跑通逻辑要验证错误流要覆盖。这时候Mock 不是锦上添花的玩具而是维持整个开发节奏不窒息的氧气面罩。你可能已经用过 vite-plugin-mock 或 mockjs 写过几行 return { code: 0, data: [...] }但那只是 Mock 的表皮。真正卡住团队进度的从来不是“能不能返回数据”而是“能不能按真实业务逻辑返回数据”比如用户登录成功后后续所有请求 header 要自动带上 token比如某个列表接口当传入 page2 且 size10 时必须返回第 11–20 条数据且 total 字段得是 157再比如支付失败时后端会返回 error_code4001而前端必须根据这个 code 跳转到特定错误页——这些光靠静态 JSON 文件根本撑不住。这就是为什么标题里特意强调“规则改写”和“断点拦截”前者让 Mock 具备条件判断与动态生成能力后者让开发者能像调试真实请求一样逐行看请求发了什么、被谁改了、最终去了哪。它不是替代后端而是把后端还没交付的契约提前用代码具象化、可执行、可验证。尤其在 vue3 ts 项目中这种能力更关键。TypeScript 的类型系统本意是保障前后端契约一致但如果 Mock 数据类型和真实 API 文档对不上ts-check 通过了运行时却报 undefined这种“类型幻觉”比没类型还危险。而规则改写能让你在 mock 层就复用接口定义比如从 OpenAPI spec 自动生成 mock 规则断点拦截则能实时校验请求参数是否符合 ts 接口定义。至于热搜词里反复出现的“免费金融数据接口”“股票数据接口api 免费”恰恰暴露了一个现实痛点很多团队拿公开 API 做 mock结果发现字段名不一致、频率限制突袭、返回结构随心所欲——这反而增加了调试成本。真正的 Mock核心不是“有数据”而是“可控、可溯、可验”。它解决的从来不是技术问题而是协作熵增问题让前端不等后端让测试不卡联调让产品能基于真实数据流做决策。下面我们就一层层拆开看这套呼吸系统怎么装、怎么调、怎么在关键时刻不掉链子。2. 整体设计思路为什么必须把 Mock 拆成三块齿轮咬合很多人把 Mock 当成一个开关开返回假数据关走真实接口。但实际项目里这种二元思维一上线就崩。我见过最典型的翻车现场某券商交易系统上线前一周前端用 mockjs 模拟行情推送一切正常切到真实 Wind 金融数据接口后发现每秒推送 50 条 tick 数据而 mock 里只模拟了 5 条/秒前端内存泄漏直接卡死。问题不在 mock 本身而在设计思路上——Mock 不是“替代”而是“镜像”。它必须包含三个不可分割的齿轮数据生成层、流量调度层、调试观测层。缺一不可否则就是纸糊的盾牌。2.1 数据生成层拒绝静态 JSON拥抱契约驱动静态 JSON 文件如 /mock/user/list.json最大的问题是“失真”。它无法响应不同参数组合不能模拟状态变更更无法体现接口间的依赖关系。比如用户详情页需要先调 /api/user/info再根据返回的 role 字段决定是否显示“风控设置”按钮而 /api/user/role 接口又依赖 /api/auth/token 的有效性。这种链式调用静态文件只能硬编码一个固定路径的返回值完全脱离业务上下文。我们采用的是契约驱动生成。具体做法是以 OpenAPI 3.0 规范的 yaml 文件为唯一信源用工具如 openapi-mock解析 schema自动生成基础 mock 规则。例如一个用户列表接口定义如下get: summary: 获取用户列表 parameters: - name: page in: query required: true schema: type: integer minimum: 1 - name: size in: query required: true schema: type: integer minimum: 1 maximum: 100 responses: 200: description: 成功 content: application/json: schema: $ref: #/components/schemas/PaginatedUserList components: schemas: PaginatedUserList: type: object properties: code: type: integer example: 0 data: type: array items: $ref: #/components/schemas/User total: type: integer example: 157openapi-mock 会据此生成类似这样的规则函数// mock/user/list.ts export default [ { url: /api/user/list, method: get, response: (req) { const page parseInt(req.query.page as string) || 1; const size parseInt(req.query.size as string) || 10; const start (page - 1) * size; const end start size; // 真实数据源应来自数据库或缓存此处用 faker 模拟 const users Array.from({ length: 200 }, (_, i) ({ id: i 1, name: 用户${i 1}, email: user${i 1}example.com, role: [admin, trader, analyst][i % 3] as const, })); return { code: 0, data: users.slice(start, end), total: users.length, }; } } ];提示这里的关键不是 faker 库而是page/size 参数被真实参与计算total 字段与数据总量严格对应。这保证了分页逻辑在 mock 和真实环境完全一致避免“mock 里一页 10 条真实环境一页 20 条”的联调灾难。2.2 流量调度层规则改写不是“if-else”而是路由策略引擎“规则改写”这个词容易让人误解为简单字符串替换。实际上在复杂系统中它是一套轻量级的请求路由策略引擎。它的核心任务是根据请求的 URL、method、header、query、body 等多维度特征动态决定该请求由哪条 mock 规则处理或是否放行至真实后端。我们不用 nginx 或网关做这事因为开发阶段需要极致的灵活性和可调试性。以金融场景为例某交易系统需同时对接内部风控 API 和外部行情 API如腾讯股票实时数据接口。我们设计了三层调度域名级分流所有https://risk.internal/api/开头的请求强制走本地 mockhttps://quote.tencent.com/开头的走代理见下文路径参数级改写对/api/order/create请求若 body 中order_type limit则返回预设的成功响应若order_type market则模拟 30% 概率返回code: 4002市价单风控拒绝Header 注入/剥离所有 mock 请求自动注入X-Mock-Source: localheader便于后端日志识别同时剥离Authorizationheader避免 mock 规则误读 token。这套调度逻辑不是写死在代码里而是通过一个配置对象声明// mock/router.config.ts export const mockRoutes [ // 规则1风控接口全 mock { match: { host: risk.internal }, handler: mock, }, // 规则2行情接口代理但加缓存控制 { match: { host: quote.tencent.com, path: /^\/v1\/stock\/.*$/ }, handler: proxy, proxyOptions: { changeOrigin: true, headers: { Cache-Control: no-cache }, }, }, // 规则3订单创建按 order_type 分支 { match: { path: /api/order/create, method: post }, handler: custom, customHandler: (req) { const body JSON.parse(req.body as string); if (body.order_type limit) { return { code: 0, order_id: Date.now().toString() }; } else if (body.order_type market) { return Math.random() 0.3 ? { code: 4002, message: 市价单风控拒绝 } : { code: 0, order_id: Date.now().toString() }; } } } ];注意这里的customHandler是函数不是字符串模板。它允许你写任意 ts 逻辑包括调用数据库、读取文件、甚至发起另一个 mock 请求。这才是“规则改写”的真正威力——它让 Mock 具备了业务逻辑的表达能力。2.3 调试观测层断点拦截不是 Chrome DevTools 的替代品而是它的增强插件断点拦截常被理解为“在请求发出前停一下”。但这太浅了。真正的断点必须能回答三个问题这个请求是谁发的它被哪条规则捕获了它的输入输出是否符合预期如果只能看到 raw request那和抓包工具没区别。我们的断点系统集成在 mock 服务启动时提供一个独立的 Web UI如 http://localhost:3001/mock-debug它实时展示所有匹配到的请求按时间倒序每个请求的完整链路原始 URL → 匹配的规则 ID → 规则执行耗时 → 返回状态码 → 响应体预览截断关键字段高亮比如对股票行情请求自动标出last_price、change_percent字段并与历史均值对比提示“当前价格波动超阈值”可交互操作点击任一请求可复制 curl 命令、查看完整响应体、重新触发该请求带相同参数更重要的是它支持条件断点。比如只在req.header[X-Debug] true时才触发断点避免日常开发被大量无关请求打断。这比在代码里加 debugger; 高效得多——你不需要修改业务代码只需在请求 header 加一个标记就能精准捕获目标流量。这三块齿轮——契约驱动的数据生成、策略引擎式的流量调度、可编程的调试观测——共同构成了一个活的 Mock 系统。它不追求“看起来像”而追求“行为一致”。当你在 vue3 组件里调用useStockQuote()hook 时它拿到的数据流、错误分支、加载状态和线上环境完全同构。这才是联调真正的意义不是“功能跑通”而是“契约可信”。3. 核心细节解析规则改写与断点拦截的实操要点规则改写和断点拦截听起来抽象但落到键盘上就是几行配置和一个调试界面。不过正是这些看似简单的细节决定了 Mock 系统是玩具还是生产级工具。我带团队落地过 7 个大型金融项目踩过的坑基本都集中在以下四个实操要点上。它们不是最佳实践而是血泪教训换来的“必须项”。3.1 规则改写的底层逻辑永远用“匹配器”代替“字符串判断”新手常犯的错误是看到 URL 就用req.url.includes(/user/)看到参数就用req.query.id 123。这在简单 demo 里没问题但一到真实项目就崩溃。原因有三URL 可能带 query string 编码/user?id123%20abc参数可能有默认值?page1size10中 page 可能缺失更致命的是后端接口路径可能重写如 nginx 把/api/v1/user代理到/user。我们强制使用标准化匹配器URL 匹配用 path-to-regexp 库Express 默认而非字符串操作。它支持命名参数、可选参数、正则捕获// ✅ 正确能匹配 /api/user/123、/api/user/123/edit、/api/user/abc const keys []; const re pathToRegexp(/api/user/:id(\\d), keys); // ✅ 正确支持 query 参数提取 const re2 pathToRegexp(/api/stock/:symbol, keys, { end: false, // 允许后面跟 ?xxx });Query/Body 匹配不直接解析而是用 JSON Schema 做校验。例如订单创建接口要求amount必须是 number 且 0symbol必须是字符串且长度 2-6import Ajv from ajv; const ajv new Ajv(); const validateOrder ajv.compile({ type: object, properties: { amount: { type: number, minimum: 0.01 }, symbol: { type: string, minLength: 2, maxLength: 6 }, }, required: [amount, symbol], }); // 在规则中调用 if (!validateOrder(req.body)) { return { code: 400, message: 参数校验失败, errors: validateOrder.errors }; }实操心得JSON Schema 校验比手写 if-else 更可靠且能自动生成错误提示。我们甚至把后端 Swagger 的 schema 直接导出作为 mock 的校验依据确保前后端对“合法请求”的定义完全一致。3.2 断点拦截的粒度控制按“业务域”而非“技术层”设置断点很多团队把断点设在 axios 拦截器里结果一打开就满屏GET /favicon.ico、OPTIONS /api/user。这不是调试是噪音轰炸。真正的断点必须服务于业务目标。我们按“业务域”划分断点层级业务域触发条件典型场景用户认证域URL 包含/auth/或 header 有Authorization登录、登出、token 刷新重点观察 token 生成逻辑和有效期行情数据域Host 为quote.tencent.com或windapi.xxx.com股票价格、指数、资金流向关注延迟、丢帧、数据一致性交易指令域Method 为 POST/PUT 且 path 含/order/或/trade/下单、撤单、查询持仓必须验证请求幂等性和错误码映射风控规则域Header 包含X-Risk-Check: true模拟风控拦截验证前端错误提示和降级逻辑每个域对应一个独立的断点开关。开发人员只需在测试用例里加一行headers: { X-Risk-Check: true }就能精准捕获风控相关流量其他请求照常飞过。这比在代码里埋debugger高效十倍——你不需要重启应用不需要定位到具体 hook只要发一个带标记的请求。3.3 规则改写的副作用管理Mock 不是真空它必须感知真实世界Mock 最大的陷阱是“过度隔离”。比如一个用户列表接口 mock 返回 100 条数据但真实后端因权限控制只返回 5 条。如果前端逻辑假设“列表非空即有数据”那在 mock 环境永远测不出空列表 bug。我们的解决方案是在 mock 规则中显式声明“副作用”。例如// mock/user/list.ts export default [ { url: /api/user/list, method: get, response: (req) { // 读取全局状态当前登录用户的角色 const currentUserRole getMockState(currentUser).role; let users generateUsers(); // 生成 200 条基础数据 // 副作用1按角色过滤 if (currentUserRole analyst) { users users.filter(u u.role ! admin); } // 副作用2模拟网络延迟随机 100-800ms const delay Math.floor(Math.random() * 700) 100; await sleep(delay); // 副作用3模拟部分数据异常如 5% 概率返回空数组 if (Math.random() 0.05) { return { code: 0, data: [], total: 0 }; } return paginate(users, req.query); } } ];getMockState()是一个全局 mock 状态管理器它存储了当前会话的用户信息、token、甚至模拟的“服务器负载”。这样mock 规则就能基于真实上下文生成数据而不是闭门造车。我们甚至用它模拟“服务降级”当getMockState(serverLoad) 0.9时所有接口返回 503。3.4 断点数据的持久化与回放让“偶发问题”不再偶发联调中最头疼的是“刚才还好好的现在突然不行了”。断点拦截的价值不仅在于实时观察更在于可重现。我们要求所有断点数据自动保存为 JSON 文件按日期哈希命名并提供命令行回放工具# 保存最近 100 个断点到文件 npx mock-cli save --limit 100 --output ./mock-dumps/20240520.json # 回放指定 dump 文件重走所有请求可选修改参数 npx mock-cli replay ./mock-dumps/20240520.json --modify body.amount50000回放时工具会精确还原原始请求的 timing、header、body并将响应写入新文件。这让我们能把 QA 提交的“偶发问题”转化为可复现的测试用例在 CI 流程中用历史 dump 文件做回归测试向后端同事提供“问题发生时的完整请求快照”而不是一句“你接口有问题”。注意事项dump 文件默认不保存敏感字段如 password、token需在配置中显式声明sensitiveFields: [password, id_card]。这是安全红线绝不能省略。4. 实操过程从零搭建一个支持规则改写与断点拦截的 Mock 系统现在我们把前面所有设计落地为可执行的步骤。以一个典型的 vue3 ts 项目为例vite 构建目标是启动一个 mock 服务支持动态规则、条件断点、实时 UI 调试。整个过程不依赖任何商业工具全部基于开源生态总耗时约 45 分钟。4.1 环境准备与依赖安装首先确认你的项目已使用 vite。如果不是请先执行npm create vitelatest my-app -- --template vue-ts创建新项目。然后进入项目目录安装核心依赖npm install -D vite-plugin-mock mockjs path-to-regexp ajv types/ajvvite-plugin-mockVite 官方推荐的 mock 插件支持热更新mockjs提供丰富的随机数据生成能力姓名、地址、股票代码等path-to-regexp标准化 URL 匹配避免字符串操作陷阱ajvJSON Schema 校验确保 mock 规则与真实接口契约一致。提示不要安装mockjs的旧版如 1.x它不支持 ES Module。务必用mockjs^1.1.0或更高版本。接着在vite.config.ts中启用插件import { defineConfig } from vite; import vue from vitejs/plugin-vue; import { viteMockServe } from vite-plugin-mock; export default defineConfig({ plugins: [ vue(), viteMockServe({ supportTs: true, mockPath: mock, // mock 规则存放目录 localEnabled: true, // 开发环境启用 prodEnabled: false, // 生产环境禁用 injectCode: import { setupMockServer } from ./mock/index; setupMockServer(); , logger: true, }), ], });这里的关键配置是injectCode它会在每个页面加载时自动执行setupMockServer()确保 mock 服务全局可用无需在每个组件里手动引入。4.2 创建契约驱动的 Mock 规则骨架在项目根目录创建mock/文件夹结构如下mock/ ├── index.ts # 入口初始化 mock 服务 ├── router.config.ts # 流量调度规则 ├── utils/ # 工具函数 │ ├── state.ts # mock 全局状态管理 │ └── schema.ts # JSON Schema 校验封装 └── api/ # 具体接口规则 ├── user.ts ├── stock.ts └── order.ts先写mock/index.ts它是整个 mock 系统的“心脏”// mock/index.ts import { createApp } from vue; import { MockServer } from vite-plugin-mock; import { mockRoutes } from ./router.config; import { initMockState } from ./utils/state; // 初始化全局 mock 状态 initMockState(); // 创建 mock 服务实例 const mockServer new MockServer({ routes: mockRoutes, // 启用断点拦截 enableBreakpoint: true, // 断点 UI 端口 breakpointPort: 3001, }); // 导出供 injectCode 调用 export function setupMockServer() { mockServer.start(); }mock/router.config.ts是流量调度的核心我们按前文设计填充// mock/router.config.ts import { MockMethod } from vite-plugin-mock; import { generateUsers } from ./api/user; import { generateStockData } from ./api/stock; export const mockRoutes: MockMethod[] [ // 用户接口全 mock支持分页和角色过滤 { url: /api/user/list, method: get, response: ({ query }) { const page Number(query.page) || 1; const size Number(query.size) || 10; const users generateUsers(); // 模拟角色过滤从全局状态读取 const currentUserRole getMockState(currentUser).role; const filtered currentUserRole analyst ? users.filter(u u.role ! admin) : users; return { code: 0, data: filtered.slice((page - 1) * size, page * size), total: filtered.length, }; } }, // 行情接口代理到腾讯接口但加缓存控制 { url: https://quote.tencent.com/v1/stock/:symbol, method: get, response: async ({ params, headers }) { // 构建代理请求 const proxyUrl https://quote.tencent.com/v1/stock/${params.symbol}; const res await fetch(proxyUrl, { headers: { ...headers, Cache-Control: no-cache } }); return res.json(); } }, // 订单接口规则改写按 order_type 分支 { url: /api/order/create, method: post, response: ({ body }) { const order JSON.parse(body as string); if (order.order_type limit) { return { code: 0, order_id: ORD_${Date.now()} }; } else if (order.order_type market) { return Math.random() 0.3 ? { code: 4002, message: 市价单风控拒绝 } : { code: 0, order_id: ORD_${Date.now()} }; } return { code: 400, message: 不支持的订单类型 }; } } ];注意generateUsers()和generateStockData()是具体的 mock 数据生成函数放在mock/api/下。它们用 mockjs 生成符合业务语义的数据比如股票代码一定是SH600000或SZ000001格式而不是随机字符串。4.3 实现断点拦截 UI 与实时调试vite-plugin-mock默认提供一个简单的断点 UI但我们做了深度定制让它真正服务于金融场景。在mock/utils/state.ts中我们实现全局状态管理// mock/utils/state.ts type MockState { currentUser: { id: number; name: string; role: admin | trader | analyst }; serverLoad: number; // 0-1模拟服务器负载 lastError: string | null; }; const state: MockState { currentUser: { id: 1, name: 张三, role: trader }, serverLoad: 0.2, lastError: null, }; export function setMockState(key: keyof MockState, value: any) { state[key] value; } export function getMockStateT extends keyof MockState(key: T): MockState[T] { return state[key]; } export function initMockState() { // 从 localStorage 读取上次状态实现跨页面保持 const saved localStorage.getItem(mock-state); if (saved) { Object.assign(state, JSON.parse(saved)); } } // 保存状态到 localStorage window.addEventListener(beforeunload, () { localStorage.setItem(mock-state, JSON.stringify(state)); });这个状态管理器让所有 mock 规则能共享上下文比如切换用户角色后用户列表自动过滤行情请求自动添加X-User-Role: traderheader。断点 UI 的定制在mock/index.ts的mockServer.start()后追加// mock/index.ts续 mockServer.start(); // 启动断点 UI 服务 import express from express; const app express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 提供断点数据 API app.get(/api/breakpoints, (req, res) { const breakpoints mockServer.getBreakpoints(); // 获取所有断点 res.json(breakpoints.slice(-50)); // 只返回最近 50 条 }); // 提供断点回放 API app.post(/api/replay, (req, res) { const { dumpFile, modify } req.body; const result replayDump(dumpFile, modify); // 实现回放逻辑 res.json(result); }); // 静态资源服务 app.use(express.static(mock-ui)); // 存放 HTML/JS/CSS app.listen(3001, () { console.log(✅ Mock Debug UI running at http://localhost:3001); });mock-ui/目录下是一个简单的 Vue 应用用 ECharts 绘制请求耗时趋势图用表格展示请求详情并提供“复制 curl”、“重新发送”、“导出 dump”按钮。它不依赖任何构建工具纯 HTML JS启动零成本。4.4 在 Vue3 组件中实战联调现在我们用一个真实的交易下单组件来验证整个系统。创建src/views/TradeView.vuescript setup langts import { ref, onMounted } from vue; import { useStockQuote } from /hooks/useStockQuote; import { createOrder } from /api/order; const symbol ref(SH600000); const price ref(10.5); const amount ref(100); // 使用行情 hook它内部调用 mock 或真实腾讯接口 const { quote, loading, error } useStockQuote(symbol); // 提交订单 const handleSubmit async () { try { const res await createOrder({ symbol: symbol.value, price: price.value, amount: amount.value, order_type: limit, // 可切换为 market 测试风控 }); alert(下单成功订单号${res.order_id}); } catch (e: any) { alert(下单失败${e.message || 未知错误}); } }; /script template div classtrade-view h2股票交易/h2 div classquote-card v-ifquote pstrong{{ quote.symbol }}/strong {{ quote.name }}/p p最新价span classprice{{ quote.last_price }}/span/p p涨跌幅span :classquote.change_percent 0 ? up : down {{ quote.change_percent }}% /span/p /div form submit.preventhandleSubmit input v-modelsymbol placeholder股票代码 / input v-model.numberprice placeholder价格 typenumber step0.01 / input v-model.numberamount placeholder数量 typenumber / button typesubmit限价下单/button !-- 添加调试开关 -- button typebutton clicksetMockState(currentUser, { role: analyst }) 切换为分析师 /button /form /div /template关键点在于useStockQuotehook 的实现// src/hooks/useStockQuote.ts import { ref, onMounted } from vue; import { useRequest } from vue-request; export function useStockQuote(symbol: Refstring) { const quote refany(null); const loading ref(false); const error refstring | null(null); // 使用 vue-request它会自动处理 loading/error 状态 const { run, data, loading: reqLoading, error: reqError } useRequest( () fetch(/api/quote/${symbol.value}).then(r r.json()), { manual: true } ); onMounted(() { run(); }); // 响应式绑定 watch(data, (newVal) { quote.value newVal; }); watch(reqLoading, (newVal) { loading.value newVal; }); watch(reqError, (newVal) { error.value newVal?.message || null; }); return { quote, loading, error }; }当symbol.value为SH600000时fetch请求会命中mock/router.config.ts中的腾讯代理规则返回真实行情当symbol.value为TEST001时我们可以添加一条 mock 规则返回预设的测试数据。而“切换为分析师”按钮会调用setMockState改变全局角色从而影响用户列表等所有依赖角色的接口。4.5 联调验证与问题排查启动项目npm run dev访问http://localhost:3000同时打开http://localhost:3001的断点 UI。验证步骤在 TradeView 页面输入SH600000观察行情是否实时刷新应走腾讯代理点击“切换为分析师”再访问用户列表页确认只显示非 admin 用户提交限价单检查断点 UI 是否捕获到/api/order/create请求响应是否为code: 0修改订单类型为market提交 5 次检查是否约 1-2 次返回code: 4002在断点 UI 中找到一次4002请求点击“重新发送”确认前端弹窗显示“市价单风控拒绝”。实操心得第一次联调时90% 的问题出在CORS 配置和HTTPS 代理上。腾讯行情接口要求 HTTPS而本地 vite 是 HTTP。解决方案是在vite.config.ts中配置代理export default defineConfig({ server: { proxy: { /api/quote: { target: https://quote.tencent.com, changeOrigin: true, secure: false, // 允许不安全的 HTTPS rewrite: (path) path.replace(/^\/api\/quote/, ), } } } });这样前端请求/api/quote/SH600000vite 会代理到https://quote.tencent.com/SH600000绕过浏览器 CORS 限制。5. 常见问题与排查技巧实录在十几个项目的 Mock 系统落地过程中我们整理出一份高频问题速查表。这些问题不是文档里写的“可能遇到”而是每个团队都真实踩过的坑附带我们验证有效的解决方案。5.1 规则不生效为什么我的 mock 请求总是走到真实后端这是最常见问题根源几乎全是URL 匹配失败。排查顺序如下检查项如何验证解决方案1. 请求 URL 是否被重写打开 Chrome DevTools → Network → 点击请求 → 查看 Headers → “Request URL” 字段如果显示http://localhost:3000/api/user/list但 mock 规则写的是/api/user/list则匹配失败。需在规则中加 url: /api/user/list