Skip to content

接口请求与适配层 ​

整体架构 ​

前端把"发请求"这件事拆成三层:底层请求客户端、应用级封装、接口适配模块。视图层只与接口适配模块打交道,不直接接触 axios。

  • 请求客户端:@vben/request 包(源码位于 packages/effects/request),是对 axios 的轻量封装,提供 get/post/put/delete、拦截器管理,以及上传、下载、SSE 等能力。
  • 应用级封装:apps/web-ele/src/api/request.ts,是项目对请求客户端的"落地配置",负责基地址、鉴权头、租户、加解密、响应归一化、token 刷新与错误处理。
  • 接口适配模块:apps/web-ele/src/api/ 下的各业务模块文件,把后端端点封装成带 TypeScript 类型、语义清晰的函数,并对请求/响应做类型适配。

请求客户端 RequestClient ​

RequestClient 是整套能力的基石,构造函数基于 axios.create 创建实例,并内置拦截器管理器、文件上传/下载器、SSE 模块。

关键能力:

  • 标准方法:get / post / put / delete,均返回 Promise<T>。
  • 参数序列化:默认 repeat(即 ids=1&ids=2&ids=3),也可配置为 brackets / comma / indices,通过 paramsSerializer 指定。
  • 超时:默认 30s。
  • 文件:upload 自动组装 FormData、download 默认返回 Blob。
  • 流式:postSSE / requestSSE 基于 fetch 实现事件流。

响应返回方式 responseReturn ​

客户端支持三种返回策略,这是"适配层"的核心开关:

取值含义
raw返回完整的 AxiosResponse(含 headers、status),不做成功/失败判定
body仅返回响应体 response.data(只按 HTTP 状态判断成功,忽略业务 code),由调用方自行判断
data解构出响应体中的 data 字段并返回,且会自动校验业务 code 是否为成功状态

项目里 requestClient 使用 responseReturn: 'data',因此页面拿到的永远是后端 data 字段里的业务数据,而非整包 {code, data, msg}。

应用级封装 request.ts ​

api/request.ts 是整个前端请求行为的"总闸门"。它创建了两个客户端实例,并挂载了完整的拦截器链。

两个客户端:requestClient 与 baseRequestClient ​

ts
export const requestClient = createRequestClient(apiURL, {
  responseReturn: 'data',
});

export const baseRequestClient = new RequestClient({ baseURL: apiURL });
客户端用途鉴权头响应归一化返回内容
requestClient绝大多数业务接口自动携带 Authorization启用(取 data 字段)业务数据
baseRequestClient登录、刷新 token、登出、验证码等"无需/不能有常规鉴权流程"的接口不自动携带不启用(raw)原始响应

之所以区分两个客户端,关键在于 token 刷新 与 登录流程本身:刷新接口不能走 requestClient,否则会因 401 触发无限刷新循环;登录、验证码等接口也不能提前携带即将失效的 token。这些特殊接口统一走 baseRequestClient,例如 core/auth.ts 中的 loginApi、refreshTokenApi、logoutApi、getCaptcha。

拦截器链 ​

应用级封装按固定顺序挂载拦截器,理解顺序有助于排查问题。

请求拦截器:在请求发出前统一注入:

  • Authorization: Bearer <token>(来自 useAccessStore)。
  • Accept-Language:跟随用户语言偏好。
  • tenant-id / visit-tenant-id:仅当开启租户(VITE_APP_TENANT_ENABLE=true)时附加。
  • API 加密:当请求头 isEncrypt 为 true 时对请求体加密,并设置加密标识头(登录等接口显式关闭 isEncrypt)。

响应拦截器:依次处理,顺序如下。

  1. 解密响应:若响应头携带加密标识(如 X-Api-Encrypt: true)且数据为字符串,则解密后再继续。
  2. Blob 业务错误解析:文件下载等场景响应是 Blob,后端可能把"未登录"等业务错误包成 HTTP 200 + JSON Blob。此拦截器会把这类 Blob 解析回 JSON,并以 axios 风格重新抛出,确保后续 401 逻辑能接管。
  3. 响应归一化:defaultResponseInterceptor({ codeField: 'code', dataField: 'data', successCode: 0 })。当 code === 0 时返回 data 字段;否则抛出异常,交由后续错误处理。
  4. 认证与 token 刷新:authenticateResponseInterceptor,专门处理 401。
  5. 通用错误提示:errorMessageResponseInterceptor,兜底用 ElMessage.error 提示,并按状态码(网络错误、超时、400/403/404/408 等)给出文案。

响应归一化 ​

后端统一返回结构为 { code, data, msg },code = 0 表示成功。归一化拦截器把这一约定"翻译"成前端友好的形态:

  • 成功(code === 0):拦截器直接把 data 字段作为返回值交给调用方,页面无需关心 code/msg。
  • 失败:抛出包含 response 的错误对象,被第 4、第 5 步拦截器捕获处理。

配合 responseReturn: 'data',页面侧看到的永远是业务数据本身,这是适配层做"去壳"的关键一环。

认证与 token 刷新 ​

当响应为 401(HTTP 状态或业务 code === 401)时,authenticateResponseInterceptor 触发:

  1. 若未开启刷新 token,或当前已是重试请求,则执行 doReAuthenticate(清空 token,按登录过期模式弹窗或登出)。
  2. 若正在刷新中,把当前请求挂入队列,待刷新完成后用新 token 重放。
  3. 否则标记"刷新中",调用 doRefreshToken:通过 baseRequestClient 请求刷新接口,拿到新 accessToken 写入 accessStore,并批量重放队列中的请求。
  4. 刷新失败时清空队列并执行 doReAuthenticate。

注意刷新请求刻意走 baseRequestClient,避免与常规鉴权拦截器相互触发。下面是 refreshTokenApi 的简化写法:

ts
export async function refreshTokenApi(refreshToken: string) {
  return baseRequestClient.post(
    `/system/auth/refresh-token?refreshToken=${refreshToken}`,
  );
}

通用错误处理 ​

错误处理的"兜底"在 errorMessageResponseInterceptor:

  • 取消请求(axios.isCancel)直接忽略。
  • 网络错误、超时给出对应提示。
  • 其余按状态码映射文案(400/401/403/404/408/默认)。
  • 特殊规避:当业务 code === 401 时不再重复提示,因为此时会跳转到登录页,只需提示一次。

如果某些接口需要自定义错误提示(例如不弹全局提示、或按不同 code 做不同处理),可在调用处用 try / catch 捕获,或调整该拦截器内 makeErrorMessage 的判定逻辑。

接口适配层 api 目录 ​

apps/web-ele/src/api/ 按后端子系统分目录(system / infra / ai / bpm / core),每个文件对应一组接口。这一层承担了"类型适配"的职责。

命名空间 + 接口定义 ​

每个模块用 namespace 收敛类型,与后端 VO/DTO 对齐,避免类型散落:

ts
export namespace SystemMenuApi {
  export interface Menu {
    id: number;
    name: string;
    permission: string;
    type: number;
    // …其余字段省略
  }
}

类型化函数 ​

把端点封装成语义函数,统一从这里导入使用:

ts
import { requestClient } from '#/api/request';

/** 查询菜单列表 */
export async function getMenuList(params?: Record<string, any>) {
  return requestClient.get<SystemMenuApi.Menu[]>('/system/menu/list', {
    params,
  });
}

/** 新增菜单 */
export async function createMenu(data: SystemMenuApi.Menu) {
  return requestClient.post('/system/menu/create', data);
}

页面只 import { getMenuList, createMenu } from '#/api/system/menu',拿到的是强类型数据,无需了解 URL 与响应结构。

分页与排序约定 ​

列表类接口统一使用 PageParam(含 pageNo、pageSize)与 PageResult<T>(含 items、total),来自 @vben/request:

ts
import type { PageParam, PageResult } from '@vben/request';

export function getUserPage(params: PageParam) {
  return requestClient.get<PageResult<SystemUserApi.User>>(
    '/system/user/page',
    { params },
  );
}

表格组件(如 vxe-table)的排序条件可通过 buildSortingField(sorts) 工具转换为后端可识别的 sortingFields[] 参数。

文件上传与下载 ​

  • 上传:requestClient.upload(url, { file, ...其余字段 }),内部自动构建 multipart/form-data 的 FormData,并支持 onUploadProgress 进度回调。
  • 下载:requestClient.download(url),默认 responseType: 'blob'、responseReturn: 'body',直接得到 Blob,随后可借助 useDownloadFile 之类的工具触发浏览器下载。

SSE 与流式接口 ​

AI 对话等流式场景使用 SSE(Server-Sent Events)。项目中有两种用法:

  • requestClient.postSSE / requestSSE:基于 fetch 封装,提供 onMessage / onEnd 回调。
  • fetchEventSource:AI 聊天直接采用 @microsoft/fetch-event-source,手动携带 Authorization 头与 AbortController 以便中途取消,示意见 api/ai/chat/message:
ts
export function sendChatMessageStream(/* … */) {
  return fetchEventSource(`${apiURL}/ai/chat/message/send-stream-my`, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ /* 请求体 */ }),
    onmessage: onMessage,
    onerror: onError,
    onclose: onClose,
    signal: ctrl.signal,
  });
}

流式数据通常仍是统一响应结构,需在前端 onmessage 中解析 JSON 并判断 code,再按事件类型(message_created / text_delta / reasoning_delta / message_done)增量更新视图。

API 加解密 ​

当环境变量 VITE_APP_API_ENCRYPT_ENABLE=true 时启用接口加解密(createApiEncrypt),支持 AES / RSA:

  • 请求:请求头带 isEncrypt: true 的接口,其 data 会被加密,并设置加密标识头(默认 X-Api-Encrypt)。
  • 响应:响应头携带该标识且数据为字符串时,自动解密。

登录等敏感接口会显式设置 isEncrypt: false 以跳过加密(此时账号密码以明文或更轻量方式传输,或依赖通道安全),相关密钥由 VITE_APP_API_ENCRYPT_* 系列环境变量注入。

环境变量与基地址 ​

请求的基地址由 useAppConfig 解析环境变量得到:

  • 开发环境:VITE_GLOB_API_URL=/admin-api,通过 Vite 代理(VITE_PROXY_PREFIX)转发到后端。
  • 生产环境:VITE_GLOB_API_URL=/api/v1,并与 VITE_BASE_URL 拼接为完整后端地址。

相关关键变量:

变量说明
VITE_GLOB_API_URL接口路径前缀,所有请求以此为开头
VITE_BASE_URL生产环境后端基础地址
VITE_PROXY_PREFIX开发环境代理重写目标
VITE_APP_TENANT_ENABLE是否启用多租户(决定 tenant-id 头)
VITE_APP_API_ENCRYPT_*接口加解密开关与密钥

如何新增一个接口 ​

  1. 在 api/<module>/ 下新建或编辑文件,用 namespace 定义请求/响应类型。
  2. 导出语义化函数,通过 requestClient 的 get/post/put/delete 调用,并用泛型标注返回类型。
  3. 页面中 import 该函数并调用,直接拿到业务数据;需要自定义错误提示时在调用处 try / catch。
  4. 若是登录/刷新/验证码/登出类接口,使用 baseRequestClient 或对应的 core/auth 函数。
  5. 涉及文件传输用 upload/download;涉及流式输出用 fetchEventSource 或 postSSE。

新增接口无需关心 token 注入、租户头、响应解包、401 刷新等横切逻辑,这些都由应用级封装统一保证。