鸿蒙网络请求封装:基于 Axios 开发一个支持“自动刷新 Token”的 HTTP 客户端
原生代码冗余:每次都要写extraData,还要手动解析 JSON。缺乏拦截器:想给所有请求统一加 Header 或者统一处理错误,只能写个包装函数,很难维护。类型弱:返回的数据全是string,需要手动JSON.parse并断言类型。引入后,我们不仅能复用前端的拦截器思维,还能利用 ArkTS 的泛型系统实现类型安全的网络层。为了让调用者用得爽,我们先定义好返回结构。// 后端返回的标准结构da
·
标签: #HarmonyOS #Axios #网络请求 #ArkTS #Token刷新 #架构设计
📉 前言:为什么原生 http 模块不够用?
原生 http.createHttp() 的痛点:
- 代码冗余:每次都要写
extraData、connectTimeout,还要手动解析 JSON。 - 缺乏拦截器:想给所有请求统一加 Header 或者统一处理错误,只能写个包装函数,很难维护。
- 类型弱:返回的数据全是
string,需要手动JSON.parse并断言类型。
引入 @ohos/axios 后,我们不仅能复用前端的拦截器思维,还能利用 ArkTS 的泛型系统实现类型安全的网络层。
🏗️ 一、 核心逻辑:Token 自动刷新流程
这是本篇的重难点。当多个并发请求同时触发 401 时,我们不能发起多次刷新请求,而是应该加锁。
并发控制流程图 (Mermaid):
🛠️ 二、 环境准备
安装鸿蒙版 Axios:
ohpm install @ohos/axios
💻 三、 代码实战:企业级封装
我们新建一个 AxiosRequest.ts 文件。
1. 定义基础结构与类型
为了让调用者用得爽,我们先定义好返回结构。
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from '@ohos/axios';
// 后端返回的标准结构
interface BaseResponse<T> {
code: number;
message: string;
data: T;
}
// 扩展 Axios 配置,允许传递自定义参数(如:是否需要 Loading)
interface CustomRequestConfig extends AxiosRequestConfig {
showLoading?: boolean;
}
2. 实现单例类与请求拦截
请求拦截器的作用很简单:有 Token 就带上。
class AxiosHttpRequest {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
// --- 请求拦截器 ---
this.instance.interceptors.request.use(
(config: CustomRequestConfig) => {
// 从 AppStorage 或 Preferences 获取 Token
const token = AppStorage.Get<string>('accessToken');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器在下一步实现...
}
}
3. 核心:响应拦截与无感刷新 (The Magic)
这里我们需要两个辅助变量:
isRefreshing: 防止多次调用刷新接口。requestsQueue: 存储在刷新期间进来的其他请求。
// ... 类内部变量
private isRefreshing = false;
private requestsQueue: Function[] = [];
// ... 在 constructor 中继续添加响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
// 这里的逻辑根据你们后端业务码来定
// 假设 http status 200 但 code 401 也是 token 过期
const res = response.data as BaseResponse<any>;
if (res.code === 401) {
return this.handle401Error(response.config);
}
return response;
},
(error: AxiosError) => {
// 处理 HTTP 状态码为 401 的情况
if (error.response?.status === 401) {
return this.handle401Error(error.config);
}
return Promise.reject(error);
}
);
// --- 处理 401 的核心逻辑 ---
private handle401Error(originConfig: AxiosRequestConfig) {
if (!this.isRefreshing) {
this.isRefreshing = true;
// 1. 发起刷新 Token 请求 (注意:这里最好用一个新的 axios 实例,避免死循环)
return this.refreshToken()
.then((newToken) => {
// 2. 刷新成功,保存新 Token
AppStorage.SetOrCreate('accessToken', newToken);
// 3. 修改原请求的 Header
originConfig.headers['Authorization'] = `Bearer ${newToken}`;
// 4. 执行队列中的请求
this.requestsQueue.forEach(cb => cb(newToken));
this.requestsQueue = [];
// 5. 重发当前请求
return this.instance(originConfig);
})
.catch((err) => {
// 6. 刷新也失败了?那是真的过期了,去登录页吧
this.requestsQueue = [];
// router.pushUrl({ url: 'pages/Login' })
return Promise.reject(err);
})
.finally(() => {
this.isRefreshing = false;
});
} else {
// 如果正在刷新,则把当前请求挂起,放入队列
return new Promise((resolve) => {
this.requestsQueue.push((newToken: string) => {
originConfig.headers['Authorization'] = `Bearer ${newToken}`;
resolve(this.instance(originConfig));
});
});
}
}
// 模拟刷新 Token 的接口
private async refreshToken(): Promise<string> {
// 实际业务中这里调用后端刷新接口
// const refreshToken = AppStorage.Get('refreshToken');
return "new_generated_token_123";
}
4. 封装便捷方法 (GET/POST)
最后,暴露简单易用的 API。
// T 是返回数据的类型,D 是请求参数的类型
public get<T>(url: string, params?: any): Promise<T> {
return this.instance.get<BaseResponse<T>>(url, { params })
.then(res => res.data.data);
}
public post<T>(url: string, data?: any): Promise<T> {
return this.instance.post<BaseResponse<T>>(url, data)
.then(res => res.data.data);
}
}
// 导出单例
export const http = new AxiosHttpRequest();
🚀 四、 调用演示:丝滑体验
在你的 UI 组件 (.ets) 中:
import { http } from '../utils/AxiosRequest';
interface UserProfile {
id: number;
name: string;
}
@Entry
@Component
struct ProfilePage {
@State user: UserProfile | null = null;
async aboutToAppear() {
try {
// 泛型支持:res 自动推断为 UserProfile 类型
// 哪怕 Token 过期,这里也会自动重试并成功返回
const res = await http.get<UserProfile>('/user/profile');
this.user = res;
console.info('用户名称:', res.name);
} catch (error) {
console.error('请求失败:', error);
}
}
build() {
// UI ...
}
}
🎯 总结
通过这次封装,我们实现了:
- 代码解耦:UI 层不需要关心 Token 怎么传,也不需要关心 401 怎么处理。
- 类型安全:利用 TypeScript 泛型,接口返回什么类型,代码里就是什么类型。
- 极致体验:Request Queue (请求队列) 的设计,确保了在并发请求场景下,Token 刷新接口只会被调用一次,避免了资源浪费和逻辑错误。
这是鸿蒙 App 开发中性价比最高的基建工作之一。
Next Step:
现在的封装还没处理 Loading 动画。试着修改 Interceptor,在请求开始时调用 promptAction.showToast 或自定义 Loading 组件,请求结束时关闭它,实现全局自动 Loading。
更多推荐




所有评论(0)