HarmonyOS 网络请求稳定性实战:超时、重试、错误分层与弱网兜底
HarmonyOS 网络请求稳定性实战:超时、重试、错误分层与弱网兜底
移动端网络问题最麻烦的地方,不是接口本身不可用,而是它经常表现得“不稳定”:地铁里请求转圈、弱网下重复点击、服务端返回了业务错误但页面只提示“失败”、登录失效后多个接口同时弹窗。用户看到的是体验差,开发者排查时看到的是一堆零散日志。
这篇文章不把网络请求当成一个 request() 调用来讲,而是把它拆成一条可维护链路:统一请求入口、超时边界、错误分层、克制重试、弱网缓存兜底和请求留账。示例代码以 ArkTS 写法表达,具体 HTTP 能力可按项目实际接入 HarmonyOS 的网络请求 API 或团队已有网络库。

1. 先确定这篇文章要解决的网络问题
实际项目里,网络层常见问题可以先分成四类,不要一上来就把所有失败都丢给页面处理。
| 问题 | 用户看到的现象 | 工程上应该做的事 |
|---|---|---|
| 请求没有边界 | loading 长时间不结束 | 统一设置超时,并给页面返回可理解状态 |
| 错误没有分类 | 所有失败都叫“请求失败” | 区分网络错误、业务错误、鉴权错误、解析错误 |
| 重试太激进 | 弱网下接口被打爆 | 只重试可恢复错误,并使用退避间隔 |
| 弱网没有兜底 | 页面空白或数据闪没 | 优先保留可用缓存,再提示刷新失败 |
本文的目标是让网络层对页面交付一个稳定结果,而不是把所有细节都泄漏给 UI。

2. 资料边界和项目落点
写网络稳定性时,建议先看三类资料:官方网络请求能力、项目内已有 HTTP 封装、线上错误日志。这里的示例不会绑定某一个业务域名,重点放在工程结构。
| 资料 | 用途 |
|---|---|
| 华为开发者文档中心:https://developer.huawei.com/consumer/cn/doc/ | 确认 HarmonyOS 能力入口和 API 版本边界 |
| HarmonyOS 指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ | 查网络、数据、日志等能力的官方说明 |
| 项目网络封装文件 | 确认是否已经有 baseURL、header、token、错误码处理 |
| 线上接口日志 | 找出超时、401、业务错误、解析失败的真实比例 |
如果项目已经有网络库,不建议推翻重写。更稳妥的做法是在现有网络库外侧加稳定性边界:请求模型、错误映射、重试策略和结果记录。
3. 请求模型要先收口
网络层最怕每个页面自己拼 URL、自己传 header、自己判断错误。先定义请求模型,让页面只描述“我要什么”,不要关心底层细节。
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
interface StableRequest<TBody = Record<string, Object>> {
requestId: string;
path: string;
method: HttpMethod;
body?: TBody;
timeoutMs: number;
retryable: boolean;
cacheKey?: string;
}
interface StableResponse<TData> {
requestId: string;
data?: TData;
fromCache: boolean;
costMs: number;
}
这段代码负责请求边界:requestId 用来串联日志,timeoutMs 决定等待上限,retryable 控制是否允许重试,cacheKey 表示失败后是否能用缓存兜底。页面不再直接拼接底层参数,后面换网络库也不会影响调用方。
4. 统一入口处理超时和请求耗时
稳定请求的第一步是统一入口。不要让页面自己 setTimeout,也不要让不同接口有不同的默认等待时间。
class StableHttpClient {
private readonly baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async send<TData>(request: StableRequest): Promise<StableResponse<TData>> {
const startAt = Date.now();
const task = this.performRequest<TData>(request);
const data = await this.withTimeout(task, request.timeoutMs, request.requestId);
return {
requestId: request.requestId,
data,
fromCache: false,
costMs: Date.now() - startAt,
};
}
private async performRequest<TData>(request: StableRequest): Promise<TData> {
const url = `${this.baseUrl}${request.path}`;
// 这里替换为项目实际网络请求能力,例如 HarmonyOS 网络请求 API 或团队已有 HTTP 封装。
return await Promise.resolve(JSON.parse(`{"url":"${url}"}`) as TData);
}
private async withTimeout<TData>(
task: Promise<TData>,
timeoutMs: number,
requestId: string
): Promise<TData> {
let timer = 0;
const timeoutTask = new Promise<TData>((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`NETWORK_TIMEOUT:${requestId}:${timeoutMs}`));
}, timeoutMs);
});
try {
return await Promise.race([task, timeoutTask]);
} finally {
clearTimeout(timer);
}
}
}
这段代码的重点不是 Promise.race 本身,而是把“等多久算失败”变成请求模型的一部分。输入只信任 StableRequest,失败时抛出带 requestId 的超时异常,下一层 ErrorMapper 可以继续处理。
5. ErrorMapper 不让页面猜错误原因
如果网络层只返回 Error,页面就只能猜。更稳的做法是把错误转换成明确类型。
type NetworkFailureKind =
| 'timeout'
| 'offline'
| 'server'
| 'auth'
| 'business'
| 'decode';
interface NetworkFailure {
requestId: string;
kind: NetworkFailureKind;
message: string;
canRetry: boolean;
shouldLogout: boolean;
}
class ErrorMapper {
map(request: StableRequest, error: Error): NetworkFailure {
const raw = error.message;
if (raw.startsWith('NETWORK_TIMEOUT')) {
return {
requestId: request.requestId,
kind: 'timeout',
message: '当前网络较慢,请稍后重试',
canRetry: request.retryable,
shouldLogout: false,
};
}
if (raw.includes('401')) {
return {
requestId: request.requestId,
kind: 'auth',
message: '登录状态已过期,请重新登录',
canRetry: false,
shouldLogout: true,
};
}
return {
requestId: request.requestId,
kind: 'server',
message: '服务暂时不可用,请稍后再试',
canRetry: request.retryable,
shouldLogout: false,
};
}
}
这一层不直接弹窗,也不直接跳登录页。它只负责把底层异常转换成业务可理解的结果:能不能重试、是否需要退出登录、用户提示文案是什么。页面只消费明确状态,避免每个页面写一套错误判断。
6. 重试策略要克制,不要把弱网变成雪崩
重试不是越多越好。登录、支付、提交订单这类接口不应该随便重试;列表、配置、推荐数据可以在可控次数内重试。
interface RetryDecision {
shouldRetry: boolean;
delayMs: number;
reason: string;
}
class RetryPolicy {
decide(failure: NetworkFailure, attempt: number): RetryDecision {
if (!failure.canRetry) {
return { shouldRetry: false, delayMs: 0, reason: 'request_not_retryable' };
}
if (failure.kind === 'auth' || failure.kind === 'business') {
return { shouldRetry: false, delayMs: 0, reason: `not_recoverable_${failure.kind}` };
}
if (attempt >= 2) {
return { shouldRetry: false, delayMs: 0, reason: 'retry_limit_reached' };
}
const delayMs = 400 * Math.pow(2, attempt);
return { shouldRetry: true, delayMs, reason: `retry_after_${delayMs}` };
}
}
async function sleep(delayMs: number): Promise<void> {
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
}
这里把最大重试次数控制在 2 次,并且使用退避间隔。它防止的是弱网下接口被页面连续触发,也防止业务错误被错误地重复提交。输入来自 ErrorMapper,下一层可以按 RetryDecision 决定是否再次请求。
7. 弱网兜底不要把旧数据直接当新数据
弱网兜底可以提升体验,但必须明确告诉页面数据来源。否则用户可能把旧数据当成最新结果。
interface CachedPayload<TData> {
data: TData;
savedAt: number;
expireAt: number;
}
class NetworkFallbackStore<TData> {
private readonly memory = new Map<string, CachedPayload<TData>>();
save(cacheKey: string, data: TData, ttlMs: number): void {
const now = Date.now();
this.memory.set(cacheKey, {
data,
savedAt: now,
expireAt: now + ttlMs,
});
}
read(cacheKey: string): CachedPayload<TData> | undefined {
const cached = this.memory.get(cacheKey);
if (!cached) {
return undefined;
}
if (cached.expireAt < Date.now()) {
this.memory.delete(cacheKey);
return undefined;
}
return cached;
}
}
这段代码只演示内存兜底,真实项目可以替换为 Preferences、RDB 或文件缓存。关键点是 savedAt 和 expireAt:页面可以显示“数据来自缓存,正在尝试刷新”,而不是静默展示旧数据。
8. 把请求、错误、重试和缓存串起来
单个类很难解决网络稳定性,真正起作用的是编排层。
class StableNetworkGateway<TData> {
constructor(
private readonly client: StableHttpClient,
private readonly mapper: ErrorMapper,
private readonly retry: RetryPolicy,
private readonly fallback: NetworkFallbackStore<TData>
) {}
async request(request: StableRequest): Promise<StableResponse<TData>> {
let attempt = 0;
while (true) {
try {
const response = await this.client.send<TData>(request);
if (request.cacheKey && response.data !== undefined) {
this.fallback.save(request.cacheKey, response.data, 5 * 60 * 1000);
}
return response;
} catch (e) {
const failure = this.mapper.map(request, e as Error);
const decision = this.retry.decide(failure, attempt);
if (decision.shouldRetry) {
attempt += 1;
await sleep(decision.delayMs);
continue;
}
if (request.cacheKey) {
const cached = this.fallback.read(request.cacheKey);
if (cached) {
return {
requestId: request.requestId,
data: cached.data,
fromCache: true,
costMs: Date.now() - cached.savedAt,
};
}
}
throw new Error(`${failure.kind}:${failure.message}`);
}
}
}
}
这段编排代码承担完整边界:先请求,失败后映射错误,再按策略重试,最后才读缓存兜底。它防止页面到处写 try/catch,也让所有网络失败都能被统一记录。

9. 请求结果要留账,排查时才有线索
线上问题最怕只有一句“用户说打不开”。每个请求都应该留下最小可排查记录。
interface NetworkRecord {
requestId: string;
path: string;
result: 'success' | 'cache' | 'failed';
costMs: number;
failureKind?: NetworkFailureKind;
retryCount: number;
createdAt: number;
}
class NetworkLedger {
private readonly records: NetworkRecord[] = [];
append(record: NetworkRecord): void {
this.records.push(record);
if (this.records.length > 100) {
this.records.shift();
}
}
latest(): NetworkRecord[] {
return this.records.slice().reverse();
}
}
这类记录不应该包含 token、手机号、完整地址等敏感信息。它只保留定位问题必要字段:接口路径、耗时、失败类型、重试次数和时间。后续如果接入日志系统,也可以从这里统一上报。
10. 页面层只处理四种状态
页面不应该知道底层是超时、DNS、HTTP 还是 JSON 解析,它只需要可展示状态。
type NetworkViewState<TData> =
| { type: 'loading' }
| { type: 'content'; data: TData; fromCache: boolean }
| { type: 'empty'; message: string }
| { type: 'error'; message: string; canRetry: boolean };
function toViewState<TData>(response?: StableResponse<TData>, error?: Error): NetworkViewState<TData> {
if (response?.data) {
return {
type: 'content',
data: response.data,
fromCache: response.fromCache,
};
}
if (error) {
return {
type: 'error',
message: error.message,
canRetry: !error.message.includes('auth'),
};
}
return { type: 'empty', message: '暂无数据' };
}
页面只围绕 loading/content/empty/error 渲染,弱网缓存通过 fromCache 告诉用户“当前展示缓存数据”。这样网络层再复杂,UI 状态也不会失控。
11. 网络稳定性验证动作
本地验证不要只测“正常网络能请求成功”,还要刻意制造失败场景。
| 验证场景 | 操作方式 | 预期结果 |
|---|---|---|
| 接口超时 | 把超时设为 1ms 或 mock 延迟 | 页面结束 loading,出现可理解提示 |
| 可恢复错误 | mock 网络异常 | 按退避策略最多重试 2 次 |
| 业务错误 | mock 业务码失败 | 不重试,返回业务提示 |
| 登录失效 | mock 401 | 不重试,触发统一登录处理 |
| 弱网缓存 | 先成功一次,再断网刷新 | 展示缓存,并标记缓存来源 |
建议把这些场景做成调试开关或单元测试用例。网络层越早能复现失败,线上排查越少依赖猜测。
12. 常见网络问题排查表
| 现象 | 优先检查 | 处理方式 |
|---|---|---|
| loading 不结束 | 是否所有请求都走统一超时 | 禁止页面绕过 StableHttpClient |
| 重复弹登录 | 401 是否被多个页面各自处理 | 收口到 ErrorMapper 和登录协调器 |
| 弱网请求越来越多 | 重试次数和间隔是否可控 | 只对可恢复错误退避重试 |
| 页面显示旧数据但无提示 | fromCache 是否传到 UI |
页面增加缓存状态提示 |
| 排查不到失败接口 | 是否记录 requestId 和路径 |
在 NetworkLedger 增加最小留账 |
排查顺序建议从“是否绕过统一入口”开始。如果入口不统一,后面的错误分层、重试和日志都会被打散。
13. 发布前的网络链路检查
发布前不要只看页面是否能打开,要留下可复核的网络验收记录。建议每次发版前选 3 个核心接口:一个列表接口、一个详情接口、一个提交类接口,分别走正常网络、超时、鉴权失效和断网缓存四组场景。
- 所有页面请求必须经过统一网络入口。
- 每个请求都有明确
timeoutMs,不能无限等待。 - 可重试接口和不可重试接口要分开声明。
- 401、业务错误、解析错误不能混在同一个提示里。
- 弱网兜底数据必须标记来源和过期时间。
- 请求日志不能包含 token、手机号、身份证、详细地址。
- 至少覆盖超时、重试、鉴权失效、缓存兜底四类测试。
interface NetworkReleaseCheck {
apiName: string;
normalPassed: boolean;
timeoutHandled: boolean;
authHandled: boolean;
cacheFallbackChecked: boolean;
owner: string;
}
const feedApiCheck: NetworkReleaseCheck = {
apiName: 'feed/list',
normalPassed: true,
timeoutHandled: true,
authHandled: true,
cacheFallbackChecked: true,
owner: 'network-module',
};
这份记录不需要复杂,但要能说明核心链路已经被验证。后续如果线上出现“弱网白屏”或“登录弹窗重复”,可以直接回看哪一项漏测。
网络链路专项证据包:把失败分层记录下来
弱网问题不能只靠 catch。一次请求失败要能分清楚是 DNS、连接、超时、状态码、业务码还是解析错误。分层越清楚,重试和兜底才不会误伤。
| 层级 | 记录字段 | 处理方向 |
|---|---|---|
| 连接层 | networkError |
判断是否可重试 |
| HTTP 层 | statusCode |
处理服务异常 |
| 业务层 | bizCode |
展示业务提示 |
| 解析层 | parserError |
回退兼容结构 |
interface NetworkFailureEvidence {
requestId: string
stage: 'connect' | 'http' | 'business' | 'parse'
retryable: boolean
message: string
}
function shouldRetryNetwork(e: NetworkFailureEvidence): boolean {
return e.retryable && (e.stage === 'connect' || e.stage === 'http')
}
这段代码把失败阶段作为重试输入,避免所有错误都机械重试。
14. 小结:稳定网络层不是多写 catch
HarmonyOS 应用里的网络稳定性,关键不是在每个页面多写几个 catch,而是把请求边界提前设计好。统一入口负责超时,错误映射负责分类,重试策略负责克制,缓存兜底负责体验,结果留账负责排查。这样写出来的网络层,后续接入新接口、改鉴权策略、补弱网体验,成本都会低很多。
更多推荐



所有评论(0)