鸿蒙H5离线包技术分享

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

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

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)

  }

}