React Native Harmony 图片资源沙箱路径加载机制详解
React Native Harmony 图片资源沙箱路径加载机制详解
导读
在 React Native 鸿蒙化(RNOH)应用中,当 JS Bundle 以文件形式放入应用沙箱(Sandbox)或随 HAP 打进 rawfile 目录时,图片等静态资源的实际加载路径与开发期 Metro 服务器模式完全不同。本文以 image组件资源沙箱路径加载.md 为核心,结合 Sandbox 示例工程 与 RNOH 图片缓存层的 C++ 实现,完整讲解图片资源在「打包 → 沙箱 / RAWFILE」两个场景下的路径合成规则,并给出可复现的工程配置与源码级验证。读完本文,你将掌握 require('./d.png') 这类写法在鸿蒙侧最终被解析成何种 URI、assets 目录在两种场景下存在与否的根本差异,以及如何在自己的 RNOH 工程中正确配置 bundle 与图片资源的落盘位置。
1 打包阶段的路径语义:相对路径是后续一切的基础
1.1 打包路径假设
文档以「打包路径为根路径 /」为假设建立模型。设 RN 代码位于 /aaa/bbb/c.tsx,三张图片分别位于不同目录:
| 图片 | 所在路径 | RN 中的写法 |
|---|---|---|
| 图片 1 | /aaa/bbb/d.png |
require('./d.png') |
| 图片 2 | /eee/f.png |
require('../../eee/f.png') |
| 图片 3 | /aaa/ggg/h.png |
require('../ggg/h.png') |
注意 require() 的相对路径是相对于引用它的 JS 文件所在目录计算的:c.tsx 在 /aaa/bbb/,因此 ./d.png 实际指向 /aaa/bbb/d.png,../../eee/f.png 是 /aaa/bbb/ 向上两级到 / 再进入 eee/,../ggg/h.png 则是向上到 /aaa/ 再进入 ggg/。
1.2 图片路径前缀:Metro 生成的资源标识
打包时 Metro 会把每张图片映射为一个「相对工程根的路径前缀 + 文件名」,即文档中的图片路径前缀:
- 图片 1 前缀:
/aaa/bbb/ - 图片 2 前缀:
/eee/ - 图片 3 前缀:
/aaa/ggg/
这一前缀的生成逻辑可以在 packages/react-native-harmony-cli/src/assetResolver.ts 中看到:getResourceIdentifier 取 Metro 提供的 httpServerLocation(即图片相对工程根的目录)拼接文件名,最终得到 aaa/bbb/d.png、eee/f.png 这类相对路径。copyAssets 会按 getAssetDestRelativePath 把这些文件拷贝到目标目录,并保留目录层级。
1.3 资源打包结果:assets 目录
文档明确:assets 是在 package.json 中指定的目录,开发者可以任意设置目录名。上述三张图片打包后形成如下结构:
assets
|--aaa/bbb/d.png
|--aaa/ggg/h.png
|--eee/f.png
也就是说,打包结果完整保留了图片相对工程根的目录层级(aaa/bbb/、aaa/ggg/、eee/ 三个前缀都被保留下来),而不是拍平成一个平铺目录。这一点非常关键——正是由于层级被保留,后续运行时才能依据「图片相对打包路径」反推出实际物理位置。
2 沙箱场景:bundle 路径即前缀,图片与 bundle 同根
2.1 路径合成规则
在沙箱场景下,假设 bundle 落在 /data/storage/base/files/dir1/bundle.harmony.js,则三张图片的实际路径为:
| 图片 | 沙箱实际路径 |
|---|---|
| 图片 1 | /data/storage/base/files/dir1/aaa/bbb/d.png |
| 图片 2 | /data/storage/base/files/dir1/eee/f.png |
| 图片 3 | /data/storage/base/files/dir1/aaa/ggg/h.png |
合成公式可概括为:
图片实际路径 = bundle 所在目录 + 图片相对工程打包路径
即沙箱场景下以 bundle 路径作为前缀,以图片与工程打包路径的相对路径作为后缀。文档特别强调:沙箱场景下这里没有 assets 目录——图片与 bundle 直接同根放置,这是与 RAWFILE 场景最本质的差异。
2.2 示例工程中的实现细节
Sandbox 示例(Index.ets)展示了把 bundle 从 rawfile 推送到沙箱并加载的完整流程:
downloadBundle():通过resourceManager.getRawFileContent('rawfile/bundle/sandbox.harmony.bundle')读出打包好的 bundle,再用fs.createStreamSync(bundlePath, 'w')写入context.filesDir(即/data/storage/base/files)下的sandbox.harmony.bundle;copyRawfileDirToSandbox():递归把 rawfile 中的assets目录复制到沙箱filesDir/assets,注释明确指出「C++ 端根据 bundle 路径推导 assets 前缀为file:///沙箱目录/assets/」;- 最后通过
new FileJSBundleProvider(filesDir + '/' + bundlePath)加载 bundle。
其中 FileJSBundleProvider 是 RNOH 提供的标准 Bundle 提供者之一,其实现见 JSBundleProvider.ts:构造时接收沙箱中的文件路径,getBundle() 用 fs.access(path, READ_ONLY) 校验文件存在后返回 filePath,若不存在则抛出带修复建议的 JSBundleProviderError。
2.3 通过 assetsDest 控制资源子目录
与文档「沙箱场景无 assets 目录」的简化模型相比,真实工程往往借助 assetsDest 配置显式指定资源目录。示例中 RNApp 的 rnInstanceConfig 设置了 assetsDest: 'assets'(见 Index.ets)。该配置项在 RNInstance.ts 中有明确注释:
指定 RN 定位资源(assets)的路径;当使用自定义
--assets-dest执行react-native bundle-harmony时必须设置该项。
因此两种模型并不冲突:文档描述的是「未显式指定子目录时,默认以 bundle 目录为前缀推导」的兜底规则;而实际工程可通过 assetsDest 让资源统一收敛到 沙箱目录/assets/ 下,与 C++ 端推导出的 file:///沙箱目录/assets/ 前缀对齐。
3 RAWFILE 场景:固定前缀 resource://RAWFILE/assets/
3.1 路径合成规则
在 RAWFILE 场景下,bundle 位于 HAP 的 rawfile 目录中,假设路径为 resource://RAWFILE/dir1/bundle.harmony.js,则三张图片的实际路径为:
| 图片 | RAWFILE 实际路径 |
|---|---|
| 图片 1 | resource://RAWFILE/assets/aaa/bbb/d.png |
| 图片 2 | resource://RAWFILE/assets/eee/f.png |
| 图片 3 | resource://RAWFILE/assets/aaa/ggg/h.png |
合成公式为:
图片实际路径 =
resource://RAWFILE/assets/+ 图片相对工程打包路径
文档强调:RAWFILE 场景下图片路径前缀固定指定为 resource://RAWFILE/assets/,且这里的 assets 目录是真实存在的(对应打包时 --assets-dest 指定的 rawfile 目录),与沙箱场景形成鲜明对比。
3.2 源码级验证:AssetLoadInterceptor
上述前缀在 RNOH 图片缓存层的 C++ 实现中可以直接找到证据。ImageCacheAssetLoadInterceptor.cpp 中声明了三个关键常量:
constexpr const char* ASSETS_DEST = "assets/";
constexpr const char* ASSET_PREFIX = "asset://";
constexpr const char* RAWFILE_PREFIX = "resource://RAWFILE/assets/";
拦截器 Resolve() 的解析逻辑(同文件 L224-L243)印证了文档结论:
- 以
asset://开头的 URI(require()资源经 RN 侧转换而来):优先在 HAP rawfile 中查找,失败后再回退到option.assetsPrefix解析出的文件路径(L178-L179 注释); - 以
resource://RAWFILE/assets/开头的 URI:GetRawfileRelativePath()剥掉固定前缀后,得到图片相对路径,再拼回assets/在 rawfile 中逐级尝试命中(对应CollectReleaseRawfileCandidates,同文件 L106-L120)。
这正好解释了两种场景的差异来源:沙箱场景资源在真实文件系统(file://),RAWFILE 场景资源在 HAP 包内的只读 rawfile(resource://),二者前缀推导基准完全不同,但后缀都是「图片相对工程打包路径」,因此第 1 章保留目录层级的打包结果在此刻发挥了决定性作用。
4 端到端实战:Sandbox 示例如何把资源送进沙箱
4.1 RN 侧打包命令
Sandbox 的 RN 工程(SandBoxRnProject/package.json)通过 react-native bundle-harmony 一次性产出 bundle 与资源:
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
注意 --assets-dest 指向的是rawfile 根目录,于是打包结果被整体放进 rawfile/assets/ 下(对应第 3 章「assets 目录存在」)。这也解释了示例为何在 downloadBundle() 之后要调用 copyRawfileDirToSandbox('rawfile/assets', ...):把 rawfile 里的 assets 树递归复制到沙箱,使沙箱场景也能按 assetsDest: 'assets' 命中图片。
4.2 RN 侧加载写法:三种方式对照
示例页面 examples.tsx 同时展示了三种加载方式:
// 方式一:require() 相对路径资源(随 bundle 打包进 assets)
<Image source={require('../../../assets/expo.png')} />
// 方式二:显式 file:// 沙箱绝对路径 URI
<Image source={{ uri: 'file:///data/storage/el2/base/files/call.png' }} />
// 方式三:require() 相对路径资源(同样随 bundle 打包)
<Image source={require('../../../assets/call.png')} />
方式二是对本文规则的直接验证:call.png 被推送到沙箱后,通过完整的 file:// 沙箱绝对路径即可被 Image 组件加载;方式一和方式三则由 RNOH 图片加载链路依据第 2/3 章的规则自动把 require() 资源解析为最终 URI。
4.3 推送 bundle 到沙箱的两种途径
在 RNOH 中,把 bundle 推进沙箱有两种方式(详见 沙箱加载bundle.md):
- rawfile 推入沙箱:
getRawFileContent()读取 rawfile 中的 bundle 字节流,用fs.createStreamSync写入context.filesDir下的目标文件,示例代码即上文downloadBundle()所演示; - hdc 工具推送:直接通过命令行把本地 bundle 推送到设备应用沙箱路径:
hdc file send ${待推送文件的本地路径} ${沙箱路径}
第 2 种方式适合在调试阶段快速更新沙箱中的 bundle 与图片,无需重新走构建流程;无论哪种方式,最终图片能否加载都取决于其落盘位置是否与第 2 章的路径合成规则一致。
5 总结与注意事项
| 对比维度 | 沙箱场景 | RAWFILE 场景 |
|---|---|---|
| bundle 示例路径 | /data/storage/base/files/dir1/bundle.harmony.js |
resource://RAWFILE/dir1/bundle.harmony.js |
| 图片前缀基准 | bundle 所在目录(默认,无 assets 目录) | 固定 resource://RAWFILE/assets/ |
| assets 目录 | 默认不存在(可用 assetsDest 显式创建) |
存在(对应 --assets-dest) |
| 资源访问通道 | 真实文件系统(file://) |
HAP 只读 rawfile(resource://) |
| 后缀 | 图片相对工程打包路径 | 图片相对工程打包路径 |
工程落地时需要特别注意:
- 打包阶段保留目录层级是沙箱/RAWFILE 两种场景能正确反解图片路径的前提,
--assets-dest决定资源树落在何处; - 沙箱场景若想统一管理资源,应在
RNApp的rnInstanceConfig中配置assetsDest,并把资源树复制到对应的沙箱子目录(如示例中的filesDir/assets); hdc file send推送资源时,务必使沙箱内的相对布局与打包结果一致,否则图片会因「前缀推导出的路径与实际落盘位置不匹配」而加载失败;- 沙箱路径的读写与文件访问权限需按实际运行环境(API 版本、应用沙箱目录映射)进行调整。
如需完整运行该示例,可参考 README_zh.md:在 SandBoxRnProject 目录执行 pnpm i 安装依赖、pnpm dev 生成 bundle,然后用 DevEco Studio 打开 SandBoxNativeProject 完成签名后运行即可。
