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,回傳一個註銷 handle(呼叫即移除這條)。

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 大體同構,這步通常只是微調欄位,不會推倒重來。

相關連結