外观
接口请求与适配层
整体架构
前端把"发请求"这件事拆成三层:底层请求客户端、应用级封装、接口适配模块。视图层只与接口适配模块打交道,不直接接触 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)。
响应拦截器:依次处理,顺序如下。
- 解密响应:若响应头携带加密标识(如
X-Api-Encrypt: true)且数据为字符串,则解密后再继续。 - Blob 业务错误解析:文件下载等场景响应是
Blob,后端可能把"未登录"等业务错误包成 HTTP 200 + JSON Blob。此拦截器会把这类 Blob 解析回 JSON,并以 axios 风格重新抛出,确保后续 401 逻辑能接管。 - 响应归一化:
defaultResponseInterceptor({ codeField: 'code', dataField: 'data', successCode: 0 })。当code === 0时返回data字段;否则抛出异常,交由后续错误处理。 - 认证与 token 刷新:
authenticateResponseInterceptor,专门处理 401。 - 通用错误提示:
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 触发:
- 若未开启刷新 token,或当前已是重试请求,则执行
doReAuthenticate(清空 token,按登录过期模式弹窗或登出)。 - 若正在刷新中,把当前请求挂入队列,待刷新完成后用新 token 重放。
- 否则标记"刷新中",调用
doRefreshToken:通过baseRequestClient请求刷新接口,拿到新accessToken写入accessStore,并批量重放队列中的请求。 - 刷新失败时清空队列并执行
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_* | 接口加解密开关与密钥 |
如何新增一个接口
- 在
api/<module>/下新建或编辑文件,用namespace定义请求/响应类型。 - 导出语义化函数,通过
requestClient的get/post/put/delete调用,并用泛型标注返回类型。 - 页面中
import该函数并调用,直接拿到业务数据;需要自定义错误提示时在调用处try / catch。 - 若是登录/刷新/验证码/登出类接口,使用
baseRequestClient或对应的core/auth函数。 - 涉及文件传输用
upload/download;涉及流式输出用fetchEventSource或postSSE。
新增接口无需关心 token 注入、租户头、响应解包、401 刷新等横切逻辑,这些都由应用级封装统一保证。