一个前端团队推出了一层仅在开发环境下运行的类型安全 API 模拟层。通过使用 Axios 拦截器和 Vite 的 tree-shaking,生产环境的构建包保持不变。工程师在等待后端接口完成时,可以使用常规模式获取数据,然后只需切换一个环境标志即可调用真实的 API。

团队为何需要更好的模拟方式

当后端路由尚未完成时,前端开发人员会遇到瓶颈。快速修复的方法——在组件内部硬编码响应,或者在 UI 中随处散布 if (process.env.NODE_ENV === 'development') 代码块——虽然能让应用继续运行,但会留下技术债。这些模拟对象会成为组件逻辑的一部分,增加将虚假数据发布到生产环境的风险,并使代码更难阅读和测试。

团队希望将所有模拟逻辑移出组件树,强制执行前后端之间的契约,并确保生产构建中不会包含任何多余的内容。

团队遵循的三步流程

  1. 契约会议 – 前端和后端工程师坐下来,列出每个请求及其 URL、方法和预期的 Payload。
  2. 类型化契约 – 他们将列表转换为 TypeScript 接口,该接口成为请求和响应结构的单一事实来源。
  3. 拦截器配置 – 一个 Axios 拦截器会检查每一个发出的请求。如果 URL 匹配已注册的模拟,拦截器将返回模拟数据;否则,请求将发送到真实的服务器。

由于拦截器是模拟逻辑存在的唯一地方,组件代码保持不变。开发人员可以继续使用他们正常的获取数据 Hook(例如 useQuery),而无需添加任何条件逻辑。

如何避免生产环境体积膨胀

团队设置了三层保障,让 Rollup(Vite 使用的打包工具)在进行生产构建时能够完全剔除模拟代码:

  • 在生产构建中,import.meta.env.DEV 解析为 false,因此整个拦截器模块在 tree-shaking 过程中会消失。
  • 运行单元测试时,MODE 变量会被设置为非 test 的值,从而将仅限测试的代码隔离开来。
  • 一个自定义标志 VITE_ENABLE_MSW 默认为 false,必须显式开启才能激活模拟。

当这三个条件都为 false 时,模拟注册表永远不会进入最终的构建包中。

组织模拟文件

仓库遵循以功能为中心的布局:

  • interfaces/ – 存放根据契约会议生成的 TypeScript 定义。
  • scenarios.ts – 包含每个端点成功响应和错误情况的具体示例。
  • devHandlers.ts – 作为中央注册表,将 URL 映射到场景数据,并将拦截器接入 Axios。

一个小型脚手架脚本可以自动生成这些文件:只需提供一个 URL 和匹配的接口,它就会创建存根文件并注册模拟。该脚本位于生产代码路径之外,因此不会影响构建包的大小。

团队获得的收益

  • 组件内零模拟 – 所有虚假数据都存在于专用层中,保持 UI 代码的纯净。
  • 端到端类型安全 – 模拟数据符合与真实响应相同的 TypeScript 接口,因此不匹配的问题可以在编译时被发现。
  • 无生产环境负担 – Tree-shaking 会完全移除拦截器和模拟数据,使构建包大小保持不变。
  • 开发与测试共享场景 – 相同的模拟定义同时驱动本地开发和自动化测试,减少了重复工作。

权衡与局限

这种方法并不能取代真实的后端。如果模拟契约与真实 API 发生偏离,开发人员只有在切换环境标志后才会发现这种不匹配。

后续关注点

  • 工具集成
  • 更广泛的采用
  • 性能监控

结论很明确:将模拟逻辑移入一个受环境控制的类型化层,可以让前端团队保持组件纯净、实现类型安全,并在发布生产构建时不会携带隐藏的模拟负载。维护一份共享契约是获得更顺畅的开发工作流和更整洁代码库的代价。