调试调测

Metro热加载

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

配置Metro

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

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

React Native 中,你的 Metro 配置应该扩展@react-native/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,并在 contextdevToolsController 中增加如下的事件监听,并删除原有的 RNInstance 实例,重新启动一个新的 RNInstance 实例,即可启用热加载的功能:

this.ctx.devToolsController.eventEmitter.subscribe("RELOAD", async () => {
  this.cleanUp();
  ths.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`),该方法接收3个参数:
      
      • ip - Metro 服务器的 IP 地址

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

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

        使用方法便是将 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地址和端口后重新打开应用即可。

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

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

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

使用 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 的构造器,并在 contextdevToolsController 中增加如下的事件监听,并进行对应的启动和关闭 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.logconsole.warnthrow 等触发一个日志或告警。

添加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.etsonCreate 对 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 实例错误相关的信息。