React-Native开发鸿蒙NEXT-多bundle的加载

463 阅读4分钟

ArkTS下加载RNbundle,大致分为以下三个步骤

  • 创建并持有RN实例对象(RNInstance)
  • 创建RN三方组件上下文(RNComponentContext)
  • 创建UI(RNAPP/RNSurface)

基于RN构建的app,可以分为单bundle应用和多bundle应用两类。

单bundle应用很好理解:整个应用基于一个bundle。多bundle应用有通过"A-RN跳转到B-RN"方式的实现,或是界面干脆由多个bundle共同绘制。

对于多bundle应用来说往往需要考虑bundle的预加载。如果跳转对象bundle体积较大在看到界面前会有一定时间的白屏等待(多个bundle已经事先预下载到本地)。虽然可以通过加载动画等方式来缓解,但不能满足部分对延迟较为敏感的客户。鸿蒙上对于多bundle的预加载处理较为简单,下面来看看ArkTS下是如何加载多个bundle的。

还是以index.ets为入口页面进行说明。

在aboutToAppear生命周期中进行单/多个bundle的创建。其中rnohCoreContext是RN自身的上下文,我们加载的bundle都在它下面。

index.ets

@StorageLink('RNOHCoreContext') rnohCoreContext: RNOHCoreContext | undefined = undefined
  aboutToAppear() {
    ......
    // 多bundle
    if (!this.rnohCoreContext) return;
    this.loadMetroBundle()
  }

loadMetroBundle方法是bundle加载的实现,通过LoadManager进行了封装,其中_rnInstance始终指向当前显示的RN对象用于原生和RN的交互(项目中不存在多个bundle同时活跃的情况)。注意assetsDest的设置,在例子中两个bundle的资源文件指向了不同的路径,这里面存在一个未解决问题,下面会提到。

index.ets

loadMetroBundle() {
  let XXXBundle = new BundleBean();
  XXXBundle.debugMode = false;
  XXXBundle.bundlePath = 'XXX/bundle.harmony.js';
  XXXBundle.assetsDest = 'rawfile/XXX/assets';
  XXXBundle.appName = 'XXX';
  LoadManager.loadMetroBundle(XXXBundle).then((flag: boolean) => {
    AppStorage.setOrCreate('isMetroAvailable', flag)
    this.isMetroAvailable = flag
    this.isBundleXXXReady = flag;
    if(flag){
      this._rnInstance = LoadManager.instanceMap.get(XXXBundle.appName);
      this.currentModuleName = XXXBundle.appName;
    }
  })
  let  YYYBundle = new BundleBean();
  YYYBundle.debugMode = false;
  YYYBundle.bundlePath = 'YYY/bundle.harmony.js';
  YYYBundle.assetsDest = 'rawfile/YYY/assets';
  YYYBundle.appName = 'YYY';
  LoadManager.loadMetroBundle(YYYBundle).then((flag: boolean) => {
    AppStorage.setOrCreate('isMetroAvailable', flag)
    this.isMetroAvailable = flag
    this.isBundleYYYReady = flag;
  })
}

LoadManager.ets封装的loadMetroBundle用于创建RN实例与上下文。注意enableCAPIArchitecture的设置,现在只有ture这个选项,HarmonyOS NEXT由于不再支持java,turboModule在原生端只有cpp实现。

LoadManager.ets

public static async loadMetroBundle(bundle:BundleBean): Promise<boolean> {
  const rnohCoreContext: RNOHCoreContext | undefined = AppStorage.get('RNOHCoreContext')
  if (LoadManager.shouldResetMetroInstance && rnohCoreContext && !LoadManager.instanceMap.has(bundle.appName)) {
    let  rnInstance = await rnohCoreContext.createAndRegisterRNInstance({
      createRNPackages: createRNPackages,
      enableNDKTextMeasuring: true,
      enableBackgroundExecutor: true,
      enableCAPIArchitecture: ENABLE_CAPI_ARCHITECTURE,
      arkTsComponentNames: arkTsComponentNames,
      assetsDest:bundle.assetsDest
    });
    LoadManager.ctx = new RNComponentContext(
      RNOHContext.fromCoreContext(rnohCoreContext!, rnInstance),
      wrapBuilder(buildCustomComponent),
      wrapBuilder(buildRNComponentForTag),
      new Map()
    );

    let provider:JSBundleProvider |null = null;
    if(bundle.debugMode){
      provider = new MetroJSBundleProvider();
    }else {
      provider = new ResourceJSBundleProvider(rnohCoreContext.uiAbilityContext.resourceManager, bundle.bundlePath);
    }

    await rnInstance.runJSBundle(provider)

    const jsBundleExecutionStatus: string = rnInstance.getBundleExecutionStatus(provider.getURL()) as string
    console.log(" getBundleExecutionStatus = ", jsBundleExecutionStatus);
    // LoadManager.shouldResetMetroInstance = false
    if (jsBundleExecutionStatus === "DONE") {
      // 缓存bundle
      LoadManager.loadedBundle.add(bundle);
      // 缓存rnInstance
      LoadManager.instanceMap.set(bundle.appName,rnInstance);
      return true
    } else {
      return false
    }
  }
  return true
}

回到index.ets。对于单bundle应用,可以选择RNApp组件作为RN加载容器。

build() {
  Column() {
    if (this.rnohCoreContext && this.shouldShow) {
      if (this.rnohCoreContext?.isDebugModeEnabled) {
        RNOHErrorDialog({ ctx: this.rnohCoreContext })
      }
      RNApp({
        rnInstanceConfig: {
          createRNPackages,
          enableNDKTextMeasuring: true,
          enableBackgroundExecutor: false,
          enableCAPIArchitecture: true,
          arkTsComponentNames: arkTsComponentNames,
        },
        initialProps: { "keyXXX": "valueXXX" } as Record<string, string>,
        appKey: "XXX",
        wrappedCustomRNComponentBuilder: wrappedCustomComponentBuilder,
        onSetUp: (rnInstance) => {
          this._rnInstance = rnInstance;
          rnInstance.enableFeatureFlag("ENABLE_RN_INSTANCE_CLEAN_UP")
        },
        jsBundleProvider: new TraceJSBundleProviderDecorator(
          new AnyJSBundleProvider([
            new MetroJSBundleProvider(),
            // NOTE: to load the bundle from file, place it in
            // `/data/app/el2/100/base/com.rnoh.tester/files/bundle.harmony.js`
            // on your device. The path mismatch is due to app sandboxing on HarmonyOS
            new FileJSBundleProvider('/data/storage/el2/base/files/bundle.harmony.js'),
            new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'hermes_bundle.hbc'),
            new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js')
          ]),
          this.rnohCoreContext.logger),
      })
    }
  }
  .height('100%')
  .width('100%')
}

RNApp组件只能存在一个,对于需要加载多个bundle,尤其是多bundle拼接出页面这种组件化开发场合,可以使用RNSurface作为加载容器。根据参数选择实现bundle的切换或者同时拼接UI。

build() {
  Column() {
       if (this.isBundleXXXReady && this.currentModuleName === "XXX") {
        RNSurface({
          surfaceConfig: {
            appKey: "XXX",
            initialProps: this.initPropsMap.get('XXX'),
          },
          ctx: new RNComponentContext(
            RNOHContext.fromCoreContext(this.rnohCoreContext!, this._rnInstance),
            wrappedCustomComponentBuilder,
            wrapBuilder(buildRNComponentForTag),
            new Map()
          ),
        })
      }
      if(this.isBundleYYYReady && this.currentModuleName === "YYY") {
        RNSurface({
          surfaceConfig: {
            appKey: "YYY",
            initialProps: this.initPropsMap.get('YYY'),
          },
          ctx: new RNComponentContext(
            RNOHContext.fromCoreContext(this.rnohCoreContext!, this._rnInstance),
            wrappedCustomComponentBuilder,
            wrapBuilder(buildRNComponentForTag),
            new Map()
          ),
        })
      }
    }
  }
  .height('100%')
  .width('100%')
}

bundle间的切换通过RN发消息给原生实现。index.ets中,isBundleXXXReady,isBundleYYYReady,currentModuleName均是@state变量。

index.ets

emitter.on(ConstUtil.event_id_open_YYY, (data) => {
  // 带入参数
  this.initProps = {};
  this.initPropsMap.delete('YYY');
  if(data && data.data && data.data.param){
    try {
      this.initPropsMap.set('YYY',JSON.parse(data.data.param) as Record<string,object>);
    } catch (error) {
      console.error("Parsing error:", error);
    }
  }
  // 切换显示YYYRN
  this.currentModuleName = 'YYY';
  this._rnInstance = LoadManager.instanceMap.get('YYY');
  console.info('this.initProps = ' + this.initPropsMap.get('YYY'));
});

项目是基于单RN开发的,多个bundle的逻辑在单RN版本上架后开始修改,边边角角考虑尚有欠缺。最后说下assetDest的问题。目前发现设置资源文件路径后,app还是只会去读取rawfile/assets/文件下的内容,在鸿蒙论坛提问了还是没有找到原因,暂时是将两个RN的资源文件夹名称改成不一样的,让两个资源文件可以都放在rawfile/assets/下(形如rawfile/assets/src/imgXXX与rawfile/assets/src/imgYYY,这样需要批量替换下RN代码里的图片引用路径)。

鸿蒙论坛相关梯子传送门

developer.huawei.com/consumer/cn…

这是大佬的回复,应该是官方马甲。

图片

如有知道为何资源无法正常加载的同学不吝赐教:)

微信扫一扫
关注该公众号悬空八只脚