RNOH 沙箱加载实战:OpenHarmony 侧 raw 文件推入沙箱、JS Bundle 加载与沙箱图片展示

原创2026-10-13 13:36:391,222 阅读
文章标签:移动开发跨平台OpenHarmony

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)

它们对应文档中描述的两个演示:

  1. OpenHarmony 侧演示:把 rawfile 中的文件推送到应用沙箱,随后成功加载沙箱中的 JS Bundle;
  2. 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;

三种图片来源对应两种能力:

  1. require('../../../assets/expo.png'):随 Bundle 打包进沙箱 assets 的资源,RNOH 会按沙箱路径规则解析;
  2. source={{ uri: 'file:///data/storage/el2/base/files/call.png' }}:通过 file:// URI 直接指向沙箱物理路径,这是 RN 侧读取沙箱图片的最直接方式;
  3. 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 可还原每一步的实质:

  1. 安装依赖:在 docs/Samples/Sandbox/SandBoxRnProject 目录下执行 pnpm i;
  2. 生成 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 推送到沙箱并成功加载后的运行效果如下图所示:

沙箱加载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、按需加载资源等生产级能力。

登录后查看全文
ohos_react_native