request
framework 的統一加密請求入口。業務程式碼一律用 request()(auto-import),它把動態金鑰加密、token / deviceId 注入、登入續期、SSR/CSR 分流全部封裝好;createApiFetch 是其底層工廠,由 framework 插件實例化後注入為 $request,業務一般不直接使用。
運作原理
createApiFetch在 framework 插件中實例化(注入getToken/getDeviceId/onRelogin),request()是它的全域薄封裝。- 每次請求產生獨立 requestId(UUID v4),同時用於:請求標頭與伺服器端對齊、aes-dynamic 的動態金鑰派生(HMAC-SHA256)、除錯日誌配對。
- 請求體統一包裝為
{ data, token, deviceId },gzip + AES-CBC 加密後以text/plainPOST 發送;回應體經decodeResponse解碼(解密;舊後端會把部分業務失敗以明文 JSON 回傳,明文失敗信封同樣放行)、再按{ status, data, error, errorCode }解包,成功時回傳其中的data;業務失敗(status: 'n')拋ApiError(message取信封error,並附著errorCode供結構化分支)。 - SSR / CSR 分流:SSR 直連後端(
baseURL = apiBase + apiPrefix)並透傳真實 IP(cf-connecting-ip / x-forwarded-for / x-real-ip)與 user-agent;CSR 走 routeRules 代理(baseURL = apiPrefix)。 - auth-ready 門控:client 端
request()自動await useAuthReady(),確保請求不會在 device + token 身分建立前發出(避免首屏 401 風暴)。 - token 自動續期:後端回傳
errorCode === 2002(token 失效)時觸發onRelogin靜默重登,重試一次;再失敗拋ApiError('登录失效,请刷新页面重试', 2002)。 - i18n 語言標頭從 cookie
i18n_redirected讀取,失敗兜底'zh-cn'。
API
| 匯出 | 簽名 | 說明 |
|---|---|---|
request | <T = unknown>(url: string, fetchOptions?: Record<string, unknown>) => Promise<T> | 業務標準入口;預設 method: 'POST',fetchOptions.body 為業務參數物件 |
createApiFetch | (options: ApiFetchOptions) => apiFetch | 底層工廠;ApiFetchOptions = { getToken, getDeviceId, onRelogin },framework 插件內部使用 |
ApiError | class ApiError extends Error { errorCode?: number } | 業務失敗拋錯載體;errorCode 來自協議信封(如 2002 token 失效),dev mock 的 errorCode option 同口徑附著 |
全域請求設定(逾時 / 公共標頭)走 swift.config.json 的 request 區塊(timeout 預設 20s),per-request 可用 fetchOptions.timeout / fetchOptions.headers 覆蓋。
使用範例
// 標準 POST(body 即業務參數)
const list = await request<Article[]>('/article/list', { body: { page: 1 } })
// POST 以外的方法
await request('/user/logout', { method: 'GET' })
// 失敗處理:request 拋 Error,必須 try/catch 給使用者回饋
async function onSubmit() {
try {
await request('/order/submit', { body: form })
useToast().success('提交成功')
} catch (e) {
useToast().error(e instanceof Error ? e.message : '提交失敗,請重試')
}
}
// 按 errorCode 結構化分支(別按 message 文案匹配——文案一改全崩)
try {
detail = await request('/movie/detail', { body: { id } })
} catch (e) {
if (e instanceof ApiError && e.errorCode === 4002) {
throw createError({ statusCode: 404 }) // 無效 / 下架內容 → 404
}
throw e
}
注意事項
業務一律用 request(),別用 frameworkInternalRequest(REQ-001,blocker)
framework 還匯出一個無 auth-ready 門控的內部版本 frameworkInternalRequest,專供 framework 自身的登入流程(autoLogin 等)避免死鎖。業務誤用會在身分未就緒時發請求 → 拿不到 deviceId / token,後端按匿名處理。
失敗拋 ApiError,必須 try/catch(REQ-002,warning)
後端回傳 status: 'n' 或網路故障時 request() 拋錯而不是回傳 { error }。不 catch → 未捕獲 promise rejection,後續成功提示等邏輯靜默不執行,使用者點了沒反應。業務失敗拋的是 ApiError(Error 子類,既有 instanceof Error / e.message 消費不變)——需要按錯誤類型分支時用 e.errorCode,不要按 message 文案匹配。
SSR 期呼叫需要絕對 apiBase(REQ-003,blocker)
SSR 分支直連後端,runtimeConfig.public.apiBase 必須是絕對位址(如 https://api.example.com)。為空時 framework fail-fast 拋出明確的「apiBase 未設定」錯誤(而非晦澀的 ERR_INVALID_URL)。沒有外部 SSR 後端的站點不應在 SSR 期呼叫 request() —— 把資料取得放 onMounted 走 CSR:
// ❌ ISR/SSR 頁 setup 頂層呼叫 request(),apiBase 未設定 → SSR 期 throw
const { data } = await useAsyncData(() => request('/article/list'))
// ✅ 純前端站 / 私有資料:挪到 onMounted 走 CSR
onMounted(async () => {
list.value = await request('/article/list')
})
安全模型
請求加密的 apiKey 同樣在 runtimeConfig.public.*(用戶端 bundle 可讀),動態金鑰協定是 obfuscation + 防重放,不是端對端加密。詳見用戶端加密的安全邊界。