☰
ArkWeb 实战 02:缓存与离线包,把几十兆的页面塞进手机里
2026/10/24 6:32:22 网站建设 项目流程

开篇:首屏加载的生死劫

做内容型应用,首屏加载速度就是生命线。上个项目,我们接手了一个纯 H5 的商品详情页。上线后,运维拉出了监控数据:在网络较差的 4G 环境下,该页面的首屏完全加载时间(FCP)高达 3.5 秒,用户跳出率飙升。

为什么这么慢?因为 HarmonyOS 的 Web 组件默认的网络策略极其“老实”。每次打开页面,它都会去服务器请求最新的 HTML、CSS 和 JS(如果没有配置好 HTTP 强缓存响应头的话)。在移动端,高频的网络抖动会导致多次 TCP 重连,时间全耗在路上了。

要彻底解决这个问题,单靠服务端的优化远远不够。我们必须借助端侧的能力:缓存策略重配与离线包(Offline Package)机制。

https://placeholder-01.jpg/

一、端侧缓存:弄懂 CacheMode 的底层逻辑

鸿蒙 Web 组件提供了一个cacheMode属性。如果不懂浏览器的缓存机制,很容易配错。

  • CacheMode.Default:遵循 RFC 标准,由 HTTP 响应头(如Cache-Control、ETag)决定。这要求前后端配合默契,如果服务端没配好,端侧就等于没缓存。

  • CacheMode.Only:绝对不要用在业务页面。它只读本地缓存,一旦你发了新版本,而用户本地有旧缓存,页面永远不会更新。

  • CacheMode.Online:优先请求网络,如果网络请求失败(如断网、超时),降级读取本地缓存。

在电商场景下,我推荐使用CacheMode.Online作为兜底:

typescript

Web({ src: this.url, controller: this.controller }) .cacheMode(CacheMode.Online)

但这只能解决“断网不至于白屏”的问题,解决不了“网络慢导致首屏耗时”的问题。要快,必须做离线包。

二、离线包架构:H5 资源的“本地化”

离线包的核心思想是:在 App 安装时,或者启动时,提前将 H5 的静态资源(HTML/CSS/JS/字体/图片)打包下载到应用沙箱。当用户打开页面时,Web 组件直接从沙箱读取文件,完全不走网络。

1. 初探 rawfile 内置方案

最简单的做法,是把 H5 构建产物放到resources/rawfile目录里。

typescript

Web({ src: $rawfile('h5_dist/index.html'), controller: this.controller })

坑点解析:
这种加载方式使用的协议是resource://。在这个协议下,H5 内所有的fetch或XMLHttpRequest请求都会面临跨域问题。而且,这种方式无法实现热更新,每次 H5 改动都要重新发版审核 App,极其臃肿。

2. 生产级方案:沙箱解压与版本管理

真正可用的方案是:服务端下发 zip 包,客户端下载后解压到内部存储(沙箱),然后用file://协议加载。

第一步:下载与解压。
我们需要引入@ohos.zlib进行文件解压。

typescript

import fs from '@ohos.file.fs'; import zlib from '@ohos.zlib'; async function downloadAndUnzip(context: Context, url: string, version: string) { const targetDir = context.filesDir + '/offline_h5'; const zipPath = context.filesDir + '/temp_h5.zip'; // 1. 模拟下载 zip 包 (省略网络请求代码) // ... // 2. 解压前,清理旧版本文件极其重要 if (fs.accessSync(targetDir)) { fs.rmdirSync(targetDir); } // 3. 解压 try { await zlib.decompressFile(zipPath, targetDir); // 记录版本号,用于下次比对 await saveLocalVersion(context, version); } catch (err) { console.error('离线包解压失败', err); } finally { // 清理临时 zip fs.unlinkSync(zipPath); } }

第二步:加载沙箱文件。

typescript

// 获取绝对路径并拼接 file:// 协议 let sandboxPath = 'file://' + getContext(this).filesDir + '/offline_h5/index.html'; Web({ src: sandboxPath, controller: this.controller })

三、跨域死结与本地拦截

用file://加载,最大的拦路虎依然是跨域(CORS)。在file://协议下,浏览器的同源策略极其严苛,哪怕你的接口服务器开启了Access-Control-Allow-Origin: *,在某些内核版本下依然会被拦截。

解法一:服务端完全配合(推荐)
确保你的接口服务端不仅配置了 CORS,还允许了OPTIONS预检请求。并且在响应头里放行所有自定义 Header。

解法二:原生请求代理(终极方案)
如果不信任服务端配置,或者 H5 内部用了特定的网络库(如 Axios 的某些拦截器)死活跨域,我们需要做一层原生代理。
在 H5 初始化时,注入一段 JS,重写XMLHttpRequest,将所有请求转发给原生的 Bridge 发起:

javascript

// 注入到 H5 的 polyfill 脚本 const originalFetch = window.fetch; window.fetch = async function(url, options) { // 通过我们之前注册的 hmNative 拦截 if (window.hmNative && window.hmNative.request) { return window.hmNative.request(url, JSON.stringify(options)) .then(res => new Response(res)); } return originalFetch(url, options); };

这套方案开发成本极高,需要原生实现完整的 HTTP 请求转发并处理 Response 流。除非万不得已,不建议使用。

https://placeholder-02.png/

四、增量更新与灰度发布

如果离线包有 20MB,每次更新都全量下载,用户的流量和你的 CDN 成本都扛不住。

引入增量更新机制:
服务端使用bsdiff算法,比对旧版本和新版本的 zip 包,生成一个只有几百 KB 的差分包(patch)。客户端下载 patch,使用bspatch算法与本地旧 zip 合并,生成新 zip。

虽然 HarmonyOS 目前没有直接提供 bspatch 的官方 API,但可以通过 NAPI 引入 C/C++ 的三方库(如libbsdiff)来实现。

灰度发布策略:
千万不要把新包一次性推给所有用户。必须有个降级开关:

  1. 服务端根据用户 ID 的 hash 值,逐步下发新包下载任务。

  2. 如果前端加载新包时出现白屏或 JS 报错,立刻触发onErrorReceive,端侧上报错误日志,并自动回退加载rawfile里的基础老版本。

  3. 服务端立刻切断该版本的灰度下发。

https://placeholder-03.png/

五、实战总结与思考

优化完离线包,我们的商品详情页首屏时间降到了 300 毫秒以内,基本上做到了“秒开”。这背后不仅仅是技术的堆叠,更是对资源生命周期和网络环境的极深把控。

我的避坑心得:

  1. 离线包的完整性校验不容忽视。下载完 zip 必须做 MD5 或 SHA256 校验,防止被运营商劫持篡改,注入恶意脚本。

  2. file://协议下的 localStorage 是不同步的。不同的file://路径可能对应不同的存储空间,建议前端统一使用 IndexedDB,或者依靠原生桥接来处理持久化。

  3. 不要迷信缓存。涉及到用户核心资产(如支付状态、最新订单)的页面,老老实实走网络请求,强制加上时间戳防止缓存。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询