# 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(config: AxiosRequestConfig, options?: RequestOptions): Promise { 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>(conf) .then((res) => { if (transformRequestData && !axios.isCancel(res)) { const ret = transformRequestData(res, opt); resolve(ret); } }) .catch((e) => reject(e)); }); } // 文件上传 uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { const formData = new window.FormData(); formData.append(params.name || 'file', params.file); return this.axiosInstance.request({ 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, options: RequestOptions) => { const { code, data, msg } = res.data; // 业务成功(code === 0) if (code === ResultEnum.SUCCESS) { return data; // 直接返回 data 字段,调用方拿到的是业务数据 } // 业务失败(code === 1) if (code === ResultEnum.ERROR) { message.error(msg); throw new Error(msg); } // 登录超时(code === 401) if (code === ResultEnum.TIMEOUT) { Modal.confirm({ title: '提示', content: '登录身份已失效,请重新登录!', onOk: () => { 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) => { 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.success('操作成功'); emit('success'); }; ``` :::tip 温馨提示 所有 API 函数返回的是已解包的响应数据(经过拦截器处理),分页接口直接返回 `{ data, count }` 结构,调用方无需再 `.data` 解包。登录接口使用 `isTransformResponse: false` 跳过自动解包,自行处理返回值。 ::: ## 错误处理 错误处理分两层: 1. **业务错误**(code !== 0):拦截器统一弹出 `message.error(msg)`,并 `throw new Error` 2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理 ```typescript import { message, Modal } from 'ant-design-vue'; responseInterceptorsCatch: (error: any) => { const { response, code } = error || {}; // 超时 if (code === 'ECONNABORTED') { message.error('接口请求超时,请刷新页面重试!'); return; } // 网络异常 if (error.toString().includes('Network Error')) { Modal.confirm({ title: '网络异常', content: '请检查您的网络连接是否正常', onOk: () => { // 处理确认操作 }, }); 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 状态码)等能力。各模块按目录组织接口函数,调用方拿到的是已解包的业务数据。