request

framework 的統一加密請求入口。業務程式碼一律用 request()(auto-import),它把動態金鑰加密、token / deviceId 注入、登入續期、SSR/CSR 分流全部封裝好;createApiFetch 是其底層工廠,由 framework 插件實例化後注入為 $request,業務一般不直接使用。

運作原理

  1. createApiFetch 在 framework 插件中實例化(注入 getToken / getDeviceId / onRelogin),request() 是它的全域薄封裝。
  2. 每次請求產生獨立 requestId(UUID v4),同時用於:請求標頭與伺服器端對齊、aes-dynamic 的動態金鑰派生(HMAC-SHA256)、除錯日誌配對。
  3. 請求體統一包裝為 { data, token, deviceId },gzip + AES-CBC 加密後以 text/plain POST 發送;回應體經 decodeResponse 解碼(解密;舊後端會把部分業務失敗以明文 JSON 回傳,明文失敗信封同樣放行)、再按 { status, data, error, errorCode } 解包,成功時回傳其中的 data;業務失敗(status: 'n')拋 ApiError(message 取信封 error,並附著 errorCode 供結構化分支)。
  4. SSR / CSR 分流:SSR 直連後端(baseURL = apiBase + apiPrefix)並透傳真實 IP(cf-connecting-ip / x-forwarded-for / x-real-ip)與 user-agent;CSR 走 routeRules 代理(baseURL = apiPrefix)。
  5. auth-ready 門控:client 端 request() 自動 await useAuthReady(),確保請求不會在 device + token 身分建立前發出(避免首屏 401 風暴)。
  6. token 自動續期:後端回傳 errorCode === 2002(token 失效)時觸發 onRelogin 靜默重登,重試一次;再失敗拋 ApiError('登录失效,请刷新页面重试', 2002)。
  7. 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 插件內部使用
ApiErrorclass 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 + 防重放,不是端對端加密。詳見用戶端加密的安全邊界。