Metro热加载

React Native 使用Metro构建 JavaScript 代码和资源,本节介绍了配置步骤和使用方式。

配置Metro

您需要在项目的 metro.config.js 文件中配置 harmony 平台的 Metro 配置选项。它可以导出:

  • 一个对象(推荐),将与Metro的内部配置默认值合并。
  • 一个函数,该函数将使用Metro的内部配置默认值被调用,并返回最终的配置对象。

在 React Native 中,你的 Metro 配置应该扩展https://www.npmjs.com/package/@react-native/metro-config@react-native/metro-config或https://www.npmjs.com/package/@expo/metro-config@expo/metro-config。这些包含构建和运行 React Native 应用所需的基本默认值。

为了在 harmony 平台上配置 Metro,您还需要拓展 react-native-harmony,通过 createHarmonyMetroConfig 创建 harmony 平台的 Metro。

下面是示例工程中默认的 metro.config.js 文件:

// metro.config.js
const { mergeConfig, getDefaultConfig } = require('@react-native/metro-config');
const {
  createHarmonyMetroConfig,
} = require('@react-native-oh/react-native-harmony/metro.config');
/**
 * Metro配置
 * https://metrobundler.dev/docs/configuration
 *
 * @type {import("metro-config").ConfigT}
 */
const config = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};
module.exports = mergeConfig(
  getDefaultConfig(__dirname),
  createHarmonyMetroConfig({
    reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony',
  }),
  config,
);

如果您使用的是 RNApp 启动的 RN 框架,那么这一步可以跳过。如果您使用的是 RNSurface 启动的 RN 框架,您就需要创建一个 RNComponentContext,并在 context 的 devToolsController 中增加如下的事件监听,并删除原有的 RNInstance 实例,重新启动一个新的 RNInstance 实例,即可启用热加载的功能:

this.ctx.devToolsController.eventEmitter.subscribe("RELOAD", async () => {
  this.cleanUp();
  this.init();
});

使用Metro

RNOH 提供了 MetroJSBundleProvider API 用于加载Metro服务提供的 jsBundle,使用方法如下:

  • 场景一 使用 RNApp.ets

    如果你使用了 RNOH 提供的组件 RNApp ,则只需要将 new MetroJSBundleProvider() 传给 jsBundleProvider 属性即可:

    RNApp({
      ...
      // 方式1
      // jsBundleProvider: new MetroJSBundleProvider()
      jsBundleProvider: new TraceJSBundleProviderDecorator(
        new AnyJSBundleProvider([
          // 方式2
          new MetroJSBundleProvider(),
        ]),
        this.rnohCoreContext.logger),
    })
    

    harmony 工程准备好后,用数据线将真机与电脑连接,打开新的命令行工具并执行:

    hdc rport tcp:8081 tcp:8081
    

    接下来打开js侧控制台,在 RN 工程目录中执行以下命令启动 RN 应用:

    npm run start
    

    执行成功后显示以下的内容:

    metro-success

    start 命令会使用工程目录下默认的 metro.config.js,您也可以在 package.json 文件中配置 start 命令,使用自定义的 config 文件:

    ···
    "scripts": {
      "start": "react-native start --config metro.config.harmony.js"
    }
    ···
    
  • 场景二 不使用 RNApp.ets

    不使用 RNApp 的话,需要开发者自己去创建并管理 RNInstance,假设你已经拥有了一个 RNInstance 的实例 rnInstance,加载 Metro 服务的方法可参考如下代码:

    rnInstance.runJSBundle(new MetroJSBundleProvider());
    

    harmony 工程准备好后,接下来打开 js 侧控制台,在 RN 工程目录中执行以下命令启动 RN 应用:

    npm run start
    

    然后用数据线将真机与电脑连接,打开新的命令行工具并执行:

    hdc rport tcp:8081 tcp:8081
    

    执行成功后显示以下的内容:

    metro-success

    start 命令会使用工程目录下默认的 metro.config.js,您也可以在 package.json 文件中配置 start 命令,使用自定义的 config 文件:

    ···
    "scripts": {
      "start": "react-native start --config metro.config.harmony.js"
    }
    ···
    
  • 场景三 访问局域网中的 Metro 服务

    • 方法一:

      上面的方案需要连接数据线并转发8081端口,**RNOH** 另外还提供了另外一个 API(`MetroJSBundleProvider.fromServerIp`),该方法接收以下参数:
      
      • ip - Metro 服务器的 IP 地址

      • port - Metro 服务的端口地址,默认 8081

      • appKeys - App name合集,默认空数组

      • scheme - 协议,取值为 'http' 或 'https',默认 'http'

      • tlsOptions - 可选。仅在 scheme 为 'https' 时生效,用于配置 CA 证书路径与证书校验策略(详见场景四)

        使用方法便是将 MetroJSBundleProvider.fromServerIp 替换掉上面的 new MetroJSBundleProvider(),如:

      RNApp({
          ...
          jsBundleProvider: new TraceJSBundleProviderDecorator(
          new AnyJSBundleProvider([
              MetroJSBundleProvider.fromServerIp('192.168.43.14', 8081),
          ]),
          this.rnohCoreContext.logger),
      })
      
    • 方法二:

      可以通过摇一摇弹出React Native Dev Menu对话框,点击Settings选项,进入Settings页面后点击Debug Server host & port for device选项,会弹出输入框,输入地址后重新打开应用即可。输入支持 IP:Port(默认按 http 处理)或带协议的完整地址(如 http://10.0.0.1:8081、https://10.0.0.1:8081)。若使用 https://,还需配置证书信任策略,详见场景四。

      如果希望通过摇一摇打开React Native Dev Menu对话框,需要在module.json5内配置权限

      "requestPermissions": [
          {
              "name": "ohos.permission.ACCELEROMETER"
          }
      ],
      

    这种方式只需要保证手机能连通电脑,打开 APP 后便能访问 Metro 服务,不需要连接数据线和转发端口。

  • 场景四 Metro 支持 TLS(HTTPS / WSS)服务

    RNOH 支持通过 HTTPS 从 Metro 拉取 JS Bundle;热更新(HMR)、Dev Menu 消息通道与调试器连接会随 Bundle URL 的协议自动升级为 WSS。适用于需要加密开发链路,或局域网内使用自签名 / 私有 CA 证书的场景。

    1. 启动带 TLS 的 Metro 服务

    先准备证书与私钥(可用 openssl、mkcert 等工具生成;若设备需通过局域网 IP 访问,证书 SAN 中需包含该 IP 或对应域名),然后在 RN 工程目录启动 Metro:

    npx react-native start --https --key ./certs/key.pem --cert ./certs/cert.pem
    

    也可在 package.json 的 start 脚本中写入上述参数。--https 开启 HTTPS;--key / --cert 分别指定私钥与证书路径。

    2. 应用侧通过 HTTPS 加载 Bundle

    使用 MetroJSBundleProvider.fromServerIp,将 scheme 设为 'https',并按需传入 tlsOptions:

    // caPath 为应用可读的 CA 证书绝对路径,例如将 rawfile 中的证书拷贝到 cacheDir 后的路径
    const metroTlsOptions = {
      caPathProvider: (url: string) => url.includes('192.168.') ? caPath : '',
      remoteValidation: 'system', // 开发自测可临时改为 'skip'(跳过证书校验,仅用于开发测试)
    };
    
    RNApp({
      ...
      jsBundleProvider: new TraceJSBundleProviderDecorator(
        new AnyJSBundleProvider([
          MetroJSBundleProvider.fromServerIp(
            '192.168.43.14',
            8081,
            [],
            'https',
            metroTlsOptions,
          ),
        ]),
        this.rnohCoreContext.logger),
      // 热更新 / 调试器等通道使用 RNInstance 上的 TLS 配置,需与 Bundle 拉取策略保持一致
      rnInstanceConfig: {
        caPathProvider: metroTlsOptions.caPathProvider,
        remoteValidation: metroTlsOptions.remoteValidation,
        ...
      },
    })
    

    tlsOptions / rnInstanceConfig 中与 TLS 相关的字段说明:

    字段说明
    caPathProvider(url: string) => string。按 URL 返回 CA 证书路径;返回空字符串表示使用系统默认 CA。mkcert / 私有 CA 场景应返回应用可读的证书绝对路径
    remoteValidation证书校验模式:'system'(系统 CA 校验)、'skip'(跳过校验,仅开发测试)、或 ValidationCallback(自定义回调,API >= 26)

    注意:

    • Bundle 拉取使用 MetroJSBundleProvider 的 tlsOptions;HMR、Inspector 等通道使用 rnInstanceConfig 中的 caPathProvider / remoteValidation。两者建议配置一致。
    • 若通过 Settings 配置 https://IP:Port 并由 new MetroJSBundleProvider() 读取该地址,仍需将 tlsOptions 传入构造函数(例如 new MetroJSBundleProvider('', [], metroTlsOptions)),并同步配置 rnInstanceConfig。
    • Worker 线程中的网络请求(如图片加载)需在 RNOHWorker.ets 中单独配置 remoteValidation / caPathProvider,详见如何配置自定义 HTTPS 证书验证。
    • remoteValidation: 'skip' 仅用于开发测试,请勿用于生产环境。

使用 Metro 服务加载 bundle 时如下场景皆可触发应用刷新:

  • 在编辑器中编辑代码,并保存,便可以看到改动点自动更新到手机上
  • 在命令行工具中输入r,也会触发应用更新
  • 在命令行工具中输入d,在弹出的 React Native Dev Menu 对话框中选择 Reload 也会触发应⽤刷新

JS调试

打开React Native Dev Menu

注意:在 Release 模式下,Dev Menu 不可使用。

  1. 分别执行以下命令:

    hdc rport tcp:8081 tcp:8081
    
    npm run start
    

    执行成功后显示以下的内容:

    metro-success

  2. 在手机上打开应用。

  3. 在命令行工具中输入 d,便可以看到手机上弹出了 React Native Dev Menu 对话框。

    调测调试-打开DevMenu

Element Inspector

您可以通过 Element Inspector 查看 RN 元素的盒子模型及样式,具体操作步骤如下:

  1. 通过 Metro 加载 bundle,打开 React Native Dev Menu对话框后选择Toggle Element Inspector`。

  2. 选择 Inspect,然后点击要审查的元素,便可看到黑色蒙层中显示出了元素的层级关系、样式以及盒子模型,如图:

    Inspect图片

  3. 点击蒙层中面包屑的其他节点可查看当前元素的父级/子级元素的样式。

  4. 重新点击页面上的其他元素,便可切换到该元素。

  5. 取消选中蒙层中的 Inspect,便可关闭元素审查。

断点调试

在启用断点调试之前,需要您启动RN实例的时候开启 debugger。您可以在 RNApp 中以 rnInstanceConfig 的参数的形式传入:

// index.ets
RNApp({
  rnInstanceConfig: {
    enableDebugger: true,
    ···
  },
  ···
})

在编码过程中,您可以使用React DevTools 来设置断点,并调试您的代码。

报错信息输出

LogBox日志

LogBox 是手机侧的故障提示框,当js侧发生错误时手机的应用界面上便会弹出错误告警,如图:

logbox

右侧部分即为 LogBox,展示了错误的说明、源码以及调用栈信息。如果您使用的是Metro服务加载bundle,点击设备上 LogBox 中的调用栈,便可在电脑端中使用编辑器(如VSCode)自动打开对应的文件以供您查看并定位问题。

如果您使用的是 RNApp 启动的 RN 框架,那么这一步可以跳过。如果您使用的是 RNSurface 启动的 RN 框架,您就需要创建一个 RNComponentContext 并创建 LogBox 的构造器,并在 context 的 devToolsController 中增加如下的事件监听,并进行对应的启动和关闭 LogBox 弹窗的操作:

this.logBoxDialogController = new CustomDialogController({
  cornerRadius: 0,
  customStyle: true,
  alignment: DialogAlignment.TopStart,
  backgroundColor: Color.Transparent,
  builder: LogBoxDialog({
    ctx: RNComponentContext,
    rnInstance: this.rnInstance,
    initialProps: this.initialProps,
    buildCustomComponent: this.buildCustomComponent,
  })
})
···
this.rnInstance.getTurboModule<LogBoxTurboModule>(LogBoxTurboModule.NAME).eventEmitter.subscribe("SHOW", () => {
  this.logBoxDialogController.open();
})
this.rnInstance.getTurboModule<LogBoxTurboModule>(LogBoxTurboModule.NAME).eventEmitter.subscribe("HIDE", () => {
  this.logBoxDialogController.close();
})

控制台日志

DevEco Studio

当js侧发生错误时,通过以下方法可在 DevEco Studio 上查看到错误信息:

  1. 手机连接到电脑,打开 DevEco Studio,打开一个项目;

  2. 在 DevEco Studio 下方点击 Log;

  3. 在 HiLog 的筛选栏中一次选择你连接的设备、All logs of selected app、[应用包名]、Warn/Error;

  4. 设置完后,js 侧发生故障时,故障信息便能实时的显示在 DevEco Studio,如图:

    DevEco-LOG

命令行工具

若是通过Metro服务加载bundle,当js侧发生错误时,命令行工具便会输出错误发生的原因和具体位置,如图:

metro-cmd

其他

在 React Native 中亦可通过 console.log、console.warn、throw 等触发一个日志或告警。

添加Trace

JS侧

可通过引入 react-native 提供的 Systrace 添加 Trace。

例如添加一个名为 ONPRESS 的同步 Trace,以跟踪一个按钮的 onPress:

import {Systrace} from 'react-native';
...
<Button
  onPress={() => {
    Systrace.beginEvent('ONPRESS');
    // do something
    Systrace.endEvent();
  }}
/>

例如添加一个名为 ASYNC 的异步 Trace,通过两个按钮控制开始和结束:

import {Systrace} from 'react-native';
...
let traceCookie = null;
...
<Button
  onPress={() => {
    if (traceCookie === null) {
      traceCookie =
      Systrace.beginAsyncEvent('ASYNC');
    }
  }}
/>
<Button
  onPress={() => {
    if (traceCookie !== null) {
      Systrace.endAsyncEvent('ASYNC', traceCookie);
    }
  }}
/>
/>

ArkTS侧

ArkTS 侧大多数类和对象的成员中存在一个 logger 或者 ctx 对象(ctx 对象内涵一个 logger,logger 内提供了封装的 startTracing() 用于添加 Trace)。

例如添加一个名为 myTrace 的 Trace:

const stopTracing = this.logger.clone("myTrace").startTracing();
// do something
stopTracing();

也可以直接使用未封装的 @ohos.hiTraceMeter 能力添加 Trace。

例如添加一个名为 myTrace 的 Trace,使用 0 作为识别标志(用于区分同名 Trace):

import hiTrace from '@ohos.hiTraceMeter';
...
hiTrace.startTrace(`myTrace`, 0)
// do something
hiTrace.finishTrace(`myTrace`, 0)

C++侧

可通过引入 react/renderer/debug/SystraceSection.h ,实例化一个 facebook::react::SystraceSection 对象添加 Trace。

Trace的范围与该 facebook::react::SystraceSection 对象的生命周期(作用域)相同,可以使用大括号控制范围。

例如添加一个名为 myTrace 的 Trace:

#include <react/renderer/debug/SystraceSection.h>
...
{
  facebook::react::SystraceSection s("myTrace");
  // do something
}

通过监听 RNOHErrors 捕获 Bundle 内错误信息

需要注意的是:该方法仅在打包 bundle 时关闭 dev 选项,即进行 product 模式打包时,才能对 bundle 内错误进行监听:

"npm run codegen && react-native bundle-harmony --dev=false"

可通过 RNOHCoreContext 获取上下文对象,使用 subscribeToRNOHError 进行监听。
如以下例子所示,可在 EntryAbility.ets 的 onCreate 对 bundle 内的错误进行监听。

import { RNAbility, RNInstanceError, RNOHCoreContext } from '@rnoh/react-native-openharmony';
import Want from '@ohos.app.ability.Want';
import {JSON} from '@kit.ArkTS';

export default class EntryAbility extends RNAbility {
  getPagePath() {
    return 'pages/Index';
  }

  override onCreate(want: Want): void {
    super.onCreate(want);
    AppStorage.get<RNOHCoreContext>('RNOHCoreContext')!.subscribeToRNOHErrors((err) => {
      console.log('HERE: error logged from entryability! ' + JSON.stringify(err))
      console.log('HERE: err.getMessage: ' + err.getMessage())
      console.log('HERE: err.getSuggestions:' + JSON.stringify(err.getSuggestions()), null, 2)
      console.log('HERE: err.getExtraData: ' + JSON.stringify(err.getExtraData()), null, 2)
      console.log('HERE: err.getStack: ' + JSON.stringify(err.getStack(), null, 2))
      //Not all errors are RNInstanceError's, so we need to check for that.
      if (err instanceof RNInstanceError) {
        console.log('HERE: err.getRNInstanceId: ' + err.getRNInstanceId())
        console.log('HERE: err.getRNInstanceName: ' + err.getRNInstanceName())
      }
    })
  }
}

在本例子中,主要是对捕获的 error 进行打印,因此在其中添加了 console.log(),在终端中打印出具体的 error 信息:
例如,当需要 error 中的 Message 信息时,直接调用 err.getMessage() 即可。

console.log('HERE: err.getMessage: ' + err.getMessage())

同时,也支持对 error 错误中的 RN 实例信息的补充,例如:

if (err instanceof RNInstanceError) 
{
    console.log('HERE: err.getRNInstanceId: ' + err.getRNInstanceId())
    console.log('HERE: err.getRNInstanceName: ' + err.getRNInstanceName())
}

当捕获的 error 属于 RN 实例的 error 时,就打印出 RN 实例错误相关的信息。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐