4
首发于公众号 前端混合开发,欢迎关注。

作为一个跨平台移动应用开发框架,React Native 需要与平台特定的编程语言(如 Android 的 Java 和 iOS 的 Objective-C)进行通信。这可以通过两种方式之一实现,这取决于你用于构建混合移动应用的架构。

在 React Native 的原始或传统架构中,这种通信过程是通过所谓的桥接来实现的。与此同时,较新的、更具实验性的架构使用 JavaScript 接口(JSI)来直接调用在 Java 或 Objective-C 中实现的方法。

让我们从高层次来看看每个选项是如何工作的,然后探索使用React Native JSI来提高我们应用的速度和性能。你可以在这个GitHub仓库中查看本文中使用的代码演示。

需要注意的是,这是一个相对高级的话题,因此需要一些 React Native 编程经验才能跟上。此外,至少对 Java 和 Objective-C 基础有一定的了解将帮助你从本教程中获得最大收益。

React Native的原始架构是如何工作的?

在 React Native 的传统架构中,一个应用被划分为三个不同的部分或线程:

  • JavaScript线程:我们在这里编写JavaScript代码和业务逻辑,以及创建节点
  • 阴影树:React Native 使用 Yoga 框架进行布局计算的地方。
  • 平台 UI(Java 或 Objective-C):渲染这个计算出的布局。

本质上,线程是进程中一系列可执行的指令。线程通常由操作系统的一个部分(称为调度器)处理,它包含有关何时以及如何执行线程的指令。

为了实现通信和协同工作,这三个线程依赖于一种被称为桥接的机制。这种操作模式始终是异步的,并确保使用 React Native 构建的应用总是使用平台特定视图而不是网页视图进行渲染。

由于这种架构,JavaScript 和平台 UI 线程不会直接通信。因此,原生方法不能直接在 JavaScript 线程中调用。

通常,为了开发一个能在 iOS 或 Android 上运行的应用,我们期望使用该平台的特定编程语言。这就是为什么新创建的 React Native 应用会有单独的 iosandroid 文件夹,它们作为引导我们的应用在各自平台上运行的入口点。

因此,为了在每个平台上运行我们的 JavaScript代码,我们依赖于一个名为 JavaScriptCore 的框架。这意味着当我们启动一个React Native应用时,我们必须同时启动三个线程,同时允许桥接器处理通信。

image.png

React Native 中的桥接是什么?

桥接,用 C++ 编写,是我们如何在 JavaScript 和平台 UI 线程之间发送编码消息的方式,这些消息格式化为 JSON 字符串:

  • JavaScript 线程决定哪个节点在屏幕上渲染,并使用桥接以序列化的 JSON 格式传递消息。
  • 在消息到达主线程之前,它需要到达阴影树线程,该线程计算屏幕上节点的位置。
  • 然而,主线程是处理发生在 UI 上的动作的,比如在文本字段中输入或按下一个按钮。

因此,每个线程都需要花费一些时间来解码 JSON 字符串。有趣的是,桥接还为 Java 或 Objective-C 提供了一个接口,用于从原生模块调度 JavaScript 执行。它异步完成所有这些操作。这些时间加起来可不少!

尽管如此,多年来桥接通常运行良好,为许多应用程序提供动力。然而,它也遇到了一些其他问题。例如:

  • 桥接与 React Native 的生命周期紧密相关,通常会在 React Native 初始化或关闭时一起初始化或关闭,这意味着启动时间更慢。
  • 如果在用户与用户界面交互时,线程间的通信过程出现某种阻塞——例如,滚动浏览一长串数据时,他们可能会瞬间看到一个白色空白区域,从而导致用户体验不佳
  • 应用中将要使用的每个模块都需要在启动时初始化,这可能导致启动时间变慢,以及系统资源使用增加、可扩展性问题等等。

React Native团队在努力减少由桥接引起的性能瓶颈的过程中,引入了新的架构。让我们来探索如何做到这一点。

React Native的新架构是如何工作的?

React Native的新架构相比于经典架构,更加便于JavaScript和平台UI线程之间的直接通信。这意味着可以直接在JavaScript线程中调用原生模块。

新架构中的一些其他差异包括:

  • 能够与多个通用引擎(如Hermes或V8)一起工作,而不仅仅依赖于JavaScriptCore引擎
  • 无需在JavaScript和平台UI线程之间序列化或反序列化消息。相反,它使用一种称为JavaScript接口或JSI的机制,我们将在下面详细讨论。
  • 使用Fabric而不是Yoga来渲染UI组件
  • 引入了 TurboModules,确保应用中使用的每个模块只在需要时加载,而不是在启动时加载。

由于这些新实现,使用新架构的 React Native 应用将记录性能提升——包括更快的启动时间。

什么是JavaScript接口(JSI)?

JavaScript接口(JSI)修复了桥接的两个关键缺陷:

  • 允许我们直接在JavaScript中调用在Java或Objective-C中创建的原生方法
  • 使我们能够与本地代码进行同步或异步通信,这提高了启动时间,并在像滚动长列表这样的场景中实现更快的响应

除了这些巨大的优势之外,我们还可以使用 JSI 来利用设备的连接功能,例如蓝牙和地理定位,通过暴露我们可以直接用 JavaScript 调用的方法。

调用平台原生模块中的方法的能力并不是全新的——我们在网页开发中使用相同的模式。例如,在JavaScript中,我们可以像这样调用DOM方法:

const paragraph = document.createElement('p')

我们甚至可以在创建的DOM上调用方法。例如,这段代码在C++中调用了一个 setHeight 方法,它改变了我们创建的元素的高度:

paragraph.setAttribute('height', 55)

如我们所见,用C++编写的JSI为我们的应用程序带来了许多性能上的改进。接下来,让我们探索如何利用TurboModules和Codegen充分发挥其潜力。

理解 Codegen 和 TurboModules

TurboModules 是新的 React Native 架构中的一种特殊的原生模块。他们的一些优点包括:

  • 仅在需要时初始化模块,以实现更快的应用启动时间
  • 使用JSI进行本地代码,这意味着平台UI和JavaScript线程之间的通信更加顺畅
  • 在原生平台上提供强类型接口

与此同时,Codegen 就像我们的 TurboModules 的静态类型检查器和生成器。本质上,当我们使用 TypeScript 或 Flow 定义我们的类型时,我们可以使用Codegen为 JSI 生成C++类型。Codegen 还为我们的模块生成更多的本地代码。

通常,使用 Codegen 和 TurboModules 使我们能够使用JSI构建可以与Java和 Objective-C 等平台特定代码进行通信的模块。这是享受JSI优势的推荐方式。

image.png

既然我们已经从高层次上介绍了这些信息,现在让我们将其付诸实践。在下一节中,我们将创建一个 TurboModule,它将允许我们在Java或 Objective-C 中访问方法。

使用新架构启动一个 React Native 应用

为了创建一个启用了新架构的新 React Native 应用,我们首先需要设置我们的文件夹。让我们开始创建一个文件夹——我们将命名为 JSISample——在这里我们将添加我们的 React Native 应用、设备名称模块和单位转换器模块。

对于下一步,我们可以按照实验性的React Native文档中的设置指南进行操作,或者简单地打开一个新的终端并运行以下命令:

// Terminal
npx react-native@latest init Demo

上述命令将创建一个新的React Native应用,其文件夹名称为 Demo

一旦成功安装,我们就可以启用新的架构。要在Android上启用它,只需打开 Demo/android/gradle.properties 文件并设置 newArchEnabled=true 。要在iOS上启用它,打开终端到 Demo/ios ,然后运行此命令:

bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

现在应该启用新的React Native架构。为了确认这一点,你可以通过运行 npm startyarn start 来启动你的iOS或Android应用。在终端中,你应该会看到以下内容:

LOG  Running "Demo" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}

在我们的应用程序设置完成后,让我们继续创建两个 TurboModules:一个用于获取设备的名称,另一个用于单位转换。

创建一个设备名称 TurboModule

为了探索JSI的重要性,我们将创建一个全新的TurboModule,我们可以将其安装到启用了新架构的React Native应用程序中。

设置文件夹结构

JSISample 文件夹中,我们需要创建一个新的文件夹,前缀为 RTN ,例如 RTNDeviceName 。在这个文件夹内,我们将创建三个额外的文件夹: iosandroidjs 。我们还将在文件夹旁边添加两个文件: package.jsonrtn-device-name.podspec

目前,我们的文件夹结构应该是这样的:

// Folder structure
RTNDeviceName
 ┣ android
 ┣ ios
 ┣ js
 ┣ package.json
 ┗ rtn-device-name.podspec

设置 package.json 文件

作为一个React Native开发者,你肯定之前已经处理过 package.json 文件。在新的React Native架构的背景下,这个文件既管理我们模块的JavaScript代码,也与我们稍后设置的平台特定代码进行接口对接。

在 package.json 文件中,粘贴这段代码:

// RTNDeviceName/package.json

{
  "name": "rtn-device-name",
  "version": "0.0.1",
  "description": "Convert units",
  "react-native": "js/index",
  "source": "js/index",
  "files": [
    "js",
    "android",
    "ios",
    "rtn-device-name.podspec",
    "!android/build",
    "!ios/build",
    "!**/__tests__",
    "!**/__fixtures__",
    "!**/__mocks__"
  ],
  "keywords": [
    "react-native",
    "ios",
    "android"
  ],
  "repository": "https://github.com/bonarhyme/rtn-device-name",
  "author": "Onuorah Bonaventure Chukwudi (https://github.com/bonarhyme)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/bonarhyme/rtn-device-name/issues"
  },
  "homepage": "https://github.com/bonarhyme/rtn-device-name#readme",
  "devDependencies": {},
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "codegenConfig": {
    "name": "RTNDeviceNameSpec",
    "type": "modules",
    "jsSrcsDir": "js",
    "android": {
      "javaPackageName": "com.rtndevicename"
    }
  }
}

我在上述文件中提供了我的详细信息。你的文件应该看起来稍有不同,因为你应该使用你自己的个人信息,仓库和模块的详细信息。

设置 podspec 文件

我们接下来要处理的文件是 podspec 文件,这是专门为我们的演示应用程序的iOS实现准备的。本质上, podspec 文件定义了我们正在设置的模块如何与iOS构建系统以及 CocoaPods(iOS应用程序的依赖管理器)进行交互。

你应该会看到很多与我们在上一节中设置的 package.json 文件相似之处,因为我们使用该文件中的值来填充这个文件中的许多字段。链接这两个文件确保了我们的 JavaScript 和原生 iOS 代码之间的一致性。

podspec 文件的内容应该看起来类似于这样:

// RTNDeviceName/rtn-device-name.podspec

require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
  s.name            = "rtn-device-name"
  s.version         = package["version"]
  s.summary         = package["description"]
  s.description     = package["description"]
  s.homepage        = package["homepage"]
  s.license         = package["license"]
  s.platforms       = { :ios => "11.0" }
  s.author          = package["author"]
  s.source          = { :git => package["repository"], :tag => "#{s.version}" }
  s.source_files    = "ios/**/*.{h,m,mm,swift}"
  install_modules_dependencies(s)
end

为Codegen定义TypeScript接口

接下来在我们的待办事项列表上是定义Codegen的TypeScript接口。在为这一步骤设置文件时,我们必须始终使用以下命名规则:

  • 以 Native 开始文件名
  • 在 Native 后面跟上我们的模块名称,使用PascalCase命名法

遵循这种命名规则对于使React Native JSI正确工作至关重要。在我们的情况下,我们将创建一个名为 NativeDeviceName.ts 的新文件,并在其中编写以下代码:

// RTNDeviceName/js/NativeDeviceName.ts

import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getDeviceName(): Promise<string>;
}
export default TurboModuleRegistry.get<Spec>('RTNDeviceName') as Spec | null;

这个TypeScript文件包含了我们将在此模块中实现的方法的接口。我们首先导入必要的React Native依赖项。然后,我们定义了一个 Spec 接口,它扩展了一个 TurboModule

在我们的 Spec 接口中,我们定义了我们想要创建的方法——在这种情况下,是 getDeviceName() 。最后,我们从 TurboModuleRegistry 调用并导出模块,指定我们的 Spec 接口和我们的模块名称。

生成原生iOS代码

在这个步骤中,我们将使用Codegen生成用 Objective-C 编写的原生iOS代码。我们只需确保我们的终端在 RTNDeviceName 文件夹中打开,并粘贴以下代码:

// Terminal

node Demo/node_modules/react-native/scripts/generate-codegen-artifacts.js \
  --path Demo/ \
  --outputPath RTNDeviceName/generated/

这个命令在我们的 RTNDeviceName 模块文件夹内生成一个名为 generated 的文件夹。 generated 文件夹包含我们模块的原生iOS代码。文件夹结构应该看起来类似于这样:

// RTNDeviceName/generated/

generated
 ┗ build
 ┃ ┗ generated
 ┃ ┃ ┗ ios
 ┃ ┃ ┃ ┣ FBReactNativeSpec
 ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec-generated.mm
 ┃ ┃ ┃ ┃ ┗ FBReactNativeSpec.h
 ┃ ┃ ┃ ┣ RTNConverterSpec
 ┃ ┃ ┃ ┃ ┣ RTNConverterSpec-generated.mm
 ┃ ┃ ┃ ┃ ┗ RTNConverterSpec.h
 ┃ ┃ ┃ ┣ FBReactNativeSpecJSI-generated.cpp
 ┃ ┃ ┃ ┣ FBReactNativeSpecJSI.h
 ┃ ┃ ┃ ┣ RTNConverterSpecJSI-generated.cpp
 ┃ ┃ ┃ ┗ RTNConverterSpecJSI.h

实现iOS的模块方法

在这个步骤中,我们需要用Objective-C编写一些iOS原生代码。首先,我们需要在 RTNDeviceName/ios 文件夹中创建两个文件: RTNDeviceName.h RTNDeviceName.mm

第一个文件是一个头文件,这种文件用于存储可以导入到Objective C文件中的函数。因此,我们需要将这段代码添加到其中:

// RTNDevicename/ios/RTNDeviceName.h

#import <RTNDeviceNameSpec/RTNDeviceNameSpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RTNDeviceName : NSObject <NativeDeviceNameSpec>

@end

NS_ASSUME_NONNULL_END

第二个文件是一个实现文件,它包含我们模块的实际原生代码。添加以下代码:

// RTNDevicename/ios/RTNDeviceName.mm

#import "RTNDeviceNameSpec.h"
#import "RTNDeviceName.h"

#import <UIKit/UIKit.h>

@implementation RTNDeviceName
RCT_EXPORT_MODULE()

- (void)getDeviceName:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSString *deviceName = [UIDevice currentDevice].name;
    resolve(deviceName);
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeDeviceNameSpecJSI>(params);
}

@end

在这个文件中,我们导入了必要的头文件,包括我们刚刚创建的 RTNDeviceName.h 和UIKit,我们将使用它来提取设备的名称。然后我们编写实际的Objective-C函数来提取并返回设备名称,然后在其下方编写必要的样板代码。

请注意,这段样板代码是由React Native团队创建的。它帮助我们实现JSI,因为编写纯JSI意味着编写纯粹的、未经掺杂的原生代码。

生成安卓代码

生成原生 Android 代码的第一步是在 RTNDeviceName/android 文件夹内创建一个 build.gradle 文件。然后,将以下代码添加到其中:

// RTNDeviceName/android/build.gradle

buildscript {
  ext.safeExtGet = {prop, fallback ->
    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
  }
  repositories {
    google()
    gradlePluginPortal()
  }
  dependencies {
    classpath("com.android.tools.build:gradle:7.3.1")
  }
}
apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
android {
  compileSdkVersion safeExtGet('compileSdkVersion', 33)
  namespace "com.rtndevicename"
}
repositories {
  mavenCentral()
  google()
}
dependencies {
  implementation 'com.facebook.react:react-native'
}

接下来,我们将创建我们的 ReactPackage 类。在这个深层嵌套的文件夹中创建一个新的 DeviceNamePackage.java 文件。

RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNamePackage.java

在我们刚刚创建的 DeviceNamePackage.java 文件中,添加以下代码,它在Java中将相关类进行分组:

// RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNamePackage.java

package com.rtndevicename;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
public class DeviceNamePackage extends TurboReactPackage {
  @Nullable
  @Override
  public NativeModule getModule(String name, ReactApplicationContext reactContext) {
     if (name.equals(DeviceNameModule.NAME)) {
         return new DeviceNameModule(reactContext);
     } else {
          return null;
     }
  }

  @Override
  public ReactModuleInfoProvider getReactModuleInfoProvider() {
     return () -> {
         final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
         moduleInfos.put(
                 DeviceNameModule.NAME,
                 new ReactModuleInfo(
                         DeviceNameModule.NAME,
                         DeviceNameModule.NAME,
                         false, // canOverrideExistingModule
                         false, // needsEagerInit
                         true, // hasConstants
                         false, // isCxxModule
                         true // isTurboModule
         ));
         return moduleInfos;
     };
  }
}

我们还将创建一个名为 DeviceNameModule.java 的文件,该文件将包含我们在Android上的设备名称模块的实际实现。它使用 getDeviceName 方法获取设备名称。文件的其余部分包含了代码与JSI正确交互所需的样板代码。

这是该文件的完整代码:

// RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNameModule.java

package com.rtndevicename;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
import com.rtndevicename.NativeDeviceNameSpec;

import android.os.Build;

public class DeviceNameModule extends NativeDeviceNameSpec {
    public static String NAME = "RTNDeviceName";
    DeviceNameModule(ReactApplicationContext context) {
        super(context);
    }
    @Override
    @NonNull
    public String getName() {
        return NAME;
    }
    @Override
    public void getDeviceName(Promise promise) {
        promise.resolve(Build.MODEL);
    }
}

遵循这些步骤后,我们可以通过打开终端并运行此命令来将我们的模块重新安装到我们的 Demo 应用中:

cd Demo
yarn add ../RTNDeviceName

此外,我们需要调用 Codegen 来为我们的 Demo 应用生成我们的 Android 代码,如下所示:

cd android
./gradlew generateCodegenArtifactsFromSchema

这完成了创建TurboModule的过程。我们的文件结构现在应该是这样的:

// Turbo module supposed structure

RTNDeviceName
 ┣ android
 ┃ ┣ src
 ┃ ┃ ┗ main
 ┃ ┃ ┃ ┗ java
 ┃ ┃ ┃ ┃ ┗ com
 ┃ ┃ ┃ ┃ ┃ ┗ rtndevicename
 ┃ ┃ ┃ ┃ ┃ ┃ ┣ DeviceNameModule.java
 ┃ ┃ ┃ ┃ ┃ ┃ ┗ DeviceNamePackage.java
 ┃ ┗ build.gradle
 ┣ generated
 ┃ ┗ build
 ┃ ┃ ┗ generated
 ┃ ┃ ┃ ┗ ios
 ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec
 ┃ ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec-generated.mm
 ┃ ┃ ┃ ┃ ┃ ┗ FBReactNativeSpec.h
 ┃ ┃ ┃ ┃ ┣ RTNConverterSpec
 ┃ ┃ ┃ ┃ ┃ ┣ RTNConverterSpec-generated.mm
 ┃ ┃ ┃ ┃ ┃ ┗ RTNConverterSpec.h
 ┃ ┃ ┃ ┃ ┣ FBReactNativeSpecJSI-generated.cpp
 ┃ ┃ ┃ ┃ ┣ FBReactNativeSpecJSI.h
 ┃ ┃ ┃ ┃ ┣ RTNConverterSpecJSI-generated.cpp
 ┃ ┃ ┃ ┃ ┗ RTNConverterSpecJSI.h
 ┣ ios
 ┃ ┣ RTNDeviceName.h
 ┃ ┗ RTNDeviceName.mm
 ┣ js
 ┃ ┗ NativeDeviceName.ts
 ┣ package.json
 ┗ rtn-device-name.podspec

然而,要使用我们刚刚创建的 RTNDeviceName 模块,我们必须设置我们的React Native代码。接下来让我们看看如何做。

为我们的React Native代码创建 DeviceName.tsx 文件

打开 src 文件夹并创建一个 component 文件夹。在 component 文件夹内,创建一个 DeviceName.tsx 文件并添加以下代码:

// Demo/src/components/DeviceName.tsx

import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import React, {useCallback, useState} from 'react';

import RTNDeviceName from 'rtn-device-name/js/NativeDeviceName';

我们首先从React Native中导入了几个组件和 StyleSheet 模块。我们还导入了React库和两个Hooks。最后一个 import 语句是为了我们之前设置的 RTNDeviceName 模块,以便我们在应用中访问原生设备名称获取功能。

现在,让我们分解我们想要添加到文件中的其余代码。首先,我们定义一个 DeviceName 功能组件。在我们的组件内部,我们创建一个状态来保存我们最终获取的设备名称:

export const DeviceName = () => {
  const [deviceName, setDeviceName] = useState<string | undefined>('');
// next code below here
}

接下来,我们添加一个异步回调。在其中,我们等待库的响应,并最终将其设置到状态中:

  const getDeviceName = useCallback(async () => {
    const theDeviceName = await RTNDeviceName?.getDeviceName();
    setDeviceName(theDeviceName);
  }, []);

// Next piece of code below here

return 声明中,我们使用了一个 TouchableOpacity 组件,该组件在按下时调用 getDeviceName 回调。我们还有一个 Text 组件来渲染 deviceName 状态:

  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={getDeviceName} style={styles.button}>
        <Text style={styles.buttonText}>Get Device Name</Text>
      </TouchableOpacity>
      <Text style={styles.deviceName}>{deviceName}</Text>
    </View>
  );

在文件的末尾,我们使用之前导入的 StyleSheet 模块来定义我们组件的样式:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingHorizontal: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  button: {
    justifyContent: 'center',
    paddingHorizontal: 10,
    paddingVertical: 5,
    borderRadius: 10,
    backgroundColor: '#007bff',
  },
  buttonText: {
    fontSize: 20,
    color: 'white',
  },
  deviceName: {
    fontSize: 20,
    marginTop: 10,
  },
});

在此刻,我们可以调用我们的组件并在我们的 src/App.tsx 文件中像这样渲染它:

// Demo/App.tsx

import React from 'react';
import {SafeAreaView, StatusBar, useColorScheme} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import {DeviceName} from './components/DeviceName';

function App(): JSX.Element {
  const isDarkMode = useColorScheme() === 'dark';

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
    flex: 1,
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar
        barStyle={isDarkMode ? 'light-content' : 'dark-content'}
        backgroundColor={backgroundStyle.backgroundColor}
      />
      <DeviceName />
    </SafeAreaView>
  );
}
export default App;

最后,我们可以使用 yarn start 或 npm start 运行我们的应用程序,并按照提示在iOS或Android上启动它。我们的应用程序应该看起来类似于下面的样子,具体取决于你在哪种设备上启动它:

image.png

就是这样!我们已经成功创建了一个TurboModule,它允许我们在React Native应用中获取用户的设备名称。

创建一个单位转换器TurboModule

我们已经看到了如何直接从JavaScript应用程序访问设备的信息有多么简单。现在,我们将构建另一个模块,允许我们转换测量单位。我们将遵循类似的设置步骤,所以我们不会过多地详述代码。

设置我们的文件夹和文件

与之前一样,我们将首先创建一个新的TurboModule文件夹,与 Demo 和 RTNDeviceName 文件夹并列。这次,我们将这个文件夹命名为 RTNConverter

然后,创建 jsiosandroid 文件夹,以及 package.jsonrtn-converter.podspec 文件。我们的文件夹结构应该看起来类似于 RTNDeviceName 的初始文件夹结构。

接下来,像这样向 package.json 添加适当的代码:

// RTNConverter/package.json

{
  "name": "rtn-converter",
  "version": "0.0.1",
  "description": "Convert units",
  "react-native": "js/index",
  "source": "js/index",
  "files": [
    "js",
    "android",
    "ios",
    "rtn-converter.podspec",
    "!android/build",
    "!ios/build",
    "!**/__tests__",
    "!**/__fixtures__",
    "!**/__mocks__"
  ],
  "keywords": [
    "react-native",
    "ios",
    "android"
  ],
  "repository": "https://github.com/bonarhyme/rtn-converter",
  "author": "Onuorah Bonaventure Chukwudi (https://github.com/bonarhyme)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/bonarhyme/rtn-converter/issues"
  },
  "homepage": "https://github.com/bonarhyme/rtn-converter#readme",
  "devDependencies": {},
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "codegenConfig": {
    "name": "RTNConverterSpec",
    "type": "modules",
    "jsSrcsDir": "js",
    "android": {
      "javaPackageName": "com.rtnconverter"
    }
  }
}

记得用你自己的详细信息填充字段。然后,添加 podspec 文件的内容:

// RTNConverter/podspec

require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
  s.name            = "rtn-converter"
  s.version         = package["version"]
  s.summary         = package["description"]
  s.description     = package["description"]
  s.homepage        = package["homepage"]
  s.license         = package["license"]
  s.platforms       = { :ios => "11.0" }
  s.author          = package["author"]
  s.source          = { :git => package["repository"], :tag => "#{s.version}" }
  s.source_files    = "ios/**/*.{h,m,mm,swift}"
  install_modules_dependencies(s)
end

定义我们的 TypeScript 接口和单位转换方法

现在,我们将通过在 RTNConverter/js 文件夹中创建一个 NativeConverter.ts 文件来定义我们的单位转换器的TypeScript接口,并添加以下代码:

// RTNConverter/js/NativeConverter.ts

import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  inchesToCentimeters(inches: number): Promise<number>;
  centimetersToInches(centimeters: number): Promise<number>;
  inchesToFeet(inches: number): Promise<number>;
  feetToInches(feet: number): Promise<number>;
  kilometersToMiles(kilometers: number): Promise<number>;
  milesToKilometers(miles: number): Promise<number>;
  feetToCentimeters(feet: number): Promise<number>;
  centimetersToFeet(centimeters: number): Promise<number>;
  yardsToMeters(yards: number): Promise<number>;
  metersToYards(meters: number): Promise<number>;
  milesToYards(miles: number): Promise<number>;
  yardsToMiles(yards: number): Promise<number>;
  feetToMeters(feet: number): Promise<number>;
  metersToFeet(meters: number): Promise<number>;
}

export default TurboModuleRegistry.get<Spec>('RTNConverter') as Spec | null;

如你所见,它包含了我们将要原生实现的方法。这与我们为 RTNDeviceName 模块所做的非常相似。然而,我们定义了几种方法来转换不同的测量单位,而不是 getDeviceName() 方法。

生成平台特定代码

我们将使用与之前类似的终端命令来生成iOS代码。打开你的终端,在我们的 JSISample 文件夹的根目录中运行命令,如下所示:

node Demo/node_modules/react-native/scripts/generate-codegen-artifacts.js \
  --path Demo/ \
  --outputPath RTNConverter/generated/

记住,这段代码使用Codegen在我们的 RTNConverter 文件夹内生成一个iOS构建。

接下来,我们将在 RTNConverter/ios 文件夹内为我们的单位转换模块创建头文件和实现文件—— RTNConverter.h RTNConverter.mm 。在头文件中,添加以下代码:

// RTNConverter/ios/RTNConverter.h

#import <RTNConverterSpec/RTNConverterSpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RTNConverter : NSObject <NativeConverterSpec>

@end

NS_ASSUME_NONNULL_END

然后,将实际的Objective-C代码添加到 RTNConverter.mm 文件中:

// RTNConverter/ios/RTNConverter.mm

#import "RTNConverterSpec.h"
#import "RTNConverter.h"

@implementation RTNConverter

RCT_EXPORT_MODULE()

- (void)inchesToCentimeters:(double)inches resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:inches*2.54];
    resolve(result);
}
- (void)centimetersToInches:(double)centimeters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:centimeters/2.54];
    resolve(result);
}
- (void)inchesToFeet:(double)inches resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:inches/12];
    resolve(result);
}
- (void)feetToInches:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:feet*12];
    resolve(result);
}
- (void)kilometersToMiles:(double)kilometers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:kilometers/1.609];
    resolve(result);
}
- (void)milesToKilometers:(double)miles resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:miles*1.609];
    resolve(result);
}
- (void)feetToCentimeters:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:feet*30.48];
    resolve(result);
}
- (void)centimetersToFeet:(double)centimeters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:centimeters/30.48];
    resolve(result);
}
- (void)yardsToMeters:(double)yards resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:yards/1.094];
    resolve(result);
}
- (void)metersToYards:(double)meters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:meters*1.094];
    resolve(result);
}
- (void)milesToYards:(double)miles resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:miles*1760];
    resolve(result);
}
- (void)yardsToMiles:(double)yards resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:yards/1760];
    resolve(result);
}
- (void)feetToMeters:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:feet/3.281];
    resolve(result);
}
- (void)metersToFeet:(double)meters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:meters*3.281];
    resolve(result);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeConverterSpecJSI>(params);
}
@end

我们的设备名称 TurboModule 的实现文件中包含了一个提取并返回设备名称的函数。这次,它包含了计算并返回转换后测量值的函数。

现在我们已经有了iOS的代码,是时候设置Android的代码了。与之前类似,我们将开始在 RTNConverter/android/build.gradle 中添加 build.gradle 文件。

// RTNConverter/android/build.gradle

buildscript {
  ext.safeExtGet = {prop, fallback ->
    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
  }
  repositories {
    google()
    gradlePluginPortal()
  }
  dependencies {
    classpath("com.android.tools.build:gradle:7.3.1")
  }
}
apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
android {
  compileSdkVersion safeExtGet('compileSdkVersion', 33)
  namespace "com.rtnconverter"
}
repositories {
  mavenCentral()
  google()
}
dependencies {
  implementation 'com.facebook.react:react-native'
}

接下来,我们将在这个深层嵌套的文件夹中创建一个 ConverterPackage.java 文件,以添加 ReactPackage

RTNConverter/android/src/main/java/com/rtnconverter

将以下代码添加到此文件中:

// RTNConverter/android/src/main/java/com/rtnconverter/ConverterPackage.java

package com.rtnconverter;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

public class ConverterPackage extends TurboReactPackage {
  @Nullable
  @Override
  public NativeModule getModule(String name, ReactApplicationContext reactContext) {
      if (name.equals(ConverterModule.NAME)) {
          return new ConverterModule(reactContext);
      } else {
          return null;
      }
  }

  @Override
  public ReactModuleInfoProvider getReactModuleInfoProvider() {
      return () -> {
          final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
          moduleInfos.put(
                  ConverterModule.NAME,
                  new ReactModuleInfo(
                          ConverterModule.NAME,
                          ConverterModule.NAME,
                          false, // canOverrideExistingModule
                          false, // needsEagerInit
                          true, // hasConstants
                          false, // isCxxModule
                          true // isTurboModule
          ));
          return moduleInfos;
      };
  }
}

上述代码在Java中将类进行了分组。在我们的情况下,它将 RTNConverterModule.java 文件中的类进行了分组。

接下来,我们将通过创建一个 ConverterModule.java 文件来为Android添加我们的转换器模块的实际实现,该文件将位于上述文件旁边。然后,将此代码添加到新创建的文件中:

// RTNConverter/android/src/main/java/com/rtnconverter/ConverterPackage.java

package com.rtnconverter;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
import com.rtnconverter.NativeConverterSpec;

public class ConverterModule extends NativeConverterSpec {
    public static String NAME = "RTNConverter";
    ConverterModule(ReactApplicationContext context) {
        super(context);
    }
    @Override
    @NonNull
    public String getName() {
        return NAME;
    }

    @Override
    public void inchesToCentimeters(double inches, Promise promise) {
        promise.resolve(inches * 2.54);
    }
    @Override
    public void centimetersToInches(double centimeters, Promise promise) {
        promise.resolve(centimeters / 2.54);
    }
    @Override
    public void inchesToFeet(double inches, Promise promise) {
        promise.resolve(inches / 12);
    }
    @Override
    public void feetToInches(double feet, Promise promise) {
        promise.resolve(feet * 12);
    }
    @Override
    public void kilometersToMiles(double kilometers, Promise promise) {
        promise.resolve(kilometers / 1.609);
    }
    @Override
    public void milesToKilometers(double miles, Promise promise) {
        promise.resolve(miles * 1.609);
    }
    @Override
    public void feetToCentimeters(double feet, Promise promise) {
        promise.resolve(feet * 30.48);
    }
    @Override
    public void centimetersToFeet(double centimeters, Promise promise) {
        promise.resolve(centimeters / 30.48);
    }
    @Override
    public void yardsToMeters(double yards, Promise promise) {
        promise.resolve(yards / 1.094);
    }
    @Override
    public void metersToYards(double meters, Promise promise) {
        promise.resolve(meters * 1.094);
    }
    @Override
    public void milesToYards(double miles, Promise promise) {
        promise.resolve(miles * 1760);
    }
    @Override
    public void yardsToMiles(double yards, Promise promise) {
        promise.resolve(yards / 1760);
    }
    @Override
    public void feetToMeters(double feet, Promise promise) {
        promise.resolve(feet / 3.281);
    }
    @Override
    public void metersToFeet(double meters, Promise promise) {
        promise.resolve(meters * 3.281);
    }
}

现在我们已经为我们的模块添加了必要的原生Android代码,接下来我们将像这样将它添加到我们的React Native应用中:

// Terminal
cd Demo
yarn add ../RTNConverter

安装、故障排除和使用我们的模块

接下来,我们将通过运行以下命令为Android安装我们的模块:

// Terminal

cd android
./gradlew generateCodegenArtifactsFromSchema

然后,我们将通过以下操作为iOS安装我们的模块:

// Terminal

cd ios
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

如果你遇到任何错误,你可以清理构建并重新安装我们库的模块,如下所示:

cd ios
rm -rf build
bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

目前,我们的模块已经可以在我们的React Native项目中使用了。让我们打开 Demo/src/components 文件夹,创建一个 UnitConverter.tsx 文件,并将这段代码添加进去:

// Demo/src/components/UnitConverter.tsx

import {
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from 'react-native';
import React, {useCallback, useState} from 'react';
import RTNCalculator from 'rtn-converter/js/NativeConverter';
const unitCombinationsConvertions = [
  'inchesToCentimeters',
  'centimetersToInches',
  'inchesToFeet',
  'feetToInches',
  'kilometersToMiles',
  'milesToKilometers',
  'feetToCentimeters',
  'centimetersToFeet',
  'yardsToMeters',
  'metersToYards',
  'milesToYards',
  'yardsToMiles',
  'feetToMeters',
  'metersToFeet',
] as const;

type UnitCombinationsConvertionsType =
  (typeof unitCombinationsConvertions)[number];

export const UnitConverter = () => {

  const [value, setValue] = useState<string>('0');
  const [result, setResult] = useState<number | undefined>();
  const [unitCombination, setUnitCombination] = useState<
    UnitCombinationsConvertionsType | undefined
  >();

  const calculate = useCallback(
    async (combination: UnitCombinationsConvertionsType) => {
      const convertedValue = await RTNCalculator?.\[combination\](Number(value));
      setUnitCombination(combination);
      setResult(convertedValue);
    },
    [value],
  );
 const camelCaseToWords = useCallback((word: string | undefined) => {
    if (!word) {
      return null;
    }
    const splitCamelCase = word.replace(/([A-Z])/g, ' $1');
    return splitCamelCase.charAt(0).toUpperCase() + splitCamelCase.slice(1);
  }, []);

  return (
    <View>
      <ScrollView contentInsetAdjustmentBehavior="automatic">
        <View style={styles.container}>
          <Text style={styles.header}>JSI Unit Converter</Text>
          <View style={styles.computationContainer}>
            <View style={styles.calcContainer}>
              <TextInput
                value={value}
                onChangeText={e => setValue(e)}
                placeholder="Enter value"
                style={styles.textInput}
                inputMode="numeric"
              />
              <Text style={styles.equalSign}>=</Text>
              <Text style={styles.result}>{result}</Text>
            </View>
            <Text style={styles.unitCombination}>
              {camelCaseToWords(unitCombination)}
            </Text>
          </View>
          <View style={styles.combinationContainer}>
            {unitCombinationsConvertions.map(combination => (
              <TouchableOpacity
                key={combination}
                onPress={() => calculate(combination)}
                style={styles.combinationButton}>
                <Text style={styles.combinationButtonText}>
                  {camelCaseToWords(combination)}
                </Text>
              </TouchableOpacity>
            ))}
          </View>
        </View>
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingHorizontal: 10,
  },
  header: {
    fontSize: 24,
    marginVertical: 20,
    textAlign: 'center',
    fontWeight: '700',
  },
  computationContainer: {
    gap: 10,
    width: '90%',
    height: 100,
    marginTop: 10,
  },
  calcContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
    height: 50,
  },
  textInput: {
    borderWidth: 1,
    borderColor: 'gray',
    width: '50%',
    backgroundColor: 'lightgray',
    fontSize: 20,
    padding: 10,
  },
  equalSign: {
    fontSize: 30,
  },
  result: {
    width: '50%',
    height: 50,
    backgroundColor: 'gray',
    fontSize: 20,
    padding: 10,
    color: 'white',
  },
  unitCombination: {
    fontSize: 16,
  },
  combinationContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 10,
    justifyContent: 'center',
  },
  combinationButton: {
    backgroundColor: 'gray',
    width: '45%',
    height: 30,
    justifyContent: 'center',
    paddingHorizontal: 5,
  },
  combinationButtonText: {
    color: 'white',
  },
});

然后,我们可以在 App.tsx 文件中像这样导入并使用我们的组件:

// Demo/src/App.tsx

import React from 'react';
import {SafeAreaView, StatusBar, useColorScheme} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import {UnitConverter} from './components/UnitConverter';
// import {DeviceName} from './components/DeviceName';

function App(): JSX.Element {
  const isDarkMode = useColorScheme() === 'dark';
  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
    flex: 1,
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar
        barStyle={isDarkMode ? 'light-content' : 'dark-content'}
        backgroundColor={backgroundStyle.backgroundColor}
      />
      {/* <DeviceName /> */}
      <UnitConverter />
    </SafeAreaView>
  );
}

export default App;

最终的应用应该看起来类似于下面的样子:

image.png

总结

React Native JSI 仍然是一个实验性的功能。然而,从提升我们的 React Native 应用性能和开发体验的角度来看,它看起来非常有前途。它绝对值得一试!

在上述部分中,我们研究了经典的React Native架构中的桥接以及它与新架构的不同之处。我们还探讨了新架构以及它如何提升我们应用的速度和性能,涵盖了JSI,Fabric和TurboModules等概念。

为了更好地理解React Native JSI和新的架构,我们构建了一个模块,通过直接访问原生代码来检索和显示用户设备的名称。我们还构建了一个单位转换模块,允许我们调用我们在Java和Objective-C中定义的方法。


王大冶
68.1k 声望105k 粉丝