ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

后端接口没就绪?前端用Mock与契约治理实现自救

后端接口没就绪?前端用Mock与契约治理实现自救

做前端的兄弟应该都有过这种经历:排期压到眼前,后端接口却迟迟不来,你能拿到的只有一句话“接口下周给你”,或者一封只有路径没有字段的文档。这几年 Vibe Coding 火起来之后,很多前端同学开始用 AI 助手批量生成对接代码,但 AI 并不会替你关心后端到底给不给力——你拿到的接口契约是残缺的,AI 生成再多的代码也只是在沙滩上盖楼。我在这两年的项目里总结了一套“后端不给力时,前端如何自救”的兜底方案,不靠关系、不靠加班,靠的是把接口对接这件事从“等后端”变成“先自己把闭环跑通”。这篇文章适合正在做前后端分离项目、又被接口进度卡住的前端开发,也适合刚接触 Vibe Coding、对“AI 生成代码之后怎么收场”还没底的同学。

1. Vibe Coding 时代,接口对接的困境到底是什么

1.1 接口对接的本质是契约管理

很多人以为前后端接口对接的难点在“写请求代码”,其实真不是。十个接口里有八个的代码量不超过二十行,真正折磨人的是字段对不上、路径变更、格式不统一、状态码乱写。接口对接的本质是契约管理,也就是说,前端和后端必须对“请求长什么样、响应长什么样、错误长什么样”这三个问题达成一致,才能各干各的。

这个契约在过去通常靠 Word 文档、Markdown 笔记或者后端在人嘴里“说”出来。问题在于口头契约不严谨,文档更新不及时,一旦后端改了字段没通知前端,联调就是一场灾难。后端不给力的时候,前端面对的往往不是“没有接口”,而是“接口契约根本不存在”,或者“存在但已经过期”。

Vibe Coding 的介入让这个问题变得更加隐蔽。你用 AI 生成了一个fetchUser()函数,AI 是根据你嘴里描述的“用户信息”来推断字段的,后端实际返回的是userName还是nickName,AI 根本不知道,因为你也不知道。于是你拿到了一个看起来能跑、一跑就报错的函数,排查半天发现不是代码逻辑问题,而是契约问题。

1.2 Vibe Coding 把哪些“坑”放大了

Vibe Coding 的核心是“用自然语言描述意图,让 AI 生成代码”,听起来很爽,但它有一个致命前提:AI 生成的内容是基于你给出的上下文,而这个上下文里最关键的接口契约恰恰是一个黑盒。

我见过不止一个前端同学把接口对接任务直接丢给 AI,说“帮我把登录接口对接一下”,AI 生成了一段看起来合情合理的 axios 代码,路径是/login,参数是{ username, password },响应是{ token, userInfo }。结果真实后端路径是/auth/sign/in,参数是{ account, pwd },响应是{ accessToken, profile }。这不是 AI 的问题,是上下文缺失导致的问题。

Vibe Coding 放大的坑有三类。第一,AI 会“虚构契约”,它不存在的东西,编得有理有据。第二,AI 生成的代码缺少防御性判断,因为后端返回的数据结构它没见过真实的,它没法替你处理data为空、code不为 0、字段缺失这些情况。第三,当后端接口频繁变动时,AI 生成的一堆散落在组件里的请求代码会成为你重构的噩梦,因为每个组件都在直接调 axios,没有中间层。

所以,Vibe Coding 下前后端对接的真正解决方案,不是换更强的 AI,而是先把“契约”这件事从后端手里抢回来。下面我讲的兜底方案,核心思路就一句话:前端自己先定义一套可执行的假契约,等后端真实接口就绪后,只改一处配置即可完成切换。

2. 兜底第一层:先把后端“虚拟化”,让前端立刻跑起来

2.1 json-server:五分钟拥有一个“假后端”

最朴素也最有效的兜底方案是本地起一个假后端。json-server是我用得最多的工具,它只需要一个 JSON 文件就能模拟 RESTful API,支持分页、过滤、排序、增删改查,对前端联调和自测来说完全够了。

安装和启动非常简单:

npm install -g json-server mkdir mock && cd mock echo '{"users": [{"id": 1, "name": "张三", "role": "admin"}], "orders": [{"id": 100, "amount": 299}]}' > db.json json-server --watch db.json --port 3000

跑起来之后,http://localhost:3000/users就能返回用户列表,GET /users/1返回单个用户,POST /users能新增数据。它甚至支持_page和_limit参数来模拟分页,这对联调列表页很有用。

我通常会把db.json放在项目仓库的mock/目录下,并写一个npm run mock脚本。这样前端同学 clone 项目后一条命令就能启动假后端,不依赖任何人的开发进度。注意,json-server默认是监听0.0.0.0,如果你在团队中共享这个 mock 服务,可以加--host 0.0.0.0,但更推荐只在本地用,因为它是动态可写的,别人改数据会影响你的调试。

有人会问:后端看到我们自己 mock,会不会觉得没必要再提供接口了?我的经验是:不会,反而会提高效率。因为前端 mock 出来的数据结构就是你要的契约,后端直接照着这个数据结构实现,双方沟通成本大幅降低。

2.2 MSW:在浏览器端拦截请求,连代理都不用配

json-server适合启动一个独立服务,但有时候你不想开两个终端,也不想配置跨域代理,这时候可以用 MSW(Mock Service Worker)。

MSW 的原理是在浏览器里注册一个 Service Worker,拦截所有网络请求,然后返回你预设的 mock 数据。前端代码不用改任何一个 URL,请求在浏览器层面就被“截胡”了。

一个最简示例:

import { setupWorker, rest } from 'msw' const worker = setupWorker( rest.get('/api/user', (req, res, ctx) => { return res(ctx.json({ id: 1, name: '张三', role: 'admin' })) }) ) worker.start()

然后你只需要在项目的入口文件里根据环境变量决定是否启动 mock。比如:

if (import.meta.env.DEV && import.meta.env.VITE_USE_MOCK === 'true') { await worker.start() }

这样你在开发环境跑前端时,所有/api/*请求都走 MSW,等后端接口真正可用,把VITE_USE_MOCK关掉,请求自动打到真实后端。MSW 最大的优点是它让前端代码永远写的是真实接口路径,不会被 mock 带偏。

2.3 让 Mock 数据“假得像真的一样”

很多前端 mock 失败的原因是数据太假。比如后端返回的字段是userName,你 mock 成name;后端返回code是'0000'表示成功,你 mock 成'200'。这样等真实接口一上,前端全是 bug。

我的原则是:mock 数据必须严格遵循后端提供的文档,如果没有文档,就参考同类项目、数据库设计、产品需求先定一份。Mock 的目标不是让页面能看,而是让前端跑通所有业务分支,包括成功、失败、空数据、异常状态。

具体来说,一个像样的 mock 数据要覆盖四种情况:

  • 正常数据:字段类型、嵌套结构与真实接口一致。
  • 空数据:list为空数组、data为null,页面不能崩。
  • 失败场景:code为非成功值、message有值,前端要能弹提示。
  • 网络异常:请求超时、500,前端要有统一的错误处理。

这些边界情况,后端在开发初期很少会主动提供,但前端必须在 mock 阶段就考虑到。否则到联调时,接口一返异常,你才发现所有页面都没做错误处理,加班就开始了。

3. 兜底第二层:让接口文档先于代码,比催后端更靠谱

3.1 OpenAPI 规范先行,前端才有章可循

Mock 能解决“前端能不能动”的问题,但解决不了“最终对接会不会崩”的问题。真正的保险是先把接口契约从口头变成可执行的规范。这个领域的事实标准就是 OpenAPI(也就是大家常说的 Swagger 规范)。

OpenAPI 是一个 YAML 或 JSON 文件,描述接口路径、请求参数、响应模型、鉴权方式等。它最大的特点是机器可读,可以校验请求、生成类型、生成文档、生成 mock server。

你不需要等后端主动给你 OpenAPI 文件,前端可以自己先写一版“草稿”。比如后端说“登录接口路径是 POST /login,传用户名密码,返回 token”,你就写进 OpenAPI:

openapi: 3.0.0 info: title: 用户服务契约草稿 version: 0.1.0 paths: /login: post: operationId: login requestBody: content: application/json: schema: type: object properties: account: type: string pwd: type: string required: [account, pwd] responses: '200': content: application/json: schema: type: object properties: accessToken: type: string profile: type: object

这份文件同时是前端 mock、类型生成、后端实现的事实依据。后端看到这么明确的定义,通常会照着实现,而不是再自己造一套。

3.2 从接口文档自动生成类型和请求逻辑

有了 OpenAPI 文件之后,前端可以借助工具直接生成 TypeScript 类型,这样类型定义永远不会和接口文档脱节。我用的比较多的是openapi-typescript:

npm install -D openapi-typescript npx openapi-typescript ./schema.yaml -o ./src/types/api.ts

生成的api.ts里会有完整的接口响应类型,前端组件的 props 直接引用这些类型,Vibe Coding 的时候把类型文件喂给 AI,AI 生成代码的质量会明显上一个台阶。因为 AI 不再靠猜,它有了确切的字段定义。

更进一步,可以用openapi-generator生成请求客户端,但我个人更倾向于只生成类型,请求层自己封装,原因是生成的客户端代码往往比较重,不够灵活。在实际项目中,类型是契约的核心,请求函数反而会经常变化,不如自己控制。

3.3 怎么“反向推动”后端把文档补齐

很多前端不敢自己先写契约,担心越界,觉得接口是后端的地盘。我的经验是:不需要抢地盘,只需要把“没有契约”的风险放在桌面上谈。

具体做法是,在项目启动的第一周就拉着后端开一个简单的“契约对齐会”,后端只要口头描述接口路径、请求参数、响应字段,前端当场记录成 OpenAPI 草稿,并说“我先按这个 mock,你看到有偏差随时告诉我”。这个姿态是“我为了配合你”,而不是“我不信任你”。

如果后端不配合,优先级退而求其次:让后端至少提供一份 Postman 的 export 文件,或者 Swagger UI 的界面截图。这些材料虽然不如 OpenAPI 完整,但至少能支撑前端把 mock 字段对齐。最糟糕的情况是啥都没有,那就只能回到我们第一层讲的 json-server,你按自己的理解 mock,并且预留好修改空间。

4. 兜底第三层:请求层封装与运行期兜底

4.1 适配器模式:让接口路径变更只改一处

前端与后端对接最容易发生的事:后端路径改了、字段名变了、返回结构从data挪到了result.data。如果你每个组件都在直接写axios.get('/old/path'),改动范围就是全局搜索加逐个修改,一天就废了。

所以请求层一定要做适配器封装。思路很简单:视图层永远不直接调 axios,而是调用你封装好的业务 API 函数,比如:

// api/user.ts import { http } from './http' export interface UserProfile { id: number userName: string role: 'admin' | 'user' } export function fetchUser(id: number) { return http.get<UserProfile>(`/v1/users/${id}`) }

组件里只关心fetchUser(1),至于后端是/v1/users/1还是/api/private/user/getInfo,都由api/user.ts里面的路径决定。后端路径一变,你只需要改这个文件。

字段变更也有应对手段——响应拦截器里做归一化。比如后端把用户接口改成了驼峰userName,而你的前端已经全面使用user_name,可以在响应拦截器统一转换。但这个策略慎用,因为过度“翻译”会让代码变得不透明。我的建议是:字段名尽量与后端保持一致,如果后端改字段,那就全局改类型定义和引用点,因为这是契约变化,不属于被动的“兜底”范畴。

4.2 跨域与代理配置,别再被环境问题卡住

前后端分离项目最容易卡住开发环境的永远是跨域。后端不给力的时候,跨域会显得格外明显:你在 localhost:5173 跑前端,后端接口在 localhost:8080,直接 fetch 就会 CORS 报错。

解决方式是用 Vite 的开发服务代理(webpack 时代是proxy配置在webpack-dev-server)。在 Vite 项目里,你的前端代码依然写/api/user,但 Vite 会把请求转发给真实后端:

// vite.config.ts import { defineConfig } from 'vite' export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })

这个配置同时区分了“mock 模式”和“联调模式”:当 mock 开着,请求走 MSW;当 mock 关掉,请求经 Vite 代理转发到后端的http://localhost:8080。这样前端不需要改任何业务代码。

注意一个细节:changeOrigin: true一定要开,否则后端通过请求头里的Origin判断来源时可能误拦。另一个常见问题是生产环境的跨域,配置在 Nginx 层,但那是后端或运维的事,前端开发阶段不用掺和。

4.3 重复提交与幂等的前端防线

后端不给力还有一个典型体现:接口没做幂等处理,前端多请求一次就多产生一条脏数据。这里前端不能把所有希望寄托在后端,至少在交互层要有防线。

按钮重复提交是最高频的场景。用户手抖点了两次“提交订单”,如果前端不做处理,后端即使有幂等标志位也来不及判断。前端兜底方案很朴素:提交后立刻把按钮置为 loading,并禁用点击事件;同时在请求层维护一个“提交中”的标志,防止并发。

let submitting = false async function handleSubmit() { if (submitting) return submitting = true try { await postOrder(data) } finally { submitting = false } }

更进一步的做法是给每个高风险请求生成一个requestId(UUID),放在请求体里,后端拿这个requestId做幂等判断。这在后端不配合的情况下,前端可以先在请求层生成并透传字段,等后端实现幂等时数据已经接好,不用再改前端。

还有一个容易被忽略的点:AI 生成代码时经常会忽略重复提交防护。因为 AI 看到的只是“提交订单”这一个需求,它不知道你项目里已经有统一的按钮防抖机制。你在让 AI 写代码前,应该先把“提交中禁用按钮”这个约束写进提示词里,否则生成的代码大概率是裸奔的。

5. 实战避坑:前后端对接时的经验与教训

5.1 常见问题速查表

我在多个项目里总结了一些高频问题,逐条列出来,覆盖范围比较杂,但都是真实踩过的。

问题常见原因兜底/解决方案
接口路径后端改了,前端不知道契约不透明,变更日志缺失请求层统一封装,路径集中在 api 目录
字段名大小写不一致后端返回 camelCase,前端用的 snake_case响应拦截器做一次性映射,后续保持统一
联调时接口返回 401 频繁登录鉴权流程没走通先在 mock 阶段加入 Token 刷新逻辑,不要只 mock 成功场景
跨域报错连本地代理都没加Vite/Webpack 代理未配置配置 changeOrigin 代理,不要靠后端开 CORS
分页字段不一致后端返回{list},前端要{rows}请求层适配,分页响应统一转成{list, total}
后端返回 success 用不同字段有的接口返回code: 0,有的返回success: true响应拦截器规整成{ code, data, message }
接口一直 pending 不返回后端服务挂了或接口卡死请求层设置超时时间,例如 8 秒统一报错
字段类型后端突然变成字符串数据库字段迁移导致响应结构变化类型检查和前端运行时校验,至少提示“数据格式异常”
按钮点击多次提交脏数据后端没幂等,前端没防重复提交态禁用 + requestId 透传
AI 生成的接口代码没有错误处理提示词没要求在接口层统一 try-catch,让组件层不用重复写

5.2 几句掏心窝的实操心得

最后我还是要说一个核心观点:Vibe Coding 可以把前端写代码的效率拉满,但前后端对接最终拼的还是契约管理。很多前端把大量时间花在“AI 生成代码—报错—让 AI 修”这个循环里,却不愿意花半天时间把接口文档、类型定义、mock 数据、请求封装体系统一起来。工具再强,也补不上契约缺失的洞。

我个人在工作中会把“后端不给力的兜底”分成三步来推进。第一步,本地 mock 先让功能闭环跑起来;第二步,用 OpenAPI 把契约版本化,前端类型和 mock 数据都由它驱动;第三步,请求层做好适配和后端同学对齐,让真实接口可以像切开关一样随时替换。这三步做完,你后面遇到后端接口变动、字段修改、服务挂掉,都能淡定应对,因为整个前端架构从一开始就为“后端可能不给力”预留了解耦空间。

最后再分享一个小技巧:如果你用 Vibe Coding 生成了一段对接代码,先别急着粘贴进项目。把这段代码里涉及接口路径、请求参数、响应字段的部分抽出来,和你的 OpenAPI 规范或 mock 数据对照一遍,确认没有“AI 虚构字段”,再放进正式文件。这个过程只需要一分钟,但能帮你躲掉很多联调期的无意义加班。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进