TypeScript 封装 Axios 与全局拦截器

在日常开发中,很多初学者使用 Axios 时通常是直接 axios.get("/api/user"),然后拿到的 res.data 默认就是一个毫无提示的 any 类型。如果在业务组件里到处充斥着这种 any,那么我们费尽心思在项目中引入 TypeScript 就毫无意义了。

一个真正成熟的架构,必须要将网络请求封装成一个“黑盒”:只要参数传进去是安全的,出来的数据就必须是带着完美类型的。

统一数据格式:定义前后端交互的 “契约”

在封装 axios 之前,我们必须先和后端定下一个规矩。通常情况下,企业级后端返回的 JSON 数据都是有固定外层结构的(比如一定包含 code、message 和 data)。

我们需要用一个泛型接口把这个外层结构框死。

示例 1:定义全局响应结构

// src/api/types.ts

// 定义统一的 API 响应结构
// 使用泛型 <T> 来代表真正核心的业务数据
export interface ApiResponse<T = any> {
    code: number;
    message: string;
    data: T;
}

分析:

通过定义 ApiResponse<T>,我们相当于做了一个通用的 “快递包装盒”。不管后端寄来的是用户信息(User)还是商品列表(Product[]),外层的包装结构永远是一致的,变化的是装在 data 里的泛型 T。

配置拦截器:自动处理 Token、Loading 动画与错误提示

为了方便管理多个不同的 API 实例(比如有的请求用户系统,有的请求支付系统),大厂通用的做法是使用 ES6 的 class 对 axios 进行面向对象封装。

示例 2:封装 axios 核心类与拦截器

// src/api/http.ts
import axios from "axios";
// 修正:Axios 官方导出的类型首字母均为大写
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from "axios";
import { ApiResponse } from "./types";

// 补充模块扩展声明:解决 config.showLoading 导致的 TS 报错问题
declare module "axios" {
    // 扩展基础配置(供实例化和发起请求时使用)
    export interface AxiosRequestConfig {
        showLoading?: boolean;
    }
    // 扩展内部配置(供请求拦截器使用,axios 1.x+ 必须)
    export interface InternalAxiosRequestConfig {
        showLoading?: boolean;
    }
}

class Http {
    private instance: AxiosInstance;

    constructor(config: AxiosRequestConfig) {
        this.instance = axios.create(config);
        this.setupInterceptors();
    }

    private setupInterceptors() {
        // 1. 请求拦截器:注意这里必须指定为 InternalAxiosRequestConfig
        this.instance.interceptors.request.use(
            (config: InternalAxiosRequestConfig) => {
                // 读取自定义扩展属性
                if (config.showLoading) {
                    console.log("[系统]: 开启全局 Loading 动画...");
                }

                const token = localStorage.getItem("token");
                if (token && config.headers) {
                    config.headers.Authorization = "Bearer " + token;
                }
                return config;
            },
            (error: AxiosError) => Promise.reject(error)
        );

        // 2. 响应拦截器
        this.instance.interceptors.response.use(
            (response: AxiosResponse) => {
                console.log("[系统]: 关闭全局 Loading 动画...");

                // 将原生响应体强转为约定结构
                const resData = response.data as ApiResponse<any>;

                if (resData.code !== 200) {
                    console.error("[系统提示]: ", resData.message);
                    return Promise.reject(new Error(resData.message));
                }

                // 剥离外层包装,返回核心业务数据
                return resData.data;
            },
            (error: AxiosError) => {
                console.error("[网络异常]: 请检查网络设置");
                return Promise.reject(error);
            }
        );
    }
}

分析:

在这个类的 setupInterceptors 方法中,我们做了一件非常重要的事情:在响应拦截器里直接把外层的 axios 包装,甚至是后端的 ApiResponse 包装全给扒掉了(return resData.data)。

这意味着,我们的业务代码后续拿到的将直接是最纯粹的核心业务数据(泛型 T),再也不用痛苦地去写 res.data.data 了。

改造 GET 与 POST 方法:让返回的数据自带完美类型提示

实例创建好之后,我们需要暴露 get 和 post 方法给业务层使用。这也是整个网络层封装技术含量最高的地方:强制类型剥离

示例 3:利用泛型重写请求方法

// 接着上面的 Http 类继续编写内部方法:
class Http {
    // ... constructor 和 setupInterceptors 代码省略

    // 泛型穿透:直接返回业务数据类型 Promise<T>
    public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
        return this.instance.get(url, config) as Promise<T>;
    }

    public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
        return this.instance.post(url, data, config) as Promise<T>;
    }
}

// 导出最终的实例
export const request = new Http({
    baseURL: "https://api.lvyenet.com",
    timeout: 10000
});

分析:

为什么我们要在这里使用 as Promise<T> 进行强转? 因为在拦截器中,我们已经把数据剥离成了纯正的业务结构。我们要求调用者在发起请求时(比如 request.get<User>("/user")),直接告诉我们这个接口最终会返回什么业务数据。这种“上帝视角”的泛型穿透,是企业级架构的基石。

实战演练:在 Vue 或 React 组件中优雅地发起请求

现在,让我们来到 Vue 或 React 的业务组件中,看看这套 “类型铠甲” 是如何保护我们的代码的。

示例 4:业务层的完美调用

import { request } from "@/api/http";

// 定义具体的业务数据模型
interface UserInfo {
    id: number;
    username: string;
    avatar: string;
}

async function fetchUserProfile() {
    try {
        // 发起请求,强行注入 UserInfo 泛型
        // 并开启自定义的 showLoading 动画
        const user = await request.get<UserInfo>("/api/user/profile", {
            showLoading: true
        });

        // user 已被精准推断为 UserInfo 类型
        // 没有 res.data,也没有 any
        console.log("欢迎回来:", user.username);

    } catch (error) {
        console.log("请求失败处理...");
    }
}

进阶技巧:如何中途取消正在进行的网络请求

在现代 fetch 和 axios 中,取消请求的标准做法是使用 AbortController。我们可以在扩展的配置中加入信号量,让类型和请求控制完美结合。

具体用法:

import { request } from "@/api/http";

const controller = new AbortController();

// 发起请求时传入 signal
request.get<UserInfo>("/api/user/profile", {
    signal: controller.signal
}).catch(err => {
    if (err.name === "CanceledError") {
        console.log("请求已被用户主动取消");
    }
});

// 在需要的时候(如用户快速切换页面或狂点按钮),调用取消方法
controller.abort();

上一篇: TypeScript 结合 Vite

下一篇: JavaScript 教程

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号