在日常开发中,很多初学者使用 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();