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 会触发框架的「静默重登 + 重试一次」逻辑 |
error | status:'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 有三重兜底:
- dev-only:拦截逻辑包在
import.meta.env.DEV里,生产构建时被 Vite DCE(dead-code elimination)整段删除——代码根本不进生产 bundle。 - 双端同一道门:SSR 端同样只在 dev 生效——生产 server 构建里
import.meta.env.DEV是静态false(构建期消除;即便个别构建器保留代码,注册与查表也都是运行时 no-op)。 - 总开关:
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 大体同构,这步通常只是微调字段,不会推倒重来。
相关链接
- Module Options ·
request.mock——enabled/builtins/systemInfo配置字段速查 - 快速上手 · CLI 命令参考 ——
sw create「启用 dev mock 数据?」与sw config set的完整用法 - 自动登录 + Token 重登链路 —— mock 登录接口后,框架真实跑的就是这条链路(
setInfo/ 写 cookie / 守卫) - 客户端加密的安全边界 —— 为什么请求体是密文、mock 为何要拦在「明文层」