2025-12-28-前端学习-接口类型定义、Axios 封装与请求规范

文章发布时间:

最后更新时间:

页面浏览: 加载中...

关于接口类型定义、Axios 封装与请求规范的常见问题

使用 TypeScript 的 React 或 Vue 项目中,通常会高度重视网络层的工程化实践,通常会从实际项目经验入手,逐步深入到设计理念、类型安全和最佳实践

关于接口类型定义、Axios 封装与请求规范的常见问题整理如下:

基本经验与动机

  • 在项目中是否对 Axios 进行过二次封装?为什么需要封装,而不是直接使用原生 Axios? 是的,在所有中大型项目中都会对 Axios 进行二次封装。主要原因是原生 Axios 配置分散、重复代码多(如每个请求都需手动设置 baseURL、headers 和错误处理)。封装后可以统一管理公共逻辑,减少冗余,提升代码一致性和可维护性,避免直接使用导致的配置不统一和后期修改困难。

  • Vue/React 项目中,你们是怎么管理 API 接口的?有统一的请求封装吗? 我们采用统一的请求封装层。通常创建一个独立的 apiClient 实例作为基础,然后在 services 或 api 目录下按业务模块(如 auth、user、room)划分文件,每个模块导出具体的请求函数。所有接口调用都通过这些封装函数进行,确保风格一致、类型安全,并便于后期维护和 mock。

  • 说说 Axios 二次封装的主要目的和好处? 主要目的是统一配置和公共逻辑处理,包括 baseURL、超时、Token 添加、错误统一处理等。好处包括:减少重复代码、提升可维护性、统一错误提示和加载状态、便于环境切换、支持类型安全(TS 项目),最终降低 bug 率并提高团队开发效率。

实现细节与规范

  • 怎么封装 Axios 的?主要封装了哪些方面(如 baseURL、超时、请求/响应拦截器、错误处理)? 首先使用 axios.create() 创建实例,设置 baseURL、timeout 和默认 headers。然后添加请求拦截器统一注入 Token 和加载状态;响应拦截器中提取 data、处理业务 code、统一错误提示(如 401 跳转登录),并支持 Token 刷新重试。

  • 在项目中,如何统一处理请求头(如添加 Token)、环境切换(开发/生产 baseURL)和错误提示? 请求头通过请求拦截器或 setAuthToken 函数统一添加 Authorization;环境切换利用 Vite 或 Webpack 的环境变量动态设置 baseURL;错误提示在响应拦截器中根据 status 或业务 code 统一处理,使用 toast 组件显示消息,或触发全局错误处理逻辑。

  • 封装后,如何组织和管理具体的 API 接口?(如按模块分文件、统一导出) 按业务模块分文件(如 auth.ts、room.ts),每个文件定义相关接口函数并导出;再创建一个 index.ts 统一导出所有模块,便于在业务组件中按需导入(如 import { login } from ‘@/api’)。这样结构清晰,便于维护和权限控制。

  • 如何处理请求取消、重复请求防抖或加载状态? 使用 Axios CancelToken 或 AbortController 实现请求取消,适用于组件卸载或搜索防抖场景;重复请求通过 URL + 方法 + 参数的 Map 缓存取消函数实现防重;加载状态可在拦截器中 dispatch 全局 loading action,或在单个请求中使用 async/await 结合状态管理。

类型安全与工程化(TypeScript)

  • 在使用 TypeScript 的项目中,你是怎么结合接口类型定义来封装 Axios 的?如何实现响应数据的类型推导? 先在 types/api.ts 中集中定义所有接口的请求参数和响应类型。然后在服务函数中使用 Axios 泛型,如 apiClient.get<RoomListResponse>(url),这样返回值的类型自动推导为定义的接口类型,实现全程类型检查和编辑器提示。

  • 说说 Axios 泛型的使用,比如如何通过 <T> 指定返回类型,确保调用时有类型提示和检查? 通过 apiClient.post<T>(url, data)的方式指定泛型 T 为具体响应类型(如 LoginResponse)。这样调用时 TypeScript 会自动推导返回值属性,提供属性提示和编译时错误检查(如访问不存在字段会报错),显著提升类型安全。

  • 怎么定义接口请求参数和响应类型的?有统一的响应包装类型(如 ApiResponse<T>)吗? 定义通用包装类型 interface ApiResponse<T> { code: number; data: T; message?: string; } 所有接口响应类型继承此泛型(如 type LoginResponse = ApiResponse<{ token: string }>;),便于统一处理业务 code 和错误。

  • 在封装中,如何处理拦截器或自定义配置的 TypeScript 类型扩展(如扩展 AxiosRequestConfig)? 通过模块声明扩展 AxiosRequestConfig 接口,添加自定义字段(如 _retry: boolean 用于 Token 刷新)。拦截器参数类型自然继承扩展后的配置,确保类型兼容和提示完整。

  • 如果后端返回结构不统一,怎么通过类型守卫或转型确保类型安全?

类型守卫(Type Guard)是 TypeScript 中的一种机制,用于在运行时缩小变量的类型范围,从而让编译器在特定代码块中更精确地推断变量的类型。它本质上是一个返回布尔值的表达式或函数,当该表达式为 true 时,TypeScript 会自动将变量的类型收窄(narrow)为更具体的类型。

1
2
3
4
5
6
7
8
9
10
//typeof 类型守卫 使用 JavaScript 的 typeof 操作符
function print(value: string | number) {
if (typeof value === 'string') {
// 这里 value 被收窄为 string
console.log(value.toUpperCase());
} else {
// 这里 value 被收窄为 number
console.log(value.toFixed(2));
}
}

在响应拦截器中先转型为 any 或 unknown,然后使用类型守卫(如 if (‘code’ in res && res.code === 0))判断成功,再返回 res.data 并断言为具体类型。这样既兼容不统一结构,又保持业务层类型安全

深度与实践相关

  • 封装后,在业务组件中调用接口的体验如何?相比直接用 Axios 有哪些改进? 体验显著提升:调用简洁,自动获得类型提示和错误检查;无需关心 Token、baseURL 或错误处理。相比直接使用,减少了大量样板代码,降低了出错概率,并提高了代码可读性。

  • 有考虑过从 OpenAPI/Swagger 自动生成类型和接口函数吗? 是的,在较大项目中会使用 openapi-typescript 或 swagger-typescript-api 从后端 OpenAPI 文档自动生成类型和请求函数。这样保持前后端类型一致,减少手动维护成本,并进一步提升工程化水平。

  • 如果项目规模很大,是怎么进一步优化网络层的(如模块化、服务层分离)? 通过严格的服务层分离:apiClient 只负责基础请求,services 层按领域划分(如 userService、orderService),每个服务聚合相关接口并处理业务逻辑;结合代码生成和 mock 工具,实现高度模块化和可测试性。

  • 说说项目中网络请求的常见痛点,以及封装如何解决的。 常见痛点包括 Token 管理散乱、错误处理不统一、环境配置易错、类型不安全。二次封装通过拦截器统一 Token 和错误、环境变量管理配置、TS 泛型确保类型安全,有效解决了这些问题,显著降低了联调和维护成本。


Axios 封装与接口管理具体实现

基础的实现

  1. 如何封装 Axios? 一个典型的 Axios 封装结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // api/utils/apiClient.ts
    import axios, { type AxiosRequestConfig } from "axios";

    const apiClient = axios.create({
    baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
    timeout: 10000,
    headers: {
    "Content-Type": "application/json",
    },
    });

    // 请求拦截器
    apiClient.interceptors.request.use(
    (config) => {
    const token = localStorage.getItem("token");
    if (token) {
    config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
    },
    (error) => Promise.reject(error)
    );

    // 响应拦截器
    apiClient.interceptors.response.use(
    (response) => response.data,
    (error) => {
    if (error.response?.status === 401) {
    localStorage.removeItem("token");
    window.location.href = "/login";
    }
    return Promise.reject(error);
    }
    );

    export default apiClient;
  2. 如何统一处理请求头、环境切换和错误提示?

    • 环境切换:通过 Vite 或 Webpack 的环境变量(如 VITE_API_BASE_URL)动态配置 baseURL。
    • 请求头处理:在请求拦截器中统一注入 Authorization 等认证头。
    • 错误提示:在响应拦截器中根据状态码或业务码统一处理错误,例如 401 跳转登录、500 显示服务器错误提示。
  3. 如何组织和管理具体的 API 接口? 按业务模块划分文件,并统一导出,便于维护。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // api/index.ts
    export { default as userApi } from "./user";
    export { default as roomApi } from "./room";

    // api/room.ts
    import apiClient from "./utils/apiClient";
    import type { RoomResponse, CreateRoomRequest } from "./types";

    const roomApi = {
    getRooms: (params: { page: number; size: number }) =>
    apiClient.get<RoomResponse>("/rooms", { params }),
    createRoom: (data: CreateRoomRequest) => apiClient.post("/rooms", data),
    updateRoom: (id: string, data: Partial<CreateRoomRequest>) =>
    apiClient.put(`/rooms/${id}`, data),
    deleteRoom: (id: string) => apiClient.delete(`/rooms/${id}`),
    };

    export default roomApi;
  4. 如何处理请求取消、重复请求防抖或加载状态? 使用 AbortController 实现请求取消和防重提交。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    const pendingRequests = new Map<string, AbortController>();

    apiClient.interceptors.request.use((config) => {
    const requestKey = `${config.method?.toUpperCase()}${config.url}`;
    if (pendingRequests.has(requestKey)) {
    pendingRequests.get(requestKey)?.abort();
    }
    const controller = new AbortController();
    config.signal = controller.signal;
    pendingRequests.set(requestKey, controller);
    return config;
    });

    apiClient.interceptors.response.use(
    (response) => {
    const requestKey = `${response.config.method?.toUpperCase()}${
    response.config.url
    }`;
    pendingRequests.delete(requestKey);
    return response;
    },
    (error) => {
    if (error.config) {
    const requestKey = `${error.config.method?.toUpperCase()}${
    error.config.url
    }`;
    pendingRequests.delete(requestKey);
    }
    return Promise.reject(error);
    }
    );

类型安全与工程化(TypeScript)

  1. 如何结合接口类型定义封装 Axios?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // api/types/index.ts
    export interface ApiResponse<T> {
    code: number;
    message: string;
    data: T;
    }

    export interface Room {
    id: string;
    name: string;
    description?: string;
    createdAt: string;
    updatedAt: string;
    }

    export interface RoomResponse extends ApiResponse<Room[]> {
    pagination: { page: number; size: number; total: number };
    }

    export interface CreateRoomRequest {
    name: string;
    description?: string;
    }
  2. Axios 泛型的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // apiService.ts
    import type { AxiosRequestConfig, AxiosResponse } from "axios";
    import apiClient from "./utils/apiClient";
    import type { ApiResponse } from "./types";

    class ApiService {
    async get<T>(
    url: string,
    config?: AxiosRequestConfig
    ): Promise<ApiResponse<T>> {
    const response: AxiosResponse<ApiResponse<T>> = await apiClient.get(
    url,
    config
    );
    return response.data;
    }

    async post<T, D = any>(
    url: string,
    data?: D,
    config?: AxiosRequestConfig
    ): Promise<ApiResponse<T>> {
    const response: AxiosResponse<ApiResponse<T>> = await apiClient.post(
    url,
    data,
    config
    );
    return response.data;
    }

    // put、delete 同理
    }

    export const apiService = new ApiService();

    // 使用示例
    const getRooms = async () => {
    const response = await apiService.get<Room[]>("/rooms", {
    params: { page: 1, size: 10 },
    });
    return response.data; // 类型为 Room[]
    };
  3. 定义统一的响应包装类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    export interface ApiResponse<T> {
    code: number;
    message: string;
    data: T;
    }

    export interface ApiError {
    code: number;
    message: string;
    details?: any;
    }
  4. 拦截器的 TypeScript 类型扩展

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 扩展 AxiosRequestConfig
    export interface CustomAxiosRequestConfig extends AxiosRequestConfig {
    showLoading?: boolean;
    showError?: boolean;
    }

    // 在拦截器中使用
    apiClient.interceptors.request.use((config: CustomAxiosRequestConfig) => {
    if (config.showLoading !== false) {
    // 显示加载状态
    }
    return config;
    });
  5. 处理不统一的后端返回结构 使用类型守卫确保类型安全。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function isApiResponse<T>(data: any): data is ApiResponse<T> {
    return (
    data && typeof data === "object" && "code" in data && "data" in data
    );
    }

    apiClient.interceptors.response.use((response) => {
    if (isApiResponse(response.data)) {
    return response.data;
    } else {
    return { code: 200, message: "success", data: response.data };
    }
    });

应用及其后期实践

  1. 封装后在业务组件中的使用 封装后调用更加简洁、安全,无需重复处理 Token、错误或类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 未封装前
    const fetchRooms = async () => {
    try {
    const response = await axios.get("/api/rooms");
    return response.data;
    } catch (error) {
    console.error(error);
    }
    };

    // 封装后
    import { roomApi } from "@/api";

    const fetchRooms = async () => {
    try {
    const response = await roomApi.getRooms({ page: 1, size: 10 });
    return response.data;
    } catch (error) {
    // 错误已统一处理
    }
    };
  2. OpenAPI/Swagger 自动生成类型和接口函数 可使用 openapi-typescript-codegen 或 swagger-typescript-api 等工具从后端 OpenAPI 文档自动生成类型定义和请求函数,实现前后端类型完全一致,显著减少手动维护成本。