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