Skip to content

API 层开发

说明

前端 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,
    });
}

API 路径说明

API 函数中的 url 不需要写 /api/v1 前缀。VAxios 创建时配置了 prefixUrlrequestOptions.urlPrefix,拦截器的 beforeRequestHook 会自动拼接前缀。最终请求路径为 /api/v1/position/page

请求拦截器

请求拦截器在 index.tstransform 中定义,自动注入 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');
};

温馨提示

所有 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 状态码)等能力。各模块按目录组织接口函数,调用方拿到的是已解包的业务数据。

小蚂蚁云团队 · 提供技术支持