鸿蒙H5离线包技术分享

简介: 鸿蒙H5离线包技术分享:本文基于鸿蒙NEXT Api 12,介绍H5离线包的下载、解压和加载三大核心问题。下载部分支持进度回调与重复下载;解压使用minizip实现并提供进度反馈;加载本地H5页面时处理本地资源与网页跳转,确保无网环境下H5页面正常显示。

鸿蒙H5离线包技术分享

  • 在开发过程中,我们常常使用H5离线包技术,实现H5本地化,解决无网情况下,H5无法加载的问题;

  • 核心问题就是三个:下载,解压,加载.下面的分享就围绕这三个问题要解答

  • 下面的所有代码是基于鸿蒙NEXT Api 12

1. 下载离线包

  • 要有下载进度回调
  • 支持重复下载

具体实现如下

 download(
    resourceUrl: string | undefined,
    targetZipPath: string,
    progressCallback?: (progress: number) => void,
    completeCallBack?: (isSuc: boolean, message: string) => void) {
    if (resourceUrl != undefined && resourceUrl.length > 0) {
      // 如果zip文件地址存在,需要先删除
      if (fs.accessSync(targetZipPath)) {
        fs.unlinkSync(targetZipPath)
      }
      try {
        request.downloadFile(getContext(), { url: resourceUrl, filePath: targetZipPath })
          .then((downloadTask: request.DownloadTask) => {
            downloadTask.on('complete', () => {
              if (progressCallback != undefined) {
                progressCallback(1)
              }
              if (completeCallBack != undefined) {
                completeCallBack(true, "下载成功")
              }
            })
            downloadTask.on('remove', () => {
              if (completeCallBack != undefined) {
                completeCallBack(false, "取消下载")
              }
            })
            downloadTask.on("progress", (receivedSize: number, totalSize: number) => {
              if (progressCallback != undefined) {
                let progress = receivedSize / totalSize
                console.log(`下载当前进度${progress} receivedSize ${receivedSize} totalSize ${totalSize}`)
                progressCallback(progress)
              }
            })
          })
          .catch((err: BusinessError) => {
            if (completeCallBack != undefined) {
              completeCallBack(false, err.message)
            }
          });
      } catch (error) {
        let err: BusinessError = error as BusinessError;
        if (completeCallBack != undefined) {
          completeCallBack(false, err.message)
        }
      }
    } else {
      if (completeCallBack != undefined) {
        completeCallBack(false, "下载地址为空")
      }
    }
  }

2. 解压离线包

  • 要有解压进度回调
    • 官方提供了zip解压组件zlib,但是不支持解压进度的回调,因此选用了minizip
  • 解压完成后自动删除压缩包

具体实现如下


unzipToDirectory(
    zipPath: string,
    targetPath: string,
    progressCallback?: (progress: number) => void,
    completeCallback?: (isSuc: boolean, message: string) => void) {
    if (!fs.accessSync(zipPath)) {
      if (completeCallback) {
        completeCallback(false, "selectPath not exists");
      }
      return
    }
    if (fs.accessSync(targetPath)) {
      fs.rmdirSync(targetPath)
    }
    fs.mkdirSync(targetPath, true);

    let minizipEntry = new MinizipNative(zipPath);

    let code: number = minizipEntry.Open()
    if (code == 0) {
      let entryNames: Array<string> = minizipEntry.GetEntryNames().sort((a: string, b: string) => {
        return a.length - b.length
      });

      let dirEntrySet = new Set<string>()
      for (let i = 0; i < entryNames.length; i++) {
        const path = entryNames[i]
        const index = path.lastIndexOf("/")
        if (index != -1) {
          const dirPath = path.substring(0, index)
          dirEntrySet.add(dirPath)
        }
      }
      let dirEntryNames = Array.from(dirEntrySet).sort((a: string, b: string) => {
        return a.length - b.length
      });

      for (let i = 0; i < dirEntryNames.length; i++) {
        let dirPath = targetPath + "/" + dirEntryNames[i]
        if (!fs.accessSync(dirPath)) {
          fs.mkdirSync(dirPath, true);
        }
      }
      let onlyFiles = entryNames.filter((value) => {
        return !value.endsWith("/")
      })
      if (progressCallback != undefined) {
        progressCallback(0)
      }
      let result: boolean = true
      for (let i = 0; i < onlyFiles.length; i++) {
        let path: string = targetPath + "/" + onlyFiles[i];
        let file = fs.openSync(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
        let arrBuffer: ArrayBuffer | undefined = minizipEntry.ExtractFileToJS(onlyFiles[i], "");
        if (arrBuffer != undefined) {
          fs.write(file.fd, arrBuffer).then((writeLen: number) => {
            let progress = (i + 1) / onlyFiles.length
            if (progressCallback != undefined) {
              progressCallback(progress)
            }
          }).catch((err: BusinessError) => {
            result = false
            if (completeCallback) {
              completeCallback(false, "fs.write failed: " + onlyFiles[i])
            }
          }).finally(() => {
            fs.closeSync(file);
          })
        } else {
          // 数据为空的话,直接创建空文件即可
          fs.write(file.fd, "")
          let progress = (i + 1) / onlyFiles.length
          if (progressCallback) {
            progressCallback(progress)
          }
        }
        if (!result) {
          break
        }
      }
      if (result) {
        if (progressCallback) {
          progressCallback(1)
        }
        fs.unlink(zipPath).then(() => {
          if (completeCallback) {
            completeCallback(true, "unzip success")
          }
        })
      }

    } else {
      if (completeCallback) {
        completeCallback(false, "Open failed")
      }
    }
  }

3. 加载本地H5页面

在加载本地网页的时候发现如果网页中的含有本地css,跳转,资源文件,都加载不出来,我们需要将上述的内容进行正则匹配后替换上述内容,再在对应的系统代理事件中进行处理

所以我们核心解决下面两个问题:

  • 支持本地资源加载
  • 支持本地网页跳转
@Component
struct WebPage {
  // 跳转过来 传递的地址
  urlPath: string = ""
  controller = new webview.WebviewController()
  schemeHandler: webview.WebSchemeHandler = new webview.WebSchemeHandler();
  responseWeb: WebResourceResponse = new WebResourceResponse();
  fileDir: string = ""

  loadData() {
    try {
      if (this.urlPath.includes("http") == false) {
        this.loadLocalUrl()
      } else {
        this.controller.loadUrl(this.urlPath)
      }

    } catch (e) {
      showShortCenterToast("加载失败")
    }
  }

  loadLocalUrl() {
    let parts = this.urlPath.split('/');
    // 如果数组长度大于 1,移除最后一个元素
    if (parts.length > 1) {
      parts.pop();
    }
    // 当前H5所在文件夹的绝对路径
    this.fileDir = parts.join('/')

    let html = fs.readTextSync(this.urlPath)
    // 要插入的指定字符串
    const insertString = "http://local";
    // 定义正则表达式
    const regex = /src="([^"]+\.(?:png|jpg|gif))"/gi;
    // 执行替换操作
    const imageHtml = html.replace(regex, (_, p1: string) => {
      let content = `src="${insertString}/${p1}"`;
      return content
    });

    // href定义正则表达式
    const cssRegex = /href="([^"]+\.(?:css|html))"/gi;
    const cssHtml = imageHtml.replace(cssRegex, (_, p1: string) => {
      let content = `href="${insertString}/${p1}"`;
      return content
    });
    this.controller.loadData(
      cssHtml,
      'text/html',
      'UTF-8'
    );
  }

  build() {
    Web({ src: this.urlPath, controller: this.controller })
      .width(FULL_WIDTH)
      .mixedMode(MixedMode.All)
      .layoutWeight(1)
      .onControllerAttached(() => {
        this.loadData()
      })
      .onLoadIntercept((event) => {
        let url = event.data.getRequestUrl()
        // 跳转拦截
        if (url.toLowerCase().startsWith('http://local') && url.toLowerCase().endsWith("html/")) {
          url = url.replace("http://local", "")
          url = url.toUpperCase()
          url = url.replace("HTML/", "html")

          const filePath = this.fileDir + "/" + url
          if (fs.accessSync(filePath)) {
            this.urlPath = filePath
            this.loadLocalUrl()
            return true
          }
        }
        return false
      })
      .onInterceptRequest((event) => {
        let url = event.request.getRequestUrl()
        // 本地资源加载拦截
        if (url.startsWith('http://local')) {
          const promise: Promise<String> = new Promise((resolve: Function, reject: Function) => {
            url = url.replace("http://local", "")
            const filePath = this.fileDir + "/" + url
            if (fs.accessSync(filePath)) {
              if (url.toLowerCase().endsWith(".png") ||
              url.toLowerCase().endsWith(".jpg") ||
              url.toLowerCase().endsWith(".gif") ||
              url.toLowerCase().endsWith(".css")) {

                const fd = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
                // 获取文件的大小
                const stat = fs.statSync(fd.fd);
                const fileSize = stat.size;
                // 创建一个指定大小的 ArrayBuffer
                const buffer = new ArrayBuffer(fileSize);
                // 读取文件内容到 ArrayBuffer
                fs.readSync(fd.fd, buffer);
                this.responseWeb.setResponseData(buffer);
                if (url.toLowerCase().endsWith(".css")) {
                  this.responseWeb.setResponseMimeType('text/css');
                } else {
                  this.responseWeb.setResponseMimeType('image/*');
                }
              }
              this.responseWeb.setResponseCode(200);
              this.responseWeb.setReasonMessage('OK');
              resolve("success");
            } else {
              reject("failed")
            }
          })
          promise.then(() => {
            this.responseWeb.setResponseIsReady(true);
          })
          this.responseWeb.setResponseIsReady(false);
          return this.responseWeb;
        }
        return null

      })
      .width(FULL_WIDTH)
      .height(FULL_HEIGHT)
  }
}
相关文章
|
18天前
|
移动开发 JavaScript 应用服务中间件
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
135 5
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
25天前
|
移动开发 前端开发 Android开发
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
206 12
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
26天前
|
移动开发 Rust JavaScript
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
386 3
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
24天前
|
移动开发 Android开发
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
86 0
|
6月前
|
缓存 数据库 数据安全/隐私保护
HarmonyOS5云服务技术分享--退出登录文档问题
本文详解HarmonyOS应用开发中的用户认证操作,涵盖登出、账号注销与重新认证三大核心功能。通过`signOut()`实现优雅用户登出,清除缓存并跳转页面;`deleteUser()`完成账号永久注销,注重二次确认与敏感操作验证;`reauthenticate`用于关键时刻的重新认证,支持多种验证方式。同时提供实战避坑指南,解决常见问题,并分享开发建议,助你打造完善的认证流程。
|
6月前
|
IDE 开发工具 数据安全/隐私保护
鸿蒙开发:应用上架第三篇,配置签名信息打出上架包
可以说,所有的签名信息文件,我们都已经完成了,正所谓,万事俱备只欠东风,这篇文章,我们着重概述一下,如何配置签名信息以及如何打出签名包。
245 4
鸿蒙开发:应用上架第三篇,配置签名信息打出上架包
|
6月前
|
安全 搜索推荐 API
HarmonyOS5云服务技术分享--账号关联开发指南
本文介绍了如何在HarmonyOS应用开发中使用ArkTS(API 12)实现账号关联功能。通过关联手机号、邮箱和华为账号,用户可自由切换登录方式并保持数据同步。文章详细说明了前提条件、3种关联方式的代码示例以及解绑操作,并提供了避坑指南、扩展技巧和最佳实践场景,帮助开发者构建灵活安全的用户体系,提升用户体验与管理效率。
|
6月前
|
存储 安全 数据安全/隐私保护
HarmonyOS5云服务技术分享--匿名登录功能指南
本文为开发者详解如何实现应用的“游客模式”登录功能,让用户无需注册即可快速体验APP。通过5步集成指南(环境准备、初始化认证模块、智能登录检测、一键游客登录及账号升级策略),手把手教你完成开发流程。同时分享安全守则与高阶技巧,如敏感操作防护和事件监听等,帮助优化用户体验并提升留存率。文末更有小互动,期待你的观点交流!
|
6月前
|
存储 IDE API
HarmonyOS5云服务技术分享--云存储SDK文章整理
本文详细介绍了如何在HarmonyOS ArkTS应用中集成华为云存储SDK。从开发环境准备、配置文件获取,到项目配置与代码实现,提供了全流程的指导。重点包括SDK初始化、网络权限设置及上传测试文件等步骤,并针对初始化失败、依赖冲突等问题提供了解决方案。帮助开发者快速上手,顺利接入华为云存储服务。
|
6月前
|
缓存 JavaScript Java
HarmonyOS5云服务技术分享--云缓存快速上手指南
本文介绍如何快速上手华为AppGallery Connect(AGC)的云缓存服务,涵盖信息获取、代码实战及避坑指南。首先详解云缓存的基础信息与密码管理,接着分别演示Node.js和Java的接入方式,包括原生Jedis、RedisTemplate及Spring Boot自动装配三种方案。最后总结常见问题与优化建议,助你实现高效缓存接入。

热门文章

最新文章