Axios 封装实践
结合 DailyUse 的实现,说明 Axios 在企业级项目里的实例、拦截器、刷新队列和 Result 模式封装方式。
[!info] related notes
- 所属 MOC: HTTP 与前端网络请求
- 前置笔记: Axios, Axios 实例, Axios 拦截器, Axios 错误模型
- 相关笔记: Axios 封装模式, 封装一个优雅的axios, 前端HTTP状态码的处理, Axios 请求配置, Axios 取消请求, Axios 数据序列化
Axios 封装实践
本指南整理 DailyUse 项目中 Axios 的架构设计、核心实现及其背后的知识点,重点是“怎么把 Axios 包成一个稳定的业务层客户端”。
1. 项目架构概览
在 DailyUse 项目中,Axios 的封装被集中在 @dailyuse/http-client 包中。这种设计实现了「一次编写,到处运行」的原则,确保了核心逻辑(如 Token 注入、刷新、错误映射)在 Web、Desktop 或其他客户端平台上保持高度一致。
核心包结构
packages/http-client: 核心逻辑库。axios-instance.ts: 基础工厂函数,负责创建 Axios 实例及请求侧拦截。axios-http-client.ts: 面向 DI(依赖注入)设计的传统客户端,返回Promise<T>。result-http-client.ts: 封装了 Result 模式的客户端,返回Promise<Result<T>>。src/types.ts: 类型定义与接口契约。
2. 核心知识点与项目实现
2.1 Axios 实例工厂
知识点:通过 axios.create() 创建独立实例,可以避免污染全局配置,并能根据不同业务场景定制 baseURL、timeout。
项目实现:
const instance = axios.create({
baseURL,
timeout,
headers: { 'Content-Type': 'application/json' },
...axiosConfig, // 允许透传额外配置
});
2.2 请求拦截器:Token 注入
知识点:在请求发送前统一处理逻辑,最常见的场景是注入身份令牌(Bearer Token)。
项目实现:
项目使用了一个 TokenProvider 接口来延迟获取 Token,确保在 Token 变化时(如登录成功后),拦截器总是能拿到最新的值。
instance.interceptors.request.use((requestConfig) => {
if (tokenProvider) {
const token = tokenProvider.getAccessToken();
if (token && requestConfig.headers) {
requestConfig.headers.Authorization = `Bearer ${token}`; // 统一注入
}
}
return requestConfig;
});
2.3 响应拦截器:数据剥离与错误映射
知识点:后端返回的数据通常带有业务信封。通过响应拦截器,可以直接剥离信封,让业务层只处理纯数据内容。
项目实现:
- 成功处理:检查
data.ok,若为true则返回data.data。 - 错误处理:将 HTTP 状态码(404, 500 等)和后端自定义错误码转换为统一的
HttpClientError对象。
3. 进阶核心:401 并发刷新 Token
这是 Axios 封装中最具挑战性也最面试高频的知识点。
痛点
当 Access Token 过期时,API 返回 401。此时如果页面上同时发出了 5 个请求,这 5 个请求都会收到 401。
DailyUse 的解决方案(队列模式)
- 并发加锁:第一个 401 请求进入时,将
isRefreshing标记为true,并调用onTokenRefresh。 - 请求排队:后续接到的 401 请求不立即抛错,而是存入一个
refreshQueue。 - 刷新重试:当 Token 刷新成功后,遍历
refreshQueue,用新 Token 修改请求头,并重新执行这些请求。
核心代码实现逻辑:
private async handleTokenRefresh(originalConfig) {
if (this.isRefreshing) {
// 1. 已在刷新中 → 排队等待 Promise
return new Promise((resolve, reject) => {
this.refreshQueue.push({ resolve, reject });
}).then((newToken) => {
originalConfig.headers.Authorization = `Bearer ${newToken}`;
return this.axios.request(originalConfig); // 使用新 Token 重试原请求
});
}
this.isRefreshing = true;
try {
const newToken = await this.onTokenRefresh!(); // 2. 调用外部(Pinia)刷新逻辑
// 3. 成功后遍历队列通知解冻 …
return this.axios.request(originalConfig);
} finally {
this.isRefreshing = false;
}
}
4. 设计模式:Result 模式 vs 传统报错
项目提供了两种客户端选择,适应不同的开发风格:
| 方案 | 所在类 | 业务层写法 | 优点 |
|---|---|---|---|
| 传统模式 | AxiosHttpClient | try { await api() } catch(e) {} | 符合大部分人的习惯,适用于简单的命令式逻辑。 |
| Result 模式 | ResultHttpClient | const res = await api(); if(res.ok) { … } | 项目推荐。通过返回对象而非抛错,利用 TS 类型守卫实现强制错误处理。 |
Result 模式优势示例:
const result = await resultHttpClient.get<User>('/profile');
if (result.ok) {
// TS 自动推断 result.data 的类型为 User
userProfile.value = result.data;
} else {
// 无论是 401、500 还是网络异常都走这里,IDE 会提示 result.error 是可选的
notify(result.error.message);
}
5. Web 应用中的深度集成
在 apps/web 中,我们将 ResultHttpClient 与 Pinia 状态管理器紧密结合。
集成要点:
- Token 延迟读取:通过回调函数在请求瞬间从
authenticationStore获取 Token(解决循环依赖)。 - 刷新闭环:
- 刷新时使用原生的
fetch而非自身的httpClient,防止递归死循环。 - 刷新成功后使用
store.$patch同步更新本地状态。
- 刷新时使用原生的
- 降级处理:如果刷新依然失败(Refresh Token 也过期),则调用
onUnauthorized清空 Store 并跳转至登录页。
开发最佳实践
- 统一出口:不要在业务代码中直接
import axios。 - 数据类型契约:利用类型定义确保前后端字段对齐。
- 关注性能:公开接口可以按需跳过鉴权拦截。
文档生成于 2026-04-07 | DailyUse 架构组