首页 / 文章 / 使用 React Native Codegen 实现应用内涡轮模块的端到端连接

使用 React Native Codegen 实现应用内涡轮模块的端到端连接

定义类型化规范,运行代码生成,然后在 iOS 和 Android 上实现 Turbo Module,支持同步、Promise、回调以及事件发射器等多种方法。

1409 词

React Native 的新架构用 Turbo Modules 替代了异步桥接机制,JavaScript 可通过 JSI 以类型化、自动生成的绑定方式调用它。这样一来,调用开销更低,且契约由代码生成工具而非传统规则来验证。本教程将在应用中构建一个模块(无需 npm 包),并介绍实际开发中会用到的三种调用方式:同步返回值、通过 Promise 或回调获取异步结果,以及事件流。

编写类型化规范

在项目根目录下创建一个 specs 文件夹,用于存放模块架构:

/your-app
  /specs

在其中添加 NativeShowCase.ts。Codegen 只会识别名称以 Native 开头的规范文件。下面的规范为每种样式定义了一个方法:haveCameraFlash 同步返回结果,toggleFlashLight 返回 Promise,fetchUser 需要传入回调函数,而 evenListenerCalled 是一个用于传递键值对的事件发射器。getEnforcing 在启动时会因缺少原生模块而抛出异常,从而及早发现连接错误。规范中引入的类型化事件发射器是较新的功能,因此请确认您的 React Native 版本支持 CodegenTypes.EventEmitter

import type { CodegenTypes, TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";

export type KeyValuePair = {
  key: string;
  value: string;
};

export interface Spec extends TurboModule {
  toggleFlashLight(): Promise<boolean>;
  haveCameraFlash(): boolean;
  fetchUser(
    id: string,
    callback: (user: { id: string; name: string }) => void
  ): void;
  readonly evenListenerCalled: CodegenTypes.EventEmitter<KeyValuePair>;
}

export default TurboModuleRegistry.getEnforcing<Spec>("NativeShowCase");

该文件是唯一的真实数据源;两个平台的绑定都是由此生成的。

配置与运行 codegen

package.json 中添加一个 codegenConfig 块。jsSrcsDir 必须与您的规格文件文件夹一致,而 javaPackageName 则决定 Android 类文件的生成位置:

"codegenConfig": {
  "name": "NativeShowCase",
  "type": "modules",
  "jsSrcsDir": "specs",
  "android": {
    "javaPackageName": "com.nativeshowcase"
  }
}

接着生成目标文件。在 Android 上,运行 Gradle 任务:

cd android && ./gradlew generateCodegenArtifactsFromSchema

在 iOS 上,代码生成是在安装 pods 的过程中完成的:

cd ios && pod install

现在您已经为两个平台生成了接口、头文件以及连接代码。

在 iOS 上实现该模块

在 Xcode 中打开 .xcworkspace 文件,创建一个名为 NativeShowCase 的组,然后添加一个名为 RCTNativeShowCase 的 Objective-C 类。将 .m 文件重命名为 .mm:Turbo Modules 需要 Objective-C++ 来与 C++ JSI 层进行交互。

该头部文件导入了生成的模块,继承了生成的基类,并采用了生成的协议:

#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN

@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>

@end

NS_ASSUME_NONNULL_END

实现部分会返回模块名称,将getTurboModule生成的JSI对象传递给React Native,同时实现每个规范方法。toggleFlashLight会处理Promise,fetchUser会调用回调函数,而haveCameraFlash则会立即返回结果,但会启动一个计时器,通过生成的emitEvenListenerCalled方法触发十次事件。

#import "RCTNativeShowCase.h"

@interface RCTNativeShowCase()
@property (nonatomic, strong) NSTimer *myTimer;
@property (nonatomic, assign) int count;
@end

@implementation RCTNativeShowCase

+ (NSString *)moduleName {
  return @"NativeShowCase";
}

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

- (void)toggleFlashLight:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
  resolve(@(false));
}

- (nonnull NSNumber *)haveCameraFlash {
  [self startTimer];
  return [NSNumber numberWithBool:true];
}

- (void)fetchUser:(nonnull NSString *)userId callback:(nonnull RCTResponseSenderBlock)callback {
  callback(@[@{@"key": userId, @"value": @"John"}]);
}


-(void)startTimer {
  if(self.myTimer) {
    [self stopTimer];
  }
  self.count = 0;
  self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(updateCount)
                                                userInfo:nil
                                                 repeats:YES];
}
- (void)updateCount {
  self.count += 1;
  [self emitEvenListenerCalled:@{@"key": @"count", @"value": @(_count)}];
  if (self.count == 10) {
    [self stopTimer];
  }
}
- (void)stopTimer {
  [self.myTimer invalidate];
  self.myTimer = nil;
}
@end

请注意,此iOS版本的fetchUser会发送key/value字段,而规范要求的是idname;在实际代码中需使两者保持一致,因为代码生成工具在运行时不会验证回调参数的格式。

在Android上实现该模块

创建在codegenConfig中声明的com.nativeshowcase包。该模块类继承自生成的NativeShowCaseSpec并重写了所有方法:它会切换一个标志并处理Promise,运行一个CountDownTimer,在每次计时时输出剩余秒数,同时为回调函数构建一个映射表。

package com.nativeshowcase

import android.os.CountDownTimer
import android.widget.Toast
import com.facebook.fbreact.specs.NativeShowCaseSpec
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext

class NativeShowCaseModule(reactContext: ReactApplicationContext) :
    NativeShowCaseSpec(reactContext) {
    var timer: CountDownTimer? = null
    var lightOn = false
    override fun toggleFlashLight(promise: Promise?) {
        Toast.makeText(reactApplicationContext, "Let's turn on flash light", Toast.LENGTH_LONG)
            .show()
        lightOn = !lightOn
        promise?.resolve(lightOn)
    }

    override fun haveCameraFlash(): Boolean {
        if (timer != null) {
            timer?.cancel()
            timer = null
        }
        timer = object : CountDownTimer(10000, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                val eventData = Arguments.createMap().apply {
                    putString("key", "count")
                    putInt("value", (millisUntilFinished / 1000).toInt())
                }
                emitEvenListenerCalled(eventData)
            }

            override fun onFinish() {
                timer?.cancel()
                timer = null
            }
        }
        timer?.start()
        return true
    }

    override fun fetchUser(id: String?, callback: Callback?) {
        val eventData = Arguments.createMap().apply {
            putString("id", id)
            putString("name", "John")
        }
        callback.let { it?.invoke(eventData) }
    }

    companion object {
        const val NAME = "NativeShowCase"
    }

    override fun getName() = NAME
}

基于BaseReactPackage构建的包类用于告知React Native如何实例化该模块。其ReactModuleInfo中的标志isTurboModule = true决定了该模块将通过新系统进行处理。

package com.nativeshowcase

import com.facebook.react.BaseReactPackage
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

class NativeShowCasePackage : BaseReactPackage() {
    override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
        if (name == NativeShowCaseModule.NAME) return NativeShowCaseModule(reactContext)
        return null
    }

    override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
        mapOf(
            NativeShowCaseModule.NAME to ReactModuleInfo(
                name = NativeShowCaseModule.NAME,
                className = NativeShowCaseModule.NAME,
                canOverrideExistingModule = false,
                needsEagerInit = true,
                isCxxModule = false,
                isTurboModule = true
            )
        )
    }
}

最后,通过将其添加到自动链接的包列表中,在MainApplication.kt中注册该包:

class MainApplication : Application(), ReactApplication {
    override val reactHost: ReactHost by lazy {
        getDefaultReactHost(
            context = applicationContext,
            packageList =
                PackageList(this).packages.apply {
                    add(NativeShowCasePackage())
                },
        )
    }
    override fun onCreate() {
        super.onCreate()
        loadReactNative(this)
    }
}

从React调用该模块

导入该规范的默认导出值,然后像操作普通对象一样使用它。该效果会订阅事件发射器,并返回一个用于在组件卸载时移除监听器的清理函数;其中一个按钮调用同步方法,另一个则使用 Promise 和回调方法。

import { StatusBar, StyleSheet, useColorScheme, View, Button } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import NativeShowCase from './specs/NativeShowCase';
import { useEffect } from 'react';

function App() {
  const isDarkMode = useColorScheme() === 'dark';
  return (
    <SafeAreaProvider>
      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
      <AppContent />
    </SafeAreaProvider>
  );
}
function AppContent() {
  useEffect(() => {
    const listener = NativeShowCase.evenListenerCalled(pair => {
      console.log('Event received:', pair);
    });
    return () => listener.remove();
  }, []);
  return (
    <View style={styles.container}>
      <Button
        title="Have Camera Flash?"
        onPress={() => {
          console.log('Have Camera Flash:', NativeShowCase.haveCameraFlash());
        }}
      />
      <Button
        title="Toggle Flashlight"
        onPress={() => {
          NativeShowCase.toggleFlashLight()
            .then(isOn => console.log('Flashlight:', isOn))
            .catch(err => console.error(err));
          NativeShowCase.fetchUser('123', user => {
            console.log('Fetched user:', user);
          });
        }}
      />
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
});
export default App;

尽量少使用同步方法:它们会阻塞 JavaScript 线程直到原生操作完成,因此仅应将其用于简单的查询操作。关于另一种绑定方式,请参阅Nitro Modules 与 Turbo Modules 的对比

关键要点

  • TypeScript 规范是契约;代码生成工具会据此为两个平台生成相应的原生接口。
  • 将规范文件命名为以Native为前缀的名称,并确保codegenConfig与文件夹和包保持同步。
  • iOS版本需要.mm文件;Android应用则必须用isTurboModule标记相关模块。
  • 请谨慎选择调用方式:简单读取操作使用同步方式,复杂任务使用Promises或回调函数,数据流处理则采用发射器模式。
  • 相关阅读