RNOH 沙箱加载实战:OpenHarmony 侧 raw 文件推入沙箱、JS Bundle 加载与沙箱图片展示
RNOH 沙箱加载实战:OpenHarmony 侧 raw 文件推入沙箱、JS Bundle 加载与沙箱图片展示
本文基于 ohos_react_native 仓库中的 Sandbox 示例工程,系统讲解在 React Native 鸿蒙化(RNOH)应用里如何把 rawfile 中的 JS Bundle 与静态资源推送到应用沙箱,再通过 FileJSBundleProvider 从沙箱加载 Bundle,并在 React Native 侧展示沙箱中的图片。读完本文,你将掌握两条沙箱推送路径(rawfile 复制与 hdc 命令推送)、Bundle 资源在沙箱与 rawfile 两种场景下的路径合成规则,以及一整套可复现的工程运行流程。
示例工程的两个演示
docs/Samples/Sandbox 目录下的示例工程由两个子工程组成,分别覆盖沙箱能力的两侧:
docs/Samples/Sandbox
├── SandBoxNativeProject # OpenHarmony 原生工程(ArkTS)
└── SandBoxRnProject # React Native 工程(打包 JS Bundle)
它们对应文档中描述的两个演示:
- OpenHarmony 侧演示:把
rawfile中的文件推送到应用沙箱,随后成功加载沙箱中的 JS Bundle; - React Native 侧演示:在 RN 应用中加载并显示 OpenHarmony 沙箱中的图片。
从 RNOH 的架构角度看,应用沙箱(context.filesDir)是 JS Bundle 和图片资源在设备上落地、被持久化读取的关键位置。将 Bundle 从随应用打包的 rawfile 资源目录“搬运”到沙箱,使得应用具备后续动态更新 Bundle、按需下发资源的能力(沙箱中的文件可被运行期覆盖或替换)。
目录结构速览
两个子工程的核心文件如下:
SandBoxNativeProject
└── entry/src/main/ets
├── entryability/EntryAbility.ets # 继承 RNAbility,暴露 rnAbility 上下文
├── pages/Index.ets # downloadBundle 推送、FileJSBundleProvider 加载
└── RNPackagesFactory.ts # RN 包工厂(示例中为空实现)
SandBoxRnProject
└── src/bundles/Sandbox
├── index.js # bundle 打包入口
└── examples.tsx # RN 侧加载沙箱图片
EntryAbility继承自 RNOH 的RNAbility,并在onWindowStageCreate中通过AppStorage.setOrCreate('RNAbility', this)将 Ability 实例暴露给页面,供Index.ets读取context(EntryAbility.ets);Index.ets是沙箱推送与加载逻辑的主战场(Index.ets);examples.tsx演示了 RN 侧引用沙箱图片的三种写法(examples.tsx)。
OpenHarmony 侧:将 raw 文件推送到沙箱
沙箱示例文档《沙箱加载bundle.md》给出了两种把 Bundle 推入沙箱的方式,本节结合 Index.ets 的完整源码逐条展开。
方式一:从 rawfile 读取并写入沙箱
Index.ets 中的 downloadBundle() 使用 resourceManager.getRawFileContent 读取 rawfile 中的 Bundle 字节流,再用 @ohos.file.fs 以同步流方式写入 filesDir:
async downloadBundle() {
if (this.rnAbility) {
// 将 bundlejs 下载并保存到沙箱
let uint8Array =
await this.rnAbility.context.resourceManager.getRawFileContent('rawfile/bundle/sandbox.harmony.bundle');
let rawBuffer = uint8Array.buffer;
let num = rawBuffer.byteLength;
// 获取沙箱路径
const sandboxDir = this.rnAbility.context.filesDir;
const bundlePath = sandboxDir + '/' + this.bundlePath;
let stream = fs.createStreamSync(bundlePath, 'w');
stream.writeSync(rawBuffer);
stream.closeSync();
// 将 assets 资源复制到沙箱,C++ 端根据 bundle 路径推导 assets 前缀为 file:///沙箱目录/assets/
const assetsSandboxDir = sandboxDir + '/' + this.assetsPath;
await this.copyRawfileDirToSandbox('rawfile/assets', assetsSandboxDir);
this.hasBundle = true;
}
}
关键点说明:
bundlePath与assetsPath:在Index.ets顶部定义,均为相对rnAbility.context.filesDir的路径(bundlePath = 'sandbox.harmony.bundle',assetsPath = 'assets');getRawFileContent的入参带rawfile/前缀:'rawfile/bundle/sandbox.harmony.bundle'是 resourceManager 读取 rawfile 的标准写法;- 写入目标:
sandboxDir + '/' + this.bundlePath即/data/storage/el2/base/files/sandbox.harmony.bundle(filesDir的真实物理路径对应应用沙箱的 el2 目录)。
除了 Bundle 本体,图片等静态资源也必须一并复制到沙箱。copyRawfileDirToSandbox 会递归遍历 rawfile 下的 assets 目录,逐个判断条目是文件还是目录后分别处理:
async copyRawfileDirToSandbox(rawfilePath: string, sandboxPath: string) {
// 确保目标目录存在
if (!fs.accessSync(sandboxPath)) {
fs.mkdirSync(sandboxPath, true);
}
// 读取 rawfile 目录下的文件列表
const fileNames = await this.rnAbility!.context.resourceManager.getRawFileList(rawfilePath);
for (const fileName of fileNames) {
const srcRawPath = rawfilePath + '/' + fileName;
const destPath = sandboxPath + '/' + fileName;
// 判断是文件还是目录:尝试读取内容,如果成功则为文件
try {
const content = await this.rnAbility!.context.resourceManager.getRawFileContent(srcRawPath);
let stream = fs.createStreamSync(destPath, 'w');
stream.writeSync(content.buffer);
stream.closeSync();
} catch (e) {
// 读取内容失败,可能是目录,递归处理
await this.copyRawfileDirToSandbox(srcRawPath, destPath);
}
}
}
这里用“先按文件读取、失败则按目录递归”的方式实现目录探测,逻辑简洁且避免了额外 API。复制完成后,沙箱中会出现 files/assets/ 目录,与 Bundle 同级——这与后文要讲的沙箱图片路径合成规则直接相关。
方式二:使用 hdc 工具推送文件到沙箱
当 Bundle 或资源不在 rawfile 中、而存在于开发机本地时,可以使用 hdc file send 命令直接推送到设备应用沙箱:
hdc file send ${待推送文件的本地路径} ${沙箱路径}
文档特别提示:推送命令中的沙箱路径可通过「应用沙箱路径和真实物理路径的对应关系」换算得到,即先确认目标应用 filesDir 对应的真实物理路径,再执行推送。这种方式适合调试阶段反复替换 Bundle,无需重新打包安装。
启动时的自检与兜底:checkBundleUpdated
Index.ets 在 aboutToAppear 中调用 checkBundleUpdated(),对沙箱中的 Bundle 与 assets 目录做存在性校验:
checkBundleUpdated(): void {
if (this.rnAbility) {
const sandboxDir = this.rnAbility.context.filesDir;
const bundlePath = sandboxDir + '/' + this.bundlePath;
const assetsDir = sandboxDir + '/' + this.assetsPath;
try {
const bundleStat = fs.statSync(bundlePath);
// 检查 assets 目录是否也存在,确保图片资源已复制到沙箱
fs.statSync(assetsDir);
if (bundleStat.size === 0) {
this.downloadBundle();
} else {
this.hasBundle = true;
}
} catch (e) {
this.downloadBundle();
}
}
}
逻辑说明:若沙箱中已有非空的 Bundle 且 assets 目录存在,则直接进入加载流程;否则(文件不存在、为空或异常)触发 downloadBundle() 重新推送。这种“先检查、后兜底”的设计保证了首次启动与异常恢复都能自洽运行。
用 FileJSBundleProvider 从沙箱加载 Bundle
文件推入沙箱后,页面通过 new FileJSBundleProvider(path) 指定 Bundle 来源。Index.ets 中的完整用法:
RNApp({
rnInstanceConfig: {
createRNPackages,
enableCAPIArchitecture: true,
arkTsComponentNames: [],
assetsDest: 'assets', // 指定沙箱中 assets 资源目录名
},
appKey: 'Sandbox',
wrappedCustomRNComponentBuilder: wrappedCustomRNComponentBuilder,
onSetUp: (rnInstance) => {
rnInstance.enableFeatureFlag('ENABLE_RN_INSTANCE_CLEAN_UP');
},
jsBundleProvider: new FileJSBundleProvider(this.rnAbility.context.filesDir + '/' + this.bundlePath)
});
jsBundleProvider 即为 Bundle 加载策略的抽象入口。在 RNOH 运行时中,所有 Bundle 提供方都继承自抽象类 JSBundleProvider,JSBundleProvider.ts 中定义了 getURL()、getBundle()、getAppKeys() 等核心契约,其设计意图是“解耦 RNOH 核心逻辑与 Bundle 的加载方式,由应用开发者决定何时、用哪个具体 Bundle”。
FileJSBundleProvider 的实现非常简单直接:
export class FileJSBundleProvider extends JSBundleProvider {
constructor(private path: string, private appKeys: string[] = []) {
super();
}
getURL(): string {
return this.path;
}
getAppKeys(): string[] {
return this.appKeys;
}
async getBundle(onProgress?, onProviderSwitch?): Promise<FileJSBundle> {
const status = await fs.access(this.path, fs.OpenMode.READ_ONLY);
if (status) {
return { filePath: this.path };
} else {
throw new Error("The file can't be accessed.");
}
}
}
即:先以只读方式 fs.access 校验沙箱文件可达,随后返回 { filePath } 交给 RNOH 加载;若文件不可访问,则抛出 JSBundleProviderError,错误信息会给出修复建议(检查该路径下是否存在 Bundle)。RNOH 还提供其他 Provider,可对比理解定位差异:
ResourceJSBundleProvider:从rawfile(resource://RAWFILE/)加载 Bundle;MetroJSBundleProvider:从 Metro 开发服务器按 URL 拉取 Bundle(支持热重载);AnyJSBundleProvider:传入多个 Provider,按顺序逐个尝试,取第一个成功者;TraceJSBundleProviderDecorator:为内层 Provider 增加打点追踪的装饰器。
对应地,JSBundleProvider.test.ts 对 FileJSBundleProvider 的行为做了单元测试覆盖:文件可访问时返回 {filePath}、文件不可访问或 fs.access 抛错时抛出 JSBundleProviderError,验证了上述契约。
从沙箱加载 Bundle 时,assets 资源路径需要与沙箱目录联动,这正是 rnInstanceConfig 中 assetsDest: 'assets' 的作用:RNOH 在沙箱场景下会以 Bundle 所在目录为前缀、拼上 assetsDest 推导出资源目录(对应前文复制到沙箱的 files/assets/)。
React Native 侧:加载沙箱中的图片
沙箱图片展示位于 examples.tsx,它在一个 View 中并排渲染了三张图(examples.tsx):
import React from 'react';
import { Text, Image, View, StyleSheet } from 'react-native';
const Sandbox = () => {
return (
<View style={{ marginTop: 20, backgroundColor: '#def5ee', width: '90%', borderRadius: 10,
marginLeft: 'auto', marginRight: 'auto' }}>
<Text style={{ fontWeight: 600, marginTop: 20 }}>沙箱bundle渲染</Text>
<View style={{ display: 'flex', alignItems: 'center', flexDirection: 'row', width: '100%' }}>
<Image source={require('../../../assets/expo.png')} style={{ width: 100, height: 100, margin: 10 }} />
<Image source={{ uri: 'file:///data/storage/el2/base/files/call.png' }} style={{ width: 100, height: 100, margin: 10 }} />
<Image source={require('../../../assets/call.png')} style={{ width: 100, height: 100, margin: 10 }} />
</View>
</View>
);
};
export default Sandbox;
三种图片来源对应两种能力:
require('../../../assets/expo.png'):随 Bundle 打包进沙箱 assets 的资源,RNOH 会按沙箱路径规则解析;source={{ uri: 'file:///data/storage/el2/base/files/call.png' }}:通过file://URI 直接指向沙箱物理路径,这是 RN 侧读取沙箱图片的最直接方式;require('../../../assets/call.png'):与第一种相同,走资源打包解析。
沙箱场景 vs RAWFILE 场景的图片路径合成
文档《image组件资源沙箱路径加载.md》对两种场景下图片最终路径的合成规则给出了精确说明。
打包场景(基准):假设打包根路径为 /,RN 代码在 /aaa/bbb/c.tsx,则:
- 图片
/aaa/bbb/d.png使用require('./d.png'),前缀/aaa/bbb/; - 图片
/eee/f.png使用require('../../eee/f.png'),前缀/eee/; - 图片
/aaa/ggg/h.png使用require('../ggg/h.png'),前缀/aaa/ggg/。
资源打包进 assets 目录(assets 目录名由 package.json 中的配置指定,可自定义):
assets
├── aaa/bbb/d.png
├── aaa/ggg/h.png
└── eee/f.png
沙箱场景:假设 Bundle 位于 /data/storage/base/files/dir1/bundle.harmony.js,则三张图片的实际路径分别为:
/data/storage/base/files/dir1/aaa/bbb/d.png
/data/storage/base/files/dir1/eee/f.png
/data/storage/base/files/dir1/aaa/ggg/h.png
重点:沙箱场景下图片路径 = Bundle 路径所在目录为前缀 + 图片与工程打包路径的相对路径,合成完整路径。这里没有
assets目录。
RAWFILE 场景:假设 Bundle 位于 resource://RAWFILE/dir1/bundle.harmony.js,则图片实际路径为:
resource://RAWFILE/assets/aaa/bbb/d.png
resource://RAWFILE/assets/eee/f.png
resource://RAWFILE/assets/aaa/ggg/h.png
重点:RAWFILE 场景下图片路径前缀固定为
resource://RAWFILE/assets/,同样以“图片与工程打包路径的相对路径”为后缀。这里带有assets目录。
理解这条规则,是排查“沙箱 Bundle 能加载但图片 404”类问题的关键:如果 Bundle 已经落到沙箱,但 assets 资源仍留在 rawfile 中,图片路径将无法按沙箱前缀解析,因此示例中 Index.ets 必须先把 rawfile/assets 完整复制到沙箱(见上文 copyRawfileDirToSandbox)。
完整运行步骤
原文档《README_zh.md》给出了标准运行流程,结合 SandBoxRnProject/package.json 可还原每一步的实质:
- 安装依赖:在
docs/Samples/Sandbox/SandBoxRnProject目录下执行pnpm i; - 生成 Bundle:执行
pnpm dev。该命令实际执行的是(package.json):
react-native bundle-harmony --dev false --entry-file ./src/bundles/Sandbox/index.js \
--bundle-output ../SandBoxNativeProject/entry/src/main/resources/rawfile/bundle/sandbox.harmony.bundle \
--assets-dest ../SandBoxNativeProject/entry/src/main/resources/rawfile \
--config ./sandbox.config.js
即将 src/bundles/Sandbox/index.js 打包为非调试 Bundle,输出到原生工程的 rawfile/bundle/sandbox.harmony.bundle,并将 assets 资源输出到 rawfile 根目录(对应 Index.ets 中 getRawFileContent('rawfile/bundle/sandbox.harmony.bundle') 与 copyRawfileDirToSandbox('rawfile/assets', ...) 的读取路径)。打包配置基于 sandbox.config.js(与 metro.config.js 一样,通过 createHarmonyMetroConfig 合并 RNOH 的 Harmony Metro 配置);
3. 打开并同步工程:用 DevEco Studio 打开 SandBoxNativeProject,等待工程自动同步完成;如需手动同步,执行 Sync and Refresh Project;
4. 配置签名:点击 File > Project Structure > Signing Configs,登录并完成签名;
5. 运行:点击右上角 run 启动项目。应用启动后,Index.ets 的 checkBundleUpdated 会检查沙箱,必要时把 rawfile 中的 Bundle 与 assets 推入沙箱,随后通过 FileJSBundleProvider 加载,最终在 RN 页面渲染出沙箱图片。
注意事项
- 两个工程需配置正确:OpenHarmony 工程与 React Native 工程要配套(Bundle 输出路径、assets 目录、appKey 需一致),文件才能在两侧之间无缝传递与加载;
- 沙箱路径与访问权限按环境调整:
filesDir对应的物理路径、文件访问权限需根据具体设备与 API 版本确认;示例中硬编码的file:///data/storage/el2/base/files/call.png即基于filesDir的实际值; - rawfile 资源需先入库:
pnpm dev生成的rawfile/bundle/sandbox.harmony.bundle与 assets 属于构建产物,本地运行前必须执行打包命令生成; - 资源路径遵循场景规则:沙箱场景与 RAWFILE 场景的图片路径合成规则不同(有无
assets前缀),从 rawfile 加载与从沙箱加载的资源布局应分别按对应规则组织; - 沙箱推送方式可按需选择:调试阶段优先用
hdc file send直接推文件(免重打包),正式运行场景用getRawFileContent+ 流写入在应用内完成推送。
效果预览
将 Bundle 推送到沙箱并成功加载后的运行效果如下图所示:
图中展示了 RN 页面成功渲染“沙箱bundle渲染”文案以及三张沙箱图片来源的展示结果,验证了从 rawfile 推入沙箱、FileJSBundleProvider 加载、RN 侧读取沙箱图片的完整链路。
小结
沙箱加载是 RNOH 应用中实现 Bundle 与资源动态化的重要一环。本示例工程以最小可运行的形式演示了完整闭环:rawfile 资源经 resourceManager 与 @ohos.file.fs 写入应用沙箱(或以 hdc file send 直接推送),FileJSBundleProvider 从沙箱读取 Bundle 交给 RNOH 渲染,RN 侧再按沙箱路径规则引用图片。理解 Index.ets 的推送与自检逻辑、JSBundleProvider.ts 的 Provider 体系,以及图片路径合成规则,即可在此基础上扩展出动态下发 Bundle、按需加载资源等生产级能力。
