HarmonyOS 文件传输实战:上传下载队列、断点续传与失败恢复怎么设计
HarmonyOS 文件传输实战:上传下载队列、断点续传与失败恢复怎么设计
文件传输最怕“接口能跑,体验不稳”。真实项目里,用户下载离线地图、上传日志包、同步大文件时,网络可能断开,应用可能切后台,用户可能暂停或取消,服务端也可能返回分片过期。如果只写一个 download(url) 或 upload(file),失败后很难恢复,也说不清楚当前文件到底传到哪里。
这篇文章只解决一个工程问题:HarmonyOS 应用里如何把上传、下载、队列、断点续传、失败恢复和日志验收设计成一条可维护链路。

本文会落到四个结果:
- 每个传输任务都有唯一 id、状态、进度和错误原因。
- 上传下载都进入队列,不让多个大任务把网络和内存打爆。
- 断点续传记录分片进度,失败后能从最近可用位置恢复。
- 用户取消、网络失败、服务端过期都有明确回退策略。
一、先区分三种失败:网络失败、业务失败、用户取消
文件传输失败不能只显示“失败”。三种失败的处理完全不同。
| 类型 | 例子 | 处理方式 |
|---|---|---|
| 网络失败 | 弱网、断网、超时 | 可重试,保留进度 |
| 业务失败 | 文件不存在、权限不足、分片过期 | 停止任务,提示原因 |
| 用户取消 | 用户手动暂停或取消 | 按用户意图保存或清理 |
如果这三类都混成一个 error,后面就会出现两个问题:用户不知道能不能重试,开发也不知道该从哪里恢复。
二、资料与版本边界:本文写应用层传输管理
本文示例面向 HarmonyOS NEXT / ArkTS 工程,应用层重点放在任务队列、分片记录、网络恢复、进度持久化和 UI 状态管理。具体网络请求可结合 @ohos.net.http、上传下载能力或团队已有网络库落地。

| 层级 | 本文关注 | 不展开 |
|---|---|---|
| 任务层 | 上传、下载、暂停、取消、恢复 | 底层 TCP 实现 |
| 队列层 | 并发数、等待、重试 | 复杂调度算法 |
| 进度层 | 分片、已完成字节、校验 | 服务端存储实现 |
| 验证层 | 弱网、断点、后台、取消 | 压测平台搭建 |

三、任务模型:别只保存 URL
先定义统一任务模型。上传和下载都可以复用同一套状态。
export type TransferType = 'upload' | 'download';
export type TransferStatus = 'waiting' | 'running' | 'paused' | 'success' | 'failed' | 'cancelled';
export interface TransferTask {
taskId: string;
type: TransferType;
sourceUri: string;
targetUri: string;
totalBytes: number;
finishedBytes: number;
status: TransferStatus;
retryCount: number;
errorMessage?: string;
updatedAt: number;
}
这个模型解决四个问题:
taskId用于日志、UI 和恢复。finishedBytes让断点续传有依据。retryCount避免无限重试。status让页面不用猜任务阶段。
四、队列控制:大文件传输不能全部并发
大文件上传下载要控制并发,尤其是移动网络和后台场景。
export class TransferQueue {
private waiting: TransferTask[] = [];
private running = new Map<string, TransferTask>();
constructor(private readonly maxRunning: number) {}
enqueue(task: TransferTask): void {
this.waiting.push(task);
}
next(): TransferTask[] {
const picked: TransferTask[] = [];
while (this.running.size < this.maxRunning && this.waiting.length > 0) {
const task = this.waiting.shift();
if (task === undefined) {
break;
}
task.status = 'running';
this.running.set(task.taskId, task);
picked.push(task);
}
return picked;
}
finish(taskId: string): void {
this.running.delete(taskId);
}
}
这段队列不是为了炫技,而是保护体验。一次跑太多下载,进度可能都很慢,失败率更高,内存也更容易上涨。
五、断点记录:恢复靠数据,不靠记忆
断点续传要记录每个任务完成到哪里。下载通常记录已完成字节,上传通常记录已上传分片。
export interface TransferCheckpoint {
taskId: string;
offset: number;
chunkSize: number;
chunkIndex: number;
checksum?: string;
savedAt: number;
}
export class CheckpointStore {
private data = new Map<string, TransferCheckpoint>();
save(checkpoint: TransferCheckpoint): void {
this.data.set(checkpoint.taskId, checkpoint);
}
get(taskId: string): TransferCheckpoint | undefined {
return this.data.get(taskId);
}
remove(taskId: string): void {
this.data.delete(taskId);
}
}
真实项目里可以把 CheckpointStore 换成 Preferences、关系型数据库或文件。关键是不能只存在内存里,否则应用重启后就无法恢复。
六、下载恢复:从 offset 开始,而不是重新下载
下载恢复时要读取 checkpoint,再从对应位置请求。
export interface DownloadRequest {
taskId: string;
url: string;
savePath: string;
startOffset: number;
}
export function buildDownloadRequest(
task: TransferTask,
store: CheckpointStore
): DownloadRequest {
const checkpoint = store.get(task.taskId);
const startOffset = checkpoint !== undefined ? checkpoint.offset : task.finishedBytes;
return {
taskId: task.taskId,
url: task.sourceUri,
savePath: task.targetUri,
startOffset
};
}
注意两点:
- 服务端必须支持范围请求或业务层分片下载,否则客户端无法真正断点。
- 恢复前要校验本地临时文件是否存在,不能只相信进度数字。
七、上传分片:每片成功后再保存进度
上传大文件更适合分片。每片成功后保存 checkpoint。
export interface UploadChunk {
taskId: string;
chunkIndex: number;
start: number;
end: number;
checksum: string;
}
export function createUploadChunks(task: TransferTask, chunkSize: number): UploadChunk[] {
const chunks: UploadChunk[] = [];
let start = 0;
let index = 0;
while (start < task.totalBytes) {
const end = Math.min(start + chunkSize, task.totalBytes);
chunks.push({
taskId: task.taskId,
chunkIndex: index,
start,
end,
checksum: `${task.taskId}_${index}_${end - start}`
});
start = end;
index += 1;
}
return chunks;
}
这段代码里的 checksum 只是示例占位。真实项目应使用可靠摘要算法,并和服务端约定校验规则。上传分片如果没有校验,弱网重试时很容易产生重复片或坏片。
八、重试策略:失败不是无限重试
重试要有边界,也要区分失败类型。
export type TransferFailType = 'network' | 'server' | 'permission' | 'expired' | 'unknown';
export interface RetryDecision {
shouldRetry: boolean;
delayMs: number;
message: string;
}
export function resolveRetryDecision(type: TransferFailType, retryCount: number): RetryDecision {
if (type === 'permission' || type === 'expired') {
return { shouldRetry: false, delayMs: 0, message: '当前任务无法恢复,需要重新发起' };
}
if (retryCount >= 3) {
return { shouldRetry: false, delayMs: 0, message: '重试次数已达上限,请稍后手动重试' };
}
return {
shouldRetry: true,
delayMs: 1000 * Math.pow(2, retryCount),
message: '网络异常,稍后自动重试'
};
}
重试次数、退避间隔要可配置。无限重试会增加耗电,也会让用户误以为任务卡住。
九、页面状态:用户要能暂停、继续、取消
文件传输页面至少要展示状态、进度和可执行动作。
export interface TransferUiState {
title: string;
progressText: string;
primaryAction: 'pause' | 'resume' | 'retry' | 'open' | 'none';
secondaryAction: 'cancel' | 'delete' | 'none';
}
export function buildTransferUiState(task: TransferTask): TransferUiState {
const percent = task.totalBytes === 0
? 0
: Math.floor((task.finishedBytes / task.totalBytes) * 100);
if (task.status === 'running') {
return { title: '传输中', progressText: `${percent}%`, primaryAction: 'pause', secondaryAction: 'cancel' };
}
if (task.status === 'paused') {
return { title: '已暂停', progressText: `${percent}%`, primaryAction: 'resume', secondaryAction: 'cancel' };
}
if (task.status === 'failed') {
const message = task.errorMessage !== undefined ? task.errorMessage : '请稍后重试';
return { title: '传输失败', progressText: message, primaryAction: 'retry', secondaryAction: 'delete' };
}
if (task.status === 'success') {
return { title: '传输完成', progressText: '100%', primaryAction: 'open', secondaryAction: 'delete' };
}
return { title: '等待中', progressText: `${percent}%`, primaryAction: 'none', secondaryAction: 'cancel' };
}
UI 状态不要散落在页面判断里。统一函数能保证上传和下载的操作含义一致。
十、日志记录:排查要能看到完整链路
文件传输问题经常跨越网络、存储、后台和服务端,必须有日志。
export interface TransferLog {
taskId: string;
action: 'enqueue' | 'start' | 'progress' | 'pause' | 'resume' | 'retry' | 'success' | 'fail' | 'cancel';
status: TransferStatus;
finishedBytes: number;
message: string;
timestamp: number;
}
export function createTransferLog(task: TransferTask, action: TransferLog['action'], message: string): TransferLog {
return {
taskId: task.taskId,
action,
status: task.status,
finishedBytes: task.finishedBytes,
message,
timestamp: Date.now()
};
}
排查时至少要能回答:任务什么时候创建、什么时候开始、失败前完成了多少、是否保存 checkpoint、是否自动重试、用户是否取消。
十一、文件传输问题排查表
| 现象 | 优先怀疑 | 检查方式 | 修复方向 |
|---|---|---|---|
| 断网后从头下载 | checkpoint 没保存 | 查 CheckpointStore |
每次进度变化后持久化 |
| 上传重复分片 | 服务端幂等不足 | 查 chunkIndex 和 checksum | 上传前查询已完成分片 |
| 任务越跑越多 | 队列无并发限制 | 查 running 数量 | 加 maxRunning |
| 用户取消后还在传 | 取消状态没传到底层 | 查 cancel 日志 | 取消后停止请求并清理临时文件 |
| 失败原因不清楚 | error 太泛 | 查失败类型 | 区分 network/server/permission |
| 切后台后状态丢失 | 进度只存在内存 | 重启应用验证 | 持久化任务和 checkpoint |
不要把所有问题都当成“网络差”。弱网只是触发器,真正的问题通常是状态没有保存。
十二、上线前传输验收表
| 检查项 | 通过标准 |
|---|---|
| 队列并发受控 | 同时运行任务数量不超过阈值 |
| 断点可恢复 | 断网、重启后能从 checkpoint 继续 |
| 用户可暂停取消 | UI 操作和任务状态一致 |
| 失败有分类 | 网络、权限、过期、服务端错误可区分 |
| 临时文件可清理 | 取消和失败不会留下垃圾文件 |
| 日志可串联 | 一个 taskId 能追踪全链路 |
| 弱网已验证 | 模拟断网、慢网、切后台都通过 |
验收一定要覆盖异常路径。文件传输不是“下载成功一次”就算完成。
十三、文件传输相关官方资料
- 华为开发者文档:Network Kit / 网络请求
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/network-kit-overview - 华为开发者文档:http 请求能力
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-http - 华为开发者文档:应用文件访问与管理
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-file-access - 华为开发者文档:后台任务
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/background-task-overview
十四、把传输做成可恢复任务
上传下载的核心不是“发起请求”,而是“任务可恢复”。只要任务模型、队列、checkpoint、重试、UI 和日志都完整,文件传输就能从一次脆弱请求变成可维护能力。
最后用这张表做复盘:
| 问题 | 稳定答案 |
|---|---|
| 任务是谁 | taskId 唯一标识 |
| 传到哪里 | finishedBytes 和 checkpoint 记录 |
| 失败怎么办 | 按失败类型决定重试或重建 |
| 用户取消怎么办 | 停止请求并清理临时文件 |
| 怎么证明稳定 | 弱网、断网、重启、切后台全走一遍 |
更多推荐



所有评论(0)