HarmonyOS 文件传输实战:上传下载队列、断点续传与失败恢复怎么设计
文件传输最怕“接口能跑,体验不稳”。真实项目里,用户下载离线地图、上传日志包、同步大文件时,网络可能断开,应用可能切后台,用户可能暂停或取消,服务端也可能返回分片过期。如果只写一个download(url)或upload(file),失败后很难恢复,也说不清楚当前文件到底传到哪里。
这篇文章只解决一个工程问题:HarmonyOS 应用里如何把上传、下载、队列、断点续传、失败恢复和日志验收设计成一条可维护链路。
本文会落到四个结果:
- 每个传输任务都有唯一 id、状态、进度和错误原因。
- 上传下载都进入队列,不让多个大任务把网络和内存打爆。
- 断点续传记录分片进度,失败后能从最近可用位置恢复。
- 用户取消、网络失败、服务端过期都有明确回退策略。
一、先区分三种失败:网络失败、业务失败、用户取消
文件传输失败不能只显示“失败”。三种失败的处理完全不同。
| 类型 | 例子 | 处理方式 |
|---|---|---|
| 网络失败 | 弱网、断网、超时 | 可重试,保留进度 |
| 业务失败 | 文件不存在、权限不足、分片过期 | 停止任务,提示原因 |
| 用户取消 | 用户手动暂停或取消 | 按用户意图保存或清理 |
如果这三类都混成一个error,后面就会出现两个问题:用户不知道能不能重试,开发也不知道该从哪里恢复。
二、资料与版本边界:本文写应用层传输管理
本文示例面向 HarmonyOS NEXT / ArkTS 工程,应用层重点放在任务队列、分片记录、网络恢复、进度持久化和 UI 状态管理。具体网络请求可结合@ohos.net.http、上传下载能力或团队已有网络库落地。
| 层级 | 本文关注 | 不展开 |
|---|---|---|
| 任务层 | 上传、下载、暂停、取消、恢复 | 底层 TCP 实现 |
| 队列层 | 并发数、等待、重试 | 复杂调度算法 |
| 进度层 | 分片、已完成字节、校验 | 服务端存储实现 |
| 验证层 | 弱网、断点、后台、取消 | 压测平台搭建 |
三、任务模型:别只保存 URL
先定义统一任务模型。上传和下载都可以复用同一套状态。
exporttypeTransferType='upload'|'download';exporttypeTransferStatus='waiting'|'running'|'paused'|'success'|'failed'|'cancelled';exportinterfaceTransferTask{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让页面不用猜任务阶段。
四、队列控制:大文件传输不能全部并发
大文件上传下载要控制并发,尤其是移动网络和后台场景。
exportclassTransferQueue{privatewaiting:TransferTask[]=[];privaterunning=newMap<string,TransferTask>();constructor(privatereadonlymaxRunning:number){}enqueue(task:TransferTask):void{this.waiting.push(task);}next():TransferTask[]{constpicked:TransferTask[]=[];while(this.running.size<this.maxRunning&&this.waiting.length>0){consttask=this.waiting.shift();if(task===undefined){break;}task.status='running';this.running.set(task.taskId,task);picked.push(task);}returnpicked;}finish(taskId:string):void{this.running.delete(taskId);}}这段队列不是为了炫技,而是保护体验。一次跑太多下载,进度可能都很慢,失败率更高,内存也更容易上涨。
五、断点记录:恢复靠数据,不靠记忆
断点续传要记录每个任务完成到哪里。下载通常记录已完成字节,上传通常记录已上传分片。
exportinterfaceTransferCheckpoint{taskId:string;offset:number;chunkSize:number;chunkIndex:number;checksum?:string;savedAt:number;}exportclassCheckpointStore{privatedata=newMap<string,TransferCheckpoint>();save(checkpoint:TransferCheckpoint):void{this.data.set(checkpoint.taskId,checkpoint);}get(taskId:string):TransferCheckpoint|undefined{returnthis.data.get(taskId);}remove(taskId:string):void{this.data.delete(taskId);}}真实项目里可以把CheckpointStore换成 Preferences、关系型数据库或文件。关键是不能只存在内存里,否则应用重启后就无法恢复。
六、下载恢复:从 offset 开始,而不是重新下载
下载恢复时要读取 checkpoint,再从对应位置请求。
exportinterfaceDownloadRequest{taskId:string;url:string;savePath:string;startOffset:number;}exportfunctionbuildDownloadRequest(task:TransferTask,store:CheckpointStore):DownloadRequest{constcheckpoint=store.get(task.taskId);conststartOffset=checkpoint!==undefined?checkpoint.offset:task.finishedBytes;return{taskId:task.taskId,url:task.sourceUri,savePath:task.targetUri,startOffset};}注意两点:
- 服务端必须支持范围请求或业务层分片下载,否则客户端无法真正断点。
- 恢复前要校验本地临时文件是否存在,不能只相信进度数字。
七、上传分片:每片成功后再保存进度
上传大文件更适合分片。每片成功后保存 checkpoint。
exportinterfaceUploadChunk{taskId:string;chunkIndex:number;start:number;end:number;checksum:string;}exportfunctioncreateUploadChunks(task:TransferTask,chunkSize:number):UploadChunk[]{constchunks:UploadChunk[]=[];letstart=0;letindex=0;while(start<task.totalBytes){constend=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;}returnchunks;}这段代码里的checksum只是示例占位。真实项目应使用可靠摘要算法,并和服务端约定校验规则。上传分片如果没有校验,弱网重试时很容易产生重复片或坏片。
八、重试策略:失败不是无限重试
重试要有边界,也要区分失败类型。
exporttypeTransferFailType='network'|'server'|'permission'|'expired'|'unknown';exportinterfaceRetryDecision{shouldRetry:boolean;delayMs:number;message:string;}exportfunctionresolveRetryDecision(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:'网络异常,稍后自动重试'};}重试次数、退避间隔要可配置。无限重试会增加耗电,也会让用户误以为任务卡住。
九、页面状态:用户要能暂停、继续、取消
文件传输页面至少要展示状态、进度和可执行动作。
exportinterfaceTransferUiState{title:string;progressText:string;primaryAction:'pause'|'resume'|'retry'|'open'|'none';secondaryAction:'cancel'|'delete'|'none';}exportfunctionbuildTransferUiState(task:TransferTask):TransferUiState{constpercent=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'){constmessage=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 状态不要散落在页面判断里。统一函数能保证上传和下载的操作含义一致。
十、日志记录:排查要能看到完整链路
文件传输问题经常跨越网络、存储、后台和服务端,必须有日志。
exportinterfaceTransferLog{taskId:string;action:'enqueue'|'start'|'progress'|'pause'|'resume'|'retry'|'success'|'fail'|'cancel';status:TransferStatus;finishedBytes:number;message:string;timestamp:number;}exportfunctioncreateTransferLog(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 记录 |
| 失败怎么办 | 按失败类型决定重试或重建 |
| 用户取消怎么办 | 停止请求并清理临时文件 |
| 怎么证明稳定 | 弱网、断网、重启、切后台全走一遍 |