鸿蒙应用网络层封装实践:统一请求、错误分级与无感重试
网络请求是鸿蒙应用里最容易写出“面条代码”的地方:页面里直接发请求、错误弹窗满天飞、超时和重试各写一套。本文给一套可落地的网络层封装思路,把请求发送、错误分级、重试和登录态处理收口到一个模块里,适合正在做第一个联网应用的开发者。
一、为什么页面里不该直接发请求
在组件里直接调用 http.createHttp 有三个典型问题:
- 请求参数、地址、鉴权头散落在各个页面,改一处要全局搜;
- 错误处理各写各的,用户一会儿看到 toast、一会儿看到弹窗;
- 会话过期后的跳转逻辑重复实现,很容易漏。
网络层封装的目标就是把这三件事收口:页面只关心“调哪个接口、拿什么数据”,其余全部交给统一模块。
二、统一请求入口
先定义一个请求函数,所有业务接口都从它走:
enum ApiError {
NETWORK = 'NETWORK_ERROR',
TIMEOUT = 'TIMEOUT',
UNAUTHORIZED = 'UNAUTHORIZED',
SERVER = 'SERVER_ERROR',
BUSINESS = 'BUSINESS_ERROR'
}
interface ApiResult<T> {
ok: boolean;
data?: T;
error?: {
code: ApiError;
message: string;
retryable: boolean;
};
}
async function request<T>(
path: string,
options: RequestOptions
): Promise<ApiResult<T>> {
const token = await SessionManager.getValidToken();
const controller = new httpHttpRequest();
try {
const resp = await controller.request(`${BASE_URL}${path}`, {
method: options.method,
header: {
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : ''
},
extraData: options.body ? JSON.stringify(options.body) : undefined,
connectTimeout: 10000,
readTimeout: 15000,
expectDataType: http.HttpDataType.OBJECT
});
return parseResponse<T>(resp);
} catch (e) {
return parseException(e);
} finally {
controller.destroy();
}
}
两个细节值得注意:
- http 对象用完必须 destroy,否则连接堆积;
- 超时时间显式设置,不要依赖默认值,弱网下默认值往往过长。
三、错误分级:先分类,再处理
把所有错误归到四类,处理策略各不相同:
| 类型 | 判定 | 处理策略 |
|---|---|---|
| 网络错误 | 请求未到达服务器 | 提示检查网络,可重试 |
| 超时 | 连接或读取超时 | 静默重试一次,再失败才提示 |
| 会话过期 | 返回 401 | 清理会话,跳转登录页 |
| 业务错误 | 返回 200 但 code 非 0 | 直接把服务端 message 透出 |
function parseResponse<T>(resp: http.HttpResponse): ApiResult<T> {
if (resp.responseCode === 401) {
SessionManager.clear();
emitter.emit('session-expired');
return { ok: false, error: { code: ApiError.UNAUTHORIZED, message: '登录已过期', retryable: false } };
}
if (resp.responseCode >= 500) {
return { ok: false, error: { code: ApiError.SERVER, message: '服务暂时不可用', retryable: true } };
}
const body = resp.result as ResponseBody<T>;
if (body.code !== 0) {
return { ok: false, error: { code: ApiError.BUSINESS, message: body.message, retryable: false } };
}
return { ok: true, data: body.data };
}
关键设计:会话过期不要在请求函数里直接跳转页面,而是发一个全局事件,由应用级组件统一处理。否则多个并发请求同时过期时,会连续跳转多次。
四、无感重试:只对值得的错误重试
重试最容易写出问题:业务错误也重试、无限重试、重复提交。原则是——只有“结果可预期一致”的请求才自动重试。
async function requestWithRetry<T>(
path: string,
options: RequestOptions
): Promise<ApiResult<T>> {
let result = await request<T>(path, options);
if (result.ok || !result.error?.retryable) {
return result;
}
await sleep(1000);
return request<T>(path, options);
}
三条边界必须守住:
- GET 可以自动重试,POST 默认不重试,除非服务端提供了幂等键;
- 重试只做一次,失败就交给用户决策;
- 重试前加短暂延迟,避免服务端刚恢复就被打爆。
五、并发请求的会话刷新
多个请求同时遇到 401 时,如果每个都去刷新会话,必然冲突。标准做法是加一把“刷新锁”:
let refreshing: Promise<string> | null = null;
async function ensureToken(): Promise<string> {
const token = await SessionManager.getValidToken();
if (token) {
return token;
}
if (!refreshing) {
refreshing = refreshSession().finally(() => {
refreshing = null;
});
}
return refreshing;
}
所有请求入口都走 ensureToken,第一个过期请求负责刷新,其余请求等待同一个 Promise,天然去重。
六、页面侧的正确姿势
封装完成后,页面代码会变得非常薄:
async loadBills() {
this.loading = true;
const result = await requestWithRetry<Bill[]>('/bills', { method: 'GET' });
this.loading = false;
if (result.ok) {
this.bills = result.data;
} else if (result.error?.code !== ApiError.UNAUTHORIZED) {
promptAction.showToast({ message: result.error?.message ?? '加载失败' });
}
}
注意这里对 UNAUTHORIZED 的静默处理:跳转登录页已经由全局事件接管,页面不再重复弹提示。
七、几个容易踩的坑
- 在 build 或 aboutToAppear 里发请求但不处理组件销毁:页面已切走,回调还在写状态,轻则报错重则崩溃,请求要与组件生命周期绑定;
- 把 http 实例做成全局单例复用:并发请求会互相干扰,一次请求一个实例更安全;
- 错误提示直接透传服务端原文:服务端文案往往不适合直接展示,重要错误要有本地文案兜底;
- 忘记处理“成功但数据为空”:列表页要区分“加载失败”和“确实没有数据”,两者的空态 UI 不一样。
八、小结
网络层封装的本质是“把变化收口”:地址会变、鉴权会变、文案会变,但“请求-分类-处理”的骨架不变。先把统一入口和错误分级做扎实,重试和会话并发是后面自然长出来的能力。
建议从自己应用里最痛的那个接口开始重构:把它接到统一入口上,跑通后再批量迁移,风险最小,收益立竿见影。
- 点赞
- 收藏
- 关注作者
评论(0)