使用 React Native Codegen 实现应用内涡轮模块的端到端连接
定义类型化规范,运行代码生成,然后在 iOS 和 Android 上实现 Turbo Module,支持同步、Promise、回调以及事件发射器等多种方法。
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字段,而规范要求的是id和name;在实际代码中需使两者保持一致,因为代码生成工具在运行时不会验证回调参数的格式。
在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与文件夹和包保持同步。
.mm文件;Android应用则必须用isTurboModule标记相关模块。相关阅读
- 在React Native中通过深度链接处理推送通知点击 —— 设置通用链接和应用链接,将Pusher Beams集成到React Native应用中,并将网页URL映射到原生界面,从而确保点击通知后能打开正确的内容。
- 规划 Expo SDK 58 升级:iOS 27、React Native 0.88 及新工具 —— 对 Expo SDK 58 测试版的实用介绍:iOS 27 和 React Native 0.88 有哪些变化,哪些功能仍处于实验阶段,以及如何安全地进行升级测试。
- 利用 Expo Prebuild 和 CNG 将原生文件夹视为构建输出 —— 连续原生生成功能如何让 Expo 应用能够使用自定义的原生模块、配置插件和 EAS 密钥,而无需提交或手动编辑 iOS 和 Android 文件夹。
- React Native通知系统:权限、频道与FCM生命周期 — 了解在基于Notifee的React Native通知系统中,权限、Android频道、FCM令牌以及前台、后台和退出状态处理程序是如何相互配合的。
- 使用hot-updater和Supabase为纯React Native应用实现自托管OTA更新 — 学习如何利用hot-updater和Supabase在纯React Native应用中实现JavaScript的空中更新功能,包括初始化、静默更新钩子、频道管理、部署脚本以及回滚机制。