Files
djangoadmin-wiki/DjangoAdmin_FastAPI_EleVue/zh/volume4/frontend/api.md
T
2026-09-12 23:12:15 +08:00

333 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API 层开发
:::tip 说明
前端 API 层位于 `src/api/` 目录,封装所有与后端的 HTTP 请求。底层基于 Axios 封装了 `VAxios` 类(位于 `src/utils/http/axios/`),提供请求拦截、响应解包、错误处理、重复请求取消等能力。
:::
## 目录结构
```
src/api/
├── common/
│ ├── user.ts # 登录、登出、获取用户信息、验证码
│ └── menu.ts # 获取菜单(adminMenus)
├── system/
│ ├── position.ts # 岗位管理接口
│ ├── user.ts # 用户管理接口
│ ├── role.ts # 角色管理接口
│ ├── menu.ts # 菜单管理接口
│ ├── dept.ts # 部门管理接口
│ ├── level.ts # 职级管理接口
│ ├── dictionary.ts # 字典管理接口
│ ├── loginLog.ts # 登录日志接口
│ ├── operLog.ts # 操作日志接口
│ └── tenant.ts # 租户管理接口
└── ...
```
## Axios 封装架构
的 HTTP 层不是简单的 Axios 实例,而是一套完整的封装体系:
```
src/utils/http/axios/
├── index.ts # 入口:创建 VAxios 实例,定义 transform 处理逻辑
├── Axios.ts # VAxios 类:封装 AxiosInstance,管理拦截器
├── axiosTransform.ts # AxiosTransform 抽象类:定义拦截器钩子接口
├── axiosCancel.ts # AxiosCanceler:重复请求取消(基于 CancelToken Map)
├── checkStatus.ts # HTTP 状态码错误提示
├── helper.ts # 工具函数(时间戳追加、日期格式化)
└── types.ts # 类型定义(RequestOptions、Result、CreateAxiosOptions)
```
### VAxios 类
`Axios.ts` 中的 `VAxios` 类是对 `AxiosInstance` 的封装,核心能力:
```typescript
// src/utils/http/axios/Axios.ts
export class VAxios {
private axiosInstance: AxiosInstance;
private options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.axiosInstance = axios.create(options);
this.setupInterceptors(); // 自动注册拦截器
}
// 通用请求方法
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: AxiosRequestConfig = cloneDeep(config);
const transform = this.getTransform();
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, transformRequestData } = transform || {};
if (beforeRequestHook) conf = beforeRequestHook(conf, opt);
conf.requestOptions = opt;
return new Promise((resolve, reject) => {
this.axiosInstance.request<any, AxiosResponse<Result>>(conf)
.then((res) => {
if (transformRequestData && !axios.isCancel(res)) {
const ret = transformRequestData(res, opt);
resolve(ret);
}
})
.catch((e) => reject(e));
});
}
// 文件上传
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
formData.append(params.name || 'file', params.file);
return this.axiosInstance.request<T>({
method: 'POST',
data: formData,
headers: { 'Content-type': ContentTypeEnum.FORM_DATA },
...config,
});
}
}
```
### 全局实例
`index.ts` 导出全局单例 `http`,所有 API 函数使用此实例:
```typescript
// src/utils/http/axios/index.ts
export const http = createAxios();
```
## API 定义示例
以岗位管理为例(`src/api/system/position.ts`),实际代码如下:
```typescript
import { http } from '@/utils/http/axios';
// 分页查询岗位列表
export function getPositionPage(params?) {
return http.request({
url: '/position/page',
method: 'GET',
params,
});
}
// 获取全部岗位列表(无分页,用于下拉选择)
export function getPositionList(params?) {
return http.request({
url: '/position/list',
method: 'GET',
params,
});
}
// 根据 ID 获取详情
export function getPositionDetail(positionId) {
return http.request({
url: '/position/detail/' + positionId,
method: 'get',
});
}
// 添加岗位
export function positionAdd(data: any) {
return http.request({
url: '/position/add',
method: 'POST',
data,
});
}
// 更新岗位
export function positionUpdate(data: any) {
return http.request({
url: '/position/update',
method: 'PUT',
data,
});
}
// 删除岗位
export function positionDelete(positionId) {
return http.request({
url: '/position/delete/' + positionId,
method: 'DELETE',
});
}
// 批量删除岗位
export function positionBatchDelete(data: any) {
return http.request({
url: '/position/batchDelete',
method: 'DELETE',
data,
});
}
```
:::tip API 路径说明
API 函数中的 `url` 不需要写 `/api/v1` 前缀。`VAxios` 创建时配置了 `prefixUrl` 和 `requestOptions.urlPrefix`,拦截器的 `beforeRequestHook` 会自动拼接前缀。最终请求路径为 `/api/v1/position/page`。
:::
## 请求拦截器
请求拦截器在 `index.ts` 的 `transform` 中定义,自动注入 Token:
```typescript
requestInterceptors: (config, options) => {
const token = storage.get('ACCESS-TOKEN', '');
if (token && config.requestOptions?.withToken !== false) {
(config as Recordable).headers.Authorization = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token;
}
return config;
},
```
Token 存储在 `localStorage` 中,键名为 `ACCESS-TOKEN`,通过 `storage` 工具类管理(支持 7 天过期)。
## 响应拦截器
响应解包逻辑在 `transformRequestData` 中处理:
```typescript
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { code, data, msg } = res.data;
// 业务成功(code === 0)
if (code === ResultEnum.SUCCESS) {
return data; // 直接返回 data 字段,调用方拿到的是业务数据
}
// 业务失败(code === 1)
if (code === ResultEnum.ERROR) {
ElMessage.error(msg);
throw new Error(msg);
}
// 登录超时(code === 401)
if (code === ResultEnum.TIMEOUT) {
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { ... })
.then(() => {
storage.clear();
window.location.href = '/login';
});
throw new Error(msg);
}
},
```
解包后的返回值约定:
| 接口类型 | 返回值结构 | 说明 |
|----------|-----------|------|
| 普通接口 | `data`(直接返回业务数据) | 拦截器已解包 `res.data.data` |
| 分页接口 | `{ data: [...], count: 100 }` | 后端通过 `R.ok(data=list, count=total)` 返回 |
| 登录接口 | `{ access_token: "..." }` | 使用 `isTransformResponse: false`,自行处理 |
## 响应头 Token 自动续签
后端支持滑动窗口续签,响应拦截器会检测 `x-refresh-token` 头并自动更新本地存储:
```typescript
responseInterceptors: (res: AxiosResponse<any>) => {
const newToken = res.headers['x-refresh-token'];
if (newToken) {
storage.set('ACCESS-TOKEN', newToken);
}
return res;
},
```
## 重复请求取消
`AxiosCanceler` 类通过 `pendingMap` 维护请求标识,相同请求自动取消前一个:
```typescript
// src/utils/http/axios/axiosCancel.ts
export class AxiosCanceler {
addPending(config: AxiosRequestConfig) {
this.removePending(config); // 先移除已有的同请求
const url = getPendingUrl(config);
config.cancelToken = new axios.CancelToken((cancel) => {
if (!pendingMap.has(url)) {
pendingMap.set(url, cancel);
}
});
}
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
pendingMap.get(url)(url); // 取消请求
pendingMap.delete(url);
}
}
}
```
## 接口调用方式
在 Vue 组件中调用 API:
```typescript
import { getPositionList, positionAdd } from '@/api/system/position'
// 查询列表(loadDataTable 传给 BasicTable 的 request 属性)
const loadDataTable = async (res: any) => {
const result = await getPositionList({ ...formParams, ...res });
return result; // 拦截器已解包,result 即为 { data, count, msg, ok, code }
};
// 添加数据
const handleSubmit = async () => {
await formRef.value?.validate();
await positionAdd(formData);
message('操作成功');
emit('success');
};
```
:::tip 温馨提示
所有 API 函数返回的是已解包的响应数据(经过拦截器处理),分页接口直接返回 `{ data, count }` 结构,调用方无需再 `.data` 解包。登录接口使用 `isTransformResponse: false` 跳过自动解包,自行处理返回值。
:::
## 错误处理
错误处理分两层:
1. **业务错误**(code !== 0):拦截器统一弹出 `ElMessage.error(msg)`,并 `throw new Error`
2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理
```typescript
responseInterceptorsCatch: (error: any) => {
const { response, code } = error || {};
// 超时
if (code === 'ECONNABORTED') {
ElMessage.error('接口请求超时,请刷新页面重试!');
return;
}
// 网络异常
if (error.toString().includes('Network Error')) {
ElMessageBox.confirm('请检查您的网络连接是否正常', '网络异常', { ... });
return Promise.reject(error);
}
// 其他 HTTP 状态码
checkStatus(error.response?.status, response?.data?.msg);
},
```
`checkStatus` 函数根据 HTTP 状态码弹出对应中文提示(400/401/403/404/405/408/500/501/502/503/504/505)。
## 总结
API 层通过 `VAxios` 类封装 Axios,提供请求拦截(自动注入 Token)、响应解包(`code===0` 返回 `data`)、重复请求取消(`AxiosCanceler`)、Token 自动续签(`x-refresh-token` 响应头)、错误处理(业务错误 + HTTP 状态码)等能力。各模块按目录组织接口函数,调用方拿到的是已解包的业务数据。