ContiNew
前端开发

HTTP 请求

讲解前端 axios 封装(src/utils/http.ts)的统一约定,包括请求头注入、401 刷新重试与错误提示。

前端所有接口请求都经过 src/utils/http.ts 导出的封装实例,业务代码不直接引用 axios。这一层统一处理了请求头注入、双令牌刷新、错误提示三件事。

基本用法

API 定义集中在 src/apis/ 下,按后端模块分文件。每个文件从 @/utils/http 导入封装对象,用 get / post / put / del 等方法拼出接口函数。

// src/apis/system/user.ts(节选)
import http from '@/utils/http'

const BASE_URL = '/system/user'

/** @desc 查询用户列表(分页) */
export function listUser(query: UserPageQuery) {
  return http.get<PageRes<UserResp[]>>(`${BASE_URL}`, query)
}

/** @desc 查询所有用户(无分页,入参为可选的分页过滤条件) */
export function listAllUser(query: Partial<UserPageQuery> = {}) {
  return http.get<UserResp[]>(`${BASE_URL}/list`, query)
}

/** @desc 新增用户 */
export function addUser(data: any) {
  return http.post(`${BASE_URL}`, data)
}

页面里通过 useTable 等 hook 调用这些函数,或直接 await。

import { listUser } from '@/apis/system/user'

const { data } = await listUser({ page: 1, size: 10 })

封装导出的方法如下。

方法说明
get / post / put / patch / del常规请求,返回 Promise<ApiRes<T>>(已解包到响应体)
request原始请求,返回 Promise<ApiRes<T>>
requestNative返回完整 AxiosResponse(需要响应头时用)
download文件下载,responseType: 'blob',返回 AxiosResponse

axios 实例配置

http.ts 创建的实例有三项关键配置(src/utils/http.ts:38)。

const http: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_PREFIX ?? import.meta.env.VITE_API_BASE_URL,
  timeout: 30 * 1000,
  // Refresh Token 由后端写入 HttpOnly Cookie,浏览器必须允许跨域请求携带 Cookie。
  withCredentials: true,
})

baseURL 优先取 VITE_API_PREFIX(开发环境走 Vite 代理),没有则取 VITE_API_BASE_URL(生产环境直连接口域名)。withCredentials: true 让跨域请求携带 HttpOnly Cookie,双令牌刷新依赖这一点。

后端统一响应结构由 Graceful Response 封装,形如 { "code": 0, "data": ..., "msg": "...", "success": true, "timestamp": ... }。响应拦截器检查 success 字段:true 直接放行,false 弹出错误提示并 reject。get 类请求会自动用 query-string 序列化参数,数组参数不会被 axios 默认格式序列化坏。

请求头注入

请求拦截器在每个请求上注入两类请求头(src/utils/http.ts:207)。

Authorization 取内存中的 Access Token,拼成 Bearer <token>。Access Token 只保存在内存(src/features/auth-session/access-token.ts),不落地 localStorage,浏览器刷新后靠 HttpOnly Cookie 里的 Refresh Token 换新的。

X-Tenant-Id 在租户功能开启(tenantStore.tenantEnabled)且已选定租户(tenantStore.tenantId)时注入,值来自登录响应或刷新响应。后端多租户插件靠这个头做行级隔离,对应 starter 配置 continew-starter.tenant.tenant-id-header。

认证接口有例外处理:/auth/refresh、/auth/logout 以及账号密码登录的 /auth/login 不携带 X-Tenant-Id,因为这些请求要么以服务端 Refresh Session 为准,要么以用户显式输入的租户编码为准;社交登录回调则保留授权前选定的租户。请求重试时会复用原始 headers,拦截器在租户状态变化时主动删除旧租户头,避免把上一次会话的租户上下文带给服务端。

401 处理与双令牌刷新

HTTP 状态 401 走 retryAfterRefresh 分支(src/utils/http.ts:165),处理流程分三步。

首次 401 触发 /auth/refresh 刷新请求,浏览器自动携带 HttpOnly Cookie。同一时间多个 401 请求共用一个单例 Promise(refreshPromise),Refresh Token 不会被并发轮换。

刷新成功后更新内存中的 Access Token 与租户 ID,给原请求打上 __isRetryRequest 标记,换上新 Token 重发。

重发后再次 401,或者刷新请求本身返回 401(Refresh Token 确定失效),进入重新登录流程。

重新登录流程由 showReLogin 负责(src/utils/http.ts:78):清理内存 Token 与持久化的用户、路由状态,调用 /auth/logout 让服务端清掉 Cookie,然后弹 Modal 引导用户重新登录,确认后跳转 /login 并带上 redirect 参数。弹窗期间如果用户已在别处重新登录,「认证代次」(auth generation)机制会让旧弹窗失效,不会把新会话重置掉。

刷新请求遇到 403、429、5xx 或网络故障时不算终态失败,Cookie 会保留,下一次请求继续尝试刷新。只有刷新返回 401 或业务层明确拒绝(HTTP 200 但 success: false)才引导重新登录。

401 的重登弹窗只对应用内操作出现。浏览器刷新后首次进入页面时没有会话,由路由守卫直接跳转登录页,不弹窗(见 路由与页签)。

错误提示

非 401 的 HTTP 错误按状态码映射到 StatusCodeMessage 里的中文提示(如 403「拒绝访问」、500「服务器错误」),映射不到时给通用兜底文案。网络层失败(无响应对象)提示「网络连接失败」。

提示形式按文案长度分流(src/utils/http.ts:193):15 字以内用 Message,更长用 Notification,都包了一层防重复弹出的 wrapper(message-error-wrapper / notification-error-wrapper)。

下载类请求(responseType: 'blob')的失败响应是 JSON 格式的 Blob,响应拦截器会用 FileReader 读出 msg 再提示,不会让错误以乱码形式吞掉。

新增接口的建议

在 src/apis/ 对应模块文件里追加函数,遵循既有约定:BASE_URL 常量拼路径、@desc 注释描述用途、返回类型用泛型标注。新增模块时在 src/apis/index.ts 里 re-export。

/** @desc 查询公告列表 */
export function listNotice(query: NoticePageQuery) {
  return http.get<PageRes<NoticeResp[]>>(`/system/notice`, query)
}

不要在页面组件里直接写 axios.get,绕过封装意味着丢掉 Token 注入、租户头、401 刷新和错误提示。

相关源码

目录