ArkWeb 混合开发手记 08|离线包、本地资源加载、虚拟域名,离线 H5 落地实
前置阅读:01~07 篇
前面 7 篇全部是在线 H5场景:页面资源全部从网络拉取。 但很多业务需求:
- 弱网环境打开 H5 不白屏
- 部分页面完全离线可用(帮助文档、协议页)
- 把 H5 打包进 APP 安装包,减少首屏请求、提升加载速度
- 本地静态 JS/CSS/ 图片,避免 file 协议带来的跨域、CORS 限制
这就是ArkWeb 离线包。很多新手直接用file://协议加载本地 html,立刻踩坑:H5 内部 ajax 请求、页面跳转、资源引用全部跨域报错,接口无法调用,路由失效。 本篇核心就是:虚拟域名映射方案,把本地 rawfile 资源伪装成 https 域名,彻底规避 file 协议跨域,实现企业级离线包。

一、两种本地资源加载方案对比
方案 1:file:// 直接加载(不推荐)
直接读取 rawfile 或者沙箱文件,url 以file://开头。 缺点:
- 浏览器安全策略限制,大量跨域报错
- fetch、ajax 请求被拦截
- 本地页面无法发起 https 接口请求
- 很多 JS 库、前端框架不兼容 file 协议 适用场景:极简单静态页面,没有接口请求。
方案 2:虚拟 HTTPS 域名映射(推荐,企业离线包标准方案)
核心思路:
- H5 页面写标准 https 地址,例如
https://app-local/index.html - 在 onResourceLoad 拦截这个域名下所有请求
- 拦截命中,读取 APP 包内 rawfile 里的 html/js/css/image 资源返回给 Web 内核
- 内核以为是正常 https 网页,不存在跨域问题,fetch/axios 全部正常
一句话总结:欺骗 Web 内核,让本地资源看起来是线上 HTTPS 网页。
二、离线包目录结构
把 H5 打包放到项目 rawfile/h5_offline/ 目录:
rawfile
└── h5_offline
├── index.html
├── css
│ └── main.css
├── js
│ └── app.js
└── images
└── logo.png
H5 内部资源引用依旧使用相对路径,不用修改前端代码。
三、虚拟域名映射完整代码实现
import { Web, WebController, WebLoadStatus, CacheMode } from '@kit.ArkWeb';
import { router } from '@ohos/router';
import { JsBridgeModel, SpaRouteItem } from './JsBridgeModel';
import { resourceManager } from '@kit.BasicServicesKit';
// 虚拟域名前缀,前端页面全部使用这个域名
const VIRTUAL_HOST = "https://app-local";
@Entry
@Component
struct ArkWebOfflinePage {
private webController: WebController = new WebController();
private jsBridge: JsBridgeModel = new JsBridgeModel();
@State spaRouteStack: SpaRouteItem[] = [];
@State loadStatus: string = "离线包页面待加载";
aboutToAppear() {
// 离线包推荐 OnlyLocal:只读取本地资源,不走网络
this.webController.setCacheMode(CacheMode.OnlyLocal);
this.jsBridge.onSpaRouteChange = (route) => {
if (route.isReplace) {
this.spaRouteStack.pop();
}
this.spaRouteStack.push(route);
};
}
onPageShow() {
this.webController.resume();
}
onPageHide() {
this.webController.pause();
}
aboutToDisappear() {
this.jsBridge.h5Callback = null;
this.jsBridge.onSpaRouteChange = null;
this.spaRouteStack = [];
this.webController.destroy();
}
private handleSpaBack() {
if (this.spaRouteStack.length > 1) {
this.spaRouteStack.pop();
this.webController.runJavaScript(`history.back()`);
} else {
router.back();
}
}
build() {
Column() {
Text(`离线SPA栈深度:${this.spaRouteStack.length} | ${this.loadStatus}`)
.fontSize(14)
.margin(10)
Web({
src: `${VIRTUAL_HOST}/index.html`,
controller: this.webController
})
.width('100%')
.height('88%')
.javaScriptAccess(true)
.registerJavaScriptProxy(
this.jsBridge,
"nativeBridge",
["getDeviceInfo", "showNativeToast", "getNativeData", "pushSpaRoute", "syncUserState"]
)
// 核心:拦截虚拟域名请求,映射rawfile本地资源
.onResourceLoad(async (resourceParam) => {
const url = resourceParam.url;
// 判断是否命中我们的虚拟域名
if (url.startsWith(VIRTUAL_HOST)) {
// 把虚拟url路径转换成rawfile路径
let rawPath = url.replace(VIRTUAL_HOST, "h5_offline");
// 处理根路径默认index.html
if(rawPath.endsWith("/")){
rawPath += "index.html";
}
// 读取rawfile文件
try {
const ctx = getContext();
const fileData = await ctx.resourceManager.getRawFileContent(rawPath);
resourceParam.responseData = fileData;
// 返回true,终止网络请求,使用本地资源
return true;
} catch (err) {
console.error("读取离线资源失败", rawPath, err);
return false;
}
}
// 非虚拟域名的请求放行(比如H5需要请求业务后端接口)
return false;
})
.onLoadStatusChange((status) => {
if(status === WebLoadStatus.FinishLoading){
this.loadStatus = "离线页面加载完成";
}else if(status === WebLoadStatus.ErrorLoading){
this.loadStatus = "离线资源加载失败";
}
})
}
.width('100%')
.height('100%')
.onBackPressed(() => {
this.handleSpaBack();
return true;
})
}
}

四、MIME 类型配置(极易踩坑)
只返回二进制资源还不够,必须设置正确 MIME,否则浏览器无法识别 css/js/png,渲染异常。
// 简易MIME映射表
function getMime(fileName:string):string{
if(fileName.endsWith(".html")) return "text/html;charset=utf-8";
if(fileName.endsWith(".js")) return "application/javascript;charset=utf-8";
if(fileName.endsWith(".css")) return "text/css;charset=utf-8";
if(fileName.endsWith(".png")) return "image/png";
if(fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) return "image/jpeg";
return "text/plain";
}
拿到资源后,给 responseData 配套设置 contentType。
坑点:MIME 写错,JS 不执行、样式不生效,控制台不报明显错误,很难排查。
五、两种离线包方案:内置包 vs 动态增量离线包
5.1 内置 rawfile 离线包(静态打包)
H5 资源打包进 APP 安装包。 ✅优点:打开快,不需要下载,安装完直接可用 ❌缺点:H5 更新,必须发 APP 版本升级,不能热更新
5.2 沙箱动态离线包(增量热更新)
APP 启动时,后台下载 zip 离线包解压到应用沙箱目录 filesDir,加载沙箱内 html。 适合 H5 频繁迭代,不用升级 APP,实现 H5 热更新。 读取沙箱文件不再使用 resourceManager,改用文件模块 fs 读取沙箱路径资源。
// 沙箱文件读取示意(动态离线包)
import fs from '@ohos.file.fs';
let file = await fs.open(path);
let stat = await fs.stat(path);
let buffer = new ArrayBuffer(stat.size);
await fs.read(file.fd, buffer);
六、离线包 + 在线混合模式(最常用业务方案)
很多项目不是完全离线,而是:
- 静态页面、图片、JS 缓存本地离线包
- 业务接口继续请求线上后端 虚拟域名方案天然支持这种混合模式。虚拟域名资源拦截,外部接口请求直接放行,不需要任何额外改造。
七、离线包场景高频踩坑清单
坑 1:前端页面写绝对路径,路径匹配失败
离线 H5 内部资源全部用相对路径,不要写 https 全量地址。
坑 2:rawfile 资源名不能带中文、空格、特殊字符 rawfile 限制文件名,中文资源直接读取失败,打包前前端资源改名。
坑 3:忘记设置 MIME,页面样式丢失,JS 不执行
坑 4:SPA 路由刷新 404 SPA 离线包必须做路由 fallback:当请求的资源不存在,返回 index.html,由前端 vue-router/react-router 接管路由。 在 onResourceLoad 捕获文件不存在异常时,返回 index.html 资源。
坑 5:离线包体积过大 rawfile 打包会增加 APP 包体积,大资源优先放到线上 CDN,离线包只放核心页面。

八、离线包 SPA 路由 fallback 兜底代码
SPA 离线包,刷新子路由地址时,Web 内核会尝试去读取对应路径文件,rawfile 不存在,直接报错。 解决方案:捕获资源读取异常,返回 index.html,交给前端路由处理。
.onResourceLoad(async (resourceParam) => {
const url = resourceParam.url;
if (url.startsWith(VIRTUAL_HOST)) {
let rawPath = url.replace(VIRTUAL_HOST, "h5_offline");
if(rawPath.endsWith("/")) rawPath += "index.html";
try {
const ctx = getContext();
const fileData = await ctx.resourceManager.getRawFileContent(rawPath);
resourceParam.responseData = fileData;
return true;
} catch (err) {
// 文件不存在,fallback返回首页index.html,SPA路由接管
const ctx = getContext();
const indexData = await ctx.resourceManager.getRawFileContent("h5_offline/index.html");
resourceParam.responseData = indexData;
return true;
}
}
return false;
})
九、本篇小结
本篇实现鸿蒙 ArkWeb 企业级离线包,使用虚拟域名方案解决本地资源跨域,支持静态内置包、沙箱动态离线包,同时兼容 SPA 前端框架。 你学会的能力:
- 区分 file 协议和虚拟 HTTPS 域名两种本地加载方案,理解跨域根源
- rawfile 资源拦截映射,构建离线 H5 页面
- MIME 类型配置,解决 JS/CSS 渲染失效问题
- SPA 离线包路由 fallback,解决子页面刷新 404
- 混合离线 + 在线模式,静态资源本地,接口访问线上服务端
实操指南:
- 把一个简单 Vue 打包产物放到 rawfile,使用虚拟域名离线加载,验证页面正常渲染
- 访问 SPA 子路由,测试 fallback 逻辑,验证刷新不 404
- 修改 MIME,故意写错,观察页面失效现象,加深理解


更多推荐


所有评论(0)