dev 期请求 mock(registerApiMock)

后端 API 还没就绪,但你想先把页面、组件、交互做完?swift-web 提供 registerApiMock:在 dev 期声明「某个请求返回什么」,组件照常调真实 request(),等 API 好了,删掉 mock 就能联调。

这是一个轻量的 dev 期权宜工具,不是专业 mock 体系:

  • 何时用:新项目后端 API 未就绪、或想纯前端本地把 UI / UX 跑起来。
  • 何时别用:旧项目改造、后端 API 现成——不需要、别引入,registerApiMock 不调就完全不存在(dev-only,生产构建被删净)。
  • 边界:搞不定的个别场景,applier 自己在组件里临时写死也行。这工具不追求覆盖 100%。

一、原理:为什么拦在请求层、为什么不用第三方包

swift-web 的 request() 把请求体 AES-CBC 动态密钥加密 + gzip 后发出,响应也是密文。registerApiMock 拦在 createApiFetch 内部、加密之前:命中就返回你给的明文 data、未命中就 passthrough 走真实后端。

这就是为什么业界那些网络层 mock 包(MSW / vite-plugin-mock / MirageJS)在这套架构里不好用:

  • 它们都拦在「网络线缆」那一层,那里的请求 / 响应是密文——要 mock 就得在 handler 里复刻一遍动态密钥派生 + 加密,又繁又脆。
  • registerApiMock 拦在明文层,handler 直接返回普通对象,天然绕开加密。
  • 额外坑:MSW 在浏览器靠 Service Worker,会和 swift-web 的 PWA(workbox)service worker 冲突。

所以这里自带一个薄机制是架构决定的,不是重复造轮子。

二、快速上手

把所有 mock 集中写在一个 dev-only 插件里。推荐用 universal plugin(不带 .client 后缀)——dev 期 SSR + CSR 双端拦截,ISR / SSR 页在 setup 顶层 await useAsyncData(() => request(...)) 也能直出 mock 数据(SSR HTML 完整,页面从第一天就是接后端的最终形态):

// app/plugins/dev-mock.ts
export default defineNuxtPlugin(() => {
  registerApiMock('/movie/home', () => ({
    list: [
      { id: 1, title: 'mock 影片一', cover: '/mock/1.jpg' },
      { id: 2, title: 'mock 影片二', cover: '/mock/2.jpg' },
    ],
  }))

  registerApiMock('/movie/detail', ({ body }) => ({
    id: (body as { id: number }).id,
    title: 'mock 详情',
  }))
})

组件 / composable 照常调真实 request(),完全不用知道有没有 mock:

const home = await request('/movie/home')

dev 期命中 mock 时,控制台会打一行 [mock] SSR POST /movie/home(或 CSR,标注拦截发生在哪端),方便你确认拦截生效。SSR 命中的日志出现在 dev server 终端,CSR 命中的在浏览器控制台。

文件名仍写成 dev-mock.client.ts 也可以——那样退回只拦 client 端(SSR 期照旧打真实后端),适合「后端已就绪、只想在浏览器里临时替换个别接口」的场景。

整合 API 时:把 app/plugins/dev-mock.ts 删掉就行——组件代码一行不用动,请求直接打到真后端。这就是「先做完 UI、后联调」的干净切口。别忘了同一步删掉 request.mock 配置段关停内置假数据——见 §七 关闭与移除 mock。

framework 自带服务零配置(内置预设)

request.mock.enabled: true 时,framework 还会自动 mock 它自己的两个服务,连写都不用写:

  • 登录 /login/* → 返回内置假用户,能直接进登录态;
  • /system/info → 注入默认 skeleton(SSR + CSR 都生效)。

覆盖:用 registerApiMock('/login/username', …) 覆盖内置登录;用 swift.config.json 的 request.mock.systemInfo 覆盖 system/info 字段。只想要机制、不要内置假数据时设 request.mock.builtins: false。

⚠️ builtins 内置假数据只在 enabled 显式写 true 时激活 —— enabled 缺省时 dev 期虽然默认开(mock 机制可用、registerApiMock 照常拦截),但内置预设不会激活,防止劫持项目自己的 dev mock 数据。这是全 ModuleOptions 中唯一「来源敏感」的开关。

三、API 参考

registerApiMock(matcher, handler, options?)

注册一条 mock,返回一个注销句柄(调用即移除这条)。

matcher(三态,决定哪个请求被拦)

形态例子说明
精确字符串'/login/username'按 path 精确匹配(忽略 url 上的 query 串)
对象{ path: '/order/submit', method: 'POST' }可加 method / query 精细匹配;省略 method 则任意方法命中
正则/^\/movie\/\d+$/测 path

优先级:精确字符串 > 对象 > 正则;同一档先注册的先命中。

handler

(ctx: { url: string; method: string; body: unknown; query: Record<string, string> }) => data | Promise<data>

返回明文 data——也就是 request<T>() 里那个 T(框架内部已经帮你解包了 { status, data } 这层,你只管返回 data)。可以是 async。

options

选项作用
delay模拟网络延迟(ms),用来验证 loading 态
status: 'n'模拟业务失败,走和真实后端 status:'n' 一样的失败分支(抛错)
errorCode配合 status:'n';填 2002 会触发框架的「静默重登 + 重试一次」逻辑
errorstatus:'n' 时的错误文案,默认「请求失败」
once命中一次后自动注销
registerApiMock({ path: '/order/submit', method: 'POST' }, () => ({ orderId: 'x' }), { delay: 300 })
registerApiMock('/pay/confirm', () => ({}), { status: 'n', error: '余额不足' })

clearApiMocks()

清空全部已注册的 mock(写测试 / 重置时用)。

整个 mock 层的开关(request.mock.enabled / builtins)+ 创建期 / 已有项目两种关法,统一见下方 §七 关闭与移除 mock。

四、登录怎么 mock(重点)

没后端时最常见的需求是「假装登录成功」。别去 monkeypatch userStore.loginByUsername——那要懂 AES cookie、要重写 store 内部,又脆又每个项目重造。

正确做法:mock 登录接口返回一个假用户即可。因为拦截在加密前、返回明文 data,框架后续的 setInfo / 写 cookie / isLoggedIn / 路由守卫全走真实逻辑:

// app/plugins/dev-mock.ts
registerApiMock('/login/username', ({ body }) => ({
  token: 'mock-token',
  user_id: '1',
  nickname: '云端漫步',
  avatar: '/mock/avatar.jpg',
}))

这样在登录页 / 登录弹窗输入任意账号密码,就能进到真实的「已登录」态,/mine 守卫放行、退出清态也正常。

五、安全模型(为什么不会泄漏到生产)

假登录如果进了生产 = 「任意账号密码即登录」后门,是灾难。registerApiMock 有三重兜底:

  1. dev-only:拦截逻辑包在 import.meta.env.DEV 里,生产构建时被 Vite DCE(dead-code elimination)整段删除——代码根本不进生产 bundle。
  2. 双端同一道门:SSR 端同样只在 dev 生效——生产 server 构建里 import.meta.env.DEV 是静态 false(构建期消除;即便个别构建器保留代码,注册与查表也都是运行时 no-op)。
  3. 总开关:request.mock.enabled 可显式关。

你照常写 mock 即可,不需要自己加「靠运行时开关挡的后门」那种旧写法。

SSR 说明:mock 是否覆盖 SSR 取决于插件注册在哪端——universal plugin(dev-mock.ts)双端都拦;.client.ts 只拦 client。once 计数在 SSR 端是 dev server 进程级的(任一请求命中一次即全局摘除)。system/info 例外:它由内置预设在 SSR 期直接注入 skeleton(零请求、不受插件时序影响,见上「内置预设」),无需你 mock。

六、和「数据层 composable」的关系

另一种常见的 dev mock 写法是:每个功能写一个 useXxx composable,里面直接返回静态数据,等 API 好了再把函数体换成真实 request()。这种「数据层 seam」也完全可用,和 registerApiMock 互补:

  • registerApiMock:组件从第一天就调真实 request(),mock 集中在一处、整合即删;登录 / system-info 这类走框架机器的请求只能用它。
  • 数据层 composable:结构未知、想强类型的业务数据,写在 composable 里也清晰。

两者按手感选,不冲突。

七、关闭与移除 mock

接入真后端:删 mock 配置是 checklist 动作,不是可选项

全局 mock 是有生命周期的:脚手架期(后端不存在 / 未就绪)request.mock.enabled: true + 内置假数据完全正当;从接入真后端那一刻起,删掉整个 request.mock 配置段——把它当作「接入真后端 checklist」的固定一条,动作固定、不靠人记得。

  • 为什么是「删配置段」而不是改 enabled: false:显式 false 不只关内置假数据,还会把 registerApiMock 的消费层整个关死——你的单点 mock 和依赖它的测试 fixture 一起失效。而删掉配置段后恰好是想要的状态:内置假数据停(它只在显式 enabled: true 时激活),registerApiMock 机制保留(缺省 dev 默认开,无注册 = 无副作用)。
  • 之后仍需 mock 的个别端点:只用 registerApiMock 单点注册,且每个注册项挂 issue 标注「临时、待后端就绪即删」——单点是显式例外(有清单、一眼可数),全局开关是隐式掩盖(什么都兜、什么都看不见)。
  • 关掉后,dev 从此依赖后端可达。后端连不上的正解是修可达性(隧道 / .env / 网络)或临时单点 mock + 挂 issue,不是把全局开关再打开。原先依赖内置假数据的测试要自补两类 app-init 状态:登录(fixture 注册后补跑 autoLogin,走真实 setInfo)与 system/info(测试里 setInfo 复刻)。
  • 为什么这么严:全局 mock 与真后端并存时,「页面能跑」在「API 正常」与「API 坏了被 mock 兜住」两种假设下看起来一模一样——协议漂移、后端异常都暴露不出来;内置假数据还会顶掉真实数据造出假状态。真实案例:某站接入真后端后 enabled: true 没删,/system/info 的内置 skeleton(刻意最小、没有 menus)被直注进 store,频道页因 menus 恒空直接 500——后端端点齐全、框架无缺陷,问题一直藏在 mock 层,接入后才炸出。

五档关法速查

从「项目级开关」到「彻底删干净」分五档,按需要选:

想做什么怎么做
新项目根本不想要 mock(已有后端 / 旧项目改造)sw create 时「启用 dev mock 数据?」选否 —— CLI 直接写 request.mock.enabled: false 并删掉 starter 自带的业务 mock 预设文件
接入真后端(联调开始)删掉整个 request.mock 配置段(见上节;别用 enabled: false——会连 registerApiMock 单点通道一起关死)
已有项目临时全关(dev 期想直接打真后端)sw config set framework.request.mock.enabled false(或手改 swift.config.json)。注意:这会连内置的 /login/*、/system/info 假数据、以及你自己注册的单点 mock 一起关,请求全部直连真后端
只关内置假数据、保留 registerApiMock 机制request.mock.builtins: false —— 机制照常拦你自己注册的 mock,只是不再自带 login / system-info
API 好了,彻底移除业务 mock删掉 app/plugins/dev-mock.ts(或对应的 mock 注册)。组件 / composable 里的 request() 调用一行不用改,直接打真后端

request.mock.enabled 默认随 import.meta.env.DEV(dev 开 / prod 关,且 prod 还会被 DCE 整段删净),所以大多数情况你不用主动关——上表是「想提前关 / 改造旧项目 / 进入联调」时才需要的开关。

整合联调时,删掉 mock 文件后还有一步:核对真实响应结构与你 mock 时假设的字段是否一致,按需调整字段映射。因为后端是固定框架、各项目 API 大体同构,这步通常只是微调字段,不会推倒重来。

相关链接