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 同口径附着

全局请求配置(超时 / 公共 header)走 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 + 防重放,不是端到端加密。详见客户端加密的安全边界。