欢迎光临
我们一直在努力

HarmonyOS ArkWeb 系列之下载任务断点续传:serialize/deserialize + WebDownloadManager 实战

文章目录

      • 核心思路
      • 流程图
      • 工具类:下载信息持久化
      • 完整的断点续传页面
      • serialize/deserialize 的本质
      • 关键:两个 setDownloadDelegate 的区别
      • 多任务下载扩展思路
      • 写在最后

下载到一半网断了,或者用户退出了 App,下载任务就丢了——这是做下载功能时最让人头疼的问题之一。ArkWeb 提供了一套完整的序列化方案,让你把下载任务"存档",随时"读档"恢复。

核心思路

大白话说:

  • 下载进行中,把任务信息"序列化"成一个字节数组
  • 把这个字节数组写到本地文件(持久化)
  • 下次打开 App,从文件里读出字节数组
  • 用 deserialize 还原出下载任务对象
  • 调 WebDownloadManager.resumeDownload() 恢复下载
  • 关键点:恢复下载时,必须先设置好 WebDownloadManager.setDownloadDelegate(),让系统知道恢复后的进度往哪里发。

    流程图

    工具类:下载信息持久化

    先封装一个存取下载信息的工具:

    // downloadUtil.ets
    import { util } from '@kit.ArkTS';
    import { fileIo } from '@kit.CoreFileKit';

    const helper = new util.Base64Helper();

    // 导出文件存储路径(运行时初始化)和文件名
    export let filePath: string;
    export const fileName = 'download_task.txt';

    export namespace DownloadUtil {
    // 初始化:从 UIContext 获取应用沙盒目录
    export function init(context: UIContext): void {
    filePath = context.getHostContext()!.filesDir;
    }

    // Uint8Array → Base64 字符串(方便文本存储)
    export function uint8ArrayToStr(uint8Array: Uint8Array): string {
    return helper.encodeToStringSync(uint8Array);
    }

    // Base64 字符串 → Uint8Array
    export function strToUint8Array(str: string): Uint8Array {
    return helper.decodeSync(str);
    }

    // 保存下载信息到文件
    export function saveDownloadInfo(downloadInfo: string): void {
    if (!fileExists(filePath)) {
    mkDirectorySync(filePath);
    }
    writeToFileSync(filePath, fileName, downloadInfo);
    }

    // 检查路径是否存在
    export function fileExists(path: string): boolean {
    try {
    return fileIo.accessSync(path);
    } catch {
    return false;
    }
    }

    // 创建目录
    export function mkDirectorySync(dirPath: string, recursion?: boolean): void {
    try {
    fileIo.mkdirSync(dirPath, recursion ?? false);
    } catch (error) {
    console.error(`创建目录失败: ${error.message}, code: ${error.code}`);
    }
    }

    // 写文件
    export function writeToFileSync(dir: string, name: string, content: string): void {
    const file = fileIo.openSync(
    dir + '/' + name,
    fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE
    );
    fileIo.writeSync(file.fd, content);
    fileIo.closeSync(file.fd);
    }

    // 读文件
    export function readFileSync(dir: string, name: string): string {
    return fileIo.readTextSync(dir + '/' + name);
    }
    }

    完整的断点续传页面

    // ResumeDownloadPage.ets
    import { webview } from '@kit.ArkWeb';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { DownloadUtil, fileName, filePath } from './downloadUtil';

    @Entry
    @Component
    struct ResumeDownloadDemo {
    controller: webview.WebviewController = new webview.WebviewController();
    delegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate();

    // 当前正在进行的下载任务
    download: webview.WebDownloadItem = new webview.WebDownloadItem();
    // 失败任务的序列化数据
    failedData: Uint8Array = new Uint8Array();

    @State statusText: string = '等待';
    @State progress: number = 0;

    aboutToAppear(): void {
    // 初始化工具类(获取沙盒路径)
    DownloadUtil.init(this.getUIContext());
    }

    build() {
    Column({ space: 12 }) {
    Text(this.statusText).fontSize(16).padding(8)
    Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
    .width('100%')

    // 步骤1:注册委托
    Button('① 注册下载委托')
    .onClick(() => {
    try {
    this.delegate.onBeforeDownload((item: webview.WebDownloadItem) => {
    item.start('/data/storage/el2/base/cache/web/' + item.getSuggestedFileName());
    this.statusText = '下载中…';
    });

    this.delegate.onDownloadUpdated((item: webview.WebDownloadItem) => {
    this.progress = item.getPercentComplete() > 0 ? item.getPercentComplete() : 0;
    this.download = item; // 保存最新的任务对象
    this.statusText = `下载中 ${this.progress}%`;
    });

    this.delegate.onDownloadFailed((item: webview.WebDownloadItem) => {
    // 下载失败时序列化,以备恢复
    this.failedData = item.serialize();
    this.statusText = `下载失败(错误码:${item.getLastErrorCode()})`;
    });

    this.delegate.onDownloadFinish((item: webview.WebDownloadItem) => {
    this.progress = 100;
    this.statusText = '下载完成!';
    });

    // 同时注册到 controller 和 WebDownloadManager
    // WebDownloadManager 用于跨进程恢复(App 重启后)
    this.controller.setDownloadDelegate(this.delegate);
    webview.WebDownloadManager.setDownloadDelegate(this.delegate);

    this.statusText = '委托已注册';
    } catch (error) {
    console.error(`注册委托失败: ${(error as BusinessError).message}`);
    }
    })

    // 步骤2:发起下载
    Button('② 开始下载')
    .onClick(() => {
    try {
    this.controller.startDownload('https://www.example.com/file.zip');
    } catch (error) {
    console.error(`发起下载失败: ${(error as BusinessError).message}`);
    }
    })

    // 步骤3:保存当前下载任务(下载中时点击)
    Button('③ 保存下载进度')
    .onClick(() => {
    try {
    // serialize() 把任务信息转成字节数组
    // uint8ArrayToStr 再转成 Base64 字符串存文件
    const serialized = DownloadUtil.uint8ArrayToStr(this.download.serialize());
    DownloadUtil.saveDownloadInfo(serialized);
    this.statusText = '进度已保存到文件';
    } catch (error) {
    console.error(`保存进度失败: ${(error as BusinessError).message}`);
    }
    })

    // 步骤4:从文件读取并恢复下载
    Button('④ 恢复下载')
    .onClick(() => {
    try {
    // 从文件读取 Base64,转回字节数组,再 deserialize
    const savedStr = DownloadUtil.readFileSync(filePath, fileName);
    const downloadItem = webview.WebDownloadItem.deserialize(
    DownloadUtil.strToUint8Array(savedStr)
    );
    // 恢复下载(必须先设置好 WebDownloadManager 的委托)
    webview.WebDownloadManager.resumeDownload(downloadItem);
    this.statusText = '恢复下载中…';
    } catch (error) {
    console.error(`恢复下载失败: ${(error as BusinessError).message}`);
    }
    })

    Web({ src: 'https://www.example.com', controller: this.controller })
    .width('100%')
    .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    }
    }

    serialize/deserialize 的本质

    这俩方法是把 WebDownloadItem 的内部状态(下载 URL、已下载字节数、临时文件路径、任务 ID……)打包成一个字节数组。这个字节数组可以存到任何地方:文件、数据库、SharedPreferences。

    恢复时反向操作,重建 WebDownloadItem 对象,然后让 WebDownloadManager 继续执行。

    关键:两个 setDownloadDelegate 的区别

    // 1. 绑定到 controller:处理当前 Web 实例发起的下载
    this.controller.setDownloadDelegate(this.delegate);

    // 2. 绑定到 WebDownloadManager:处理跨进程恢复的下载
    // App 重启后恢复下载,必须用这个
    webview.WebDownloadManager.setDownloadDelegate(this.delegate);

    两个都设置最稳妥,不然 App 重启后恢复的下载回调会丢。

    多任务下载扩展思路

    当前示例只演示单任务。多任务场景可以维护一个 Map 或数组:

    // 用 guid 作为 key 管理多个下载任务
    private downloadMap: Map<string, webview.WebDownloadItem> = new Map();

    this.delegate.onDownloadUpdated((item: webview.WebDownloadItem) => {
    this.downloadMap.set(item.getGuid(), item);
    });

    this.delegate.onDownloadFinish((item: webview.WebDownloadItem) => {
    this.downloadMap.delete(item.getGuid());
    });

    // 保存所有任务
    this.downloadMap.forEach((item, guid) => {
    DownloadUtil.saveDownloadInfo(guid, DownloadUtil.uint8ArrayToStr(item.serialize()));
    });

    写在最后

    断点续传这块 API 设计得比较底层,但逻辑很清晰:serialize → 存文件 → 读文件 → deserialize → resumeDownload。记住恢复之前一定要先设好 WebDownloadManager.setDownloadDelegate,不然回调不触发,恢复的任务像黑洞一样悄悄下载,你完全不知道进度。

    赞(0)
    未经允许不得转载:171主机测评 » HarmonyOS ArkWeb 系列之下载任务断点续传:serialize/deserialize + WebDownloadManager 实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址