Verkabelung eines In-App-Turbo-Moduls von Anfang bis Ende mit React Native Codegen
Definieren Sie eine typisierte Spezifikation, führen Sie Codegen aus und implementieren Sie ein Turbo Module für iOS und Android mit synchronen Methoden sowie Methoden basierend auf Promise, Callback und Event-Emitter.
Die neue Architektur von React Native ersetzt die asynchrone Brücke durch Turbo Modules, über die JavaScript mittels JSI mit typisierten, generierten Bindungen aufgerufen wird. Dadurch entsteht weniger Aufrufaufwand und ein durch Codegen statt durch Konvention überprüftes Vertragsmodell. In dieser Anleitung wird ein Modul innerhalb einer App erstellt (ohne npm-Paket) und die drei Aufrufmethoden vorgestellt, die Sie in der Praxis verwenden werden: ein synchroner Rückgabewert, asynchrone Ergebnisse über Promise oder Callback sowie ein Ereignisstrom.
Erstellung der typisierten Spezifikation
Erstellen Sie ein Verzeichnis specs im Projektverzeichnis, um die Modellschemata zu speichern:
/your-app
/specs
Fügen Sie innerhalb davon NativeShowCase.ts hinzu. Codegen erfasst nur Spezifikationsdateien, deren Namen mit Native beginnen. Die untenstehende Spezifikation deklariert eine Methode pro Stil: haveCameraFlash gibt synchron zurück, toggleFlashLight gibt ein Promise zurück, fetchUser nimmt einen Callback entgegen und evenListenerCalled ist ein Ereignisemitter, der Schlüssel/Wert-Paare überträgt. getEnforcing wirft bei Startzeit einen Fehler aus, falls die native Komponente fehlt, wodurch Verkabelungsfehler frühzeitig aufgedeckt werden. Typisierte Ereignisemitter in Spezifikationen sind eine neuere Ergänzung – überprüfen Sie daher, ob Ihre React Native-Version CodegenTypes.EventEmitter unterstützt.
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");
Diese Datei ist die einzige Quelle der Wahrheit; die Bindungen beider Plattformen werden daraus generiert.
Konfigurieren und Ausführen von Codegen
Fügen Sie einen codegenConfig-Block zu package.json hinzu. jsSrcsDir muss mit Ihrem Specs-Ordner übereinstimmen, und javaPackageName bestimmt, wo die Android-Klassen generiert werden:
"codegenConfig": {
"name": "NativeShowCase",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativeshowcase"
}
}
Erstellen Sie anschließend die Artefakte. Auf Android führen Sie die Gradle-Aufgabe aus:
cd android && ./gradlew generateCodegenArtifactsFromSchema
Auf iOS wird codegen im Zuge der Installation von Pods ausgeführt:
cd ios && pod install
Nun haben Sie für beide Plattformen generierte Schnittstellen, Header und Verbindungscode.
Implementierung des Moduls auf iOS
Öffnen Sie das .xcworkspace-Datei in Xcode, erstellen Sie eine NativeShowCase-Gruppe und fügen Sie eine Objective-C-Klasse mit dem Namen RCTNativeShowCase hinzu. Ändern Sie die Endung der .m-Datei in .mm: Turbo Modules benötigen Objective-C++ zur Interoperabilität mit der C++ JSI-Schicht.
Der Header importiert das generierte Modul und erbt von der generierten Basisklasse, wobei er gleichzeitig das generierte Protokoll übernimmt:
#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>
@end
NS_ASSUME_NONNULL_END
Die Implementierung gibt den Modulnamen zurück, übergibt React Native ein JSI-Objekt aus getTurboModule und implementiert jede Spezifikationsmethode. toggleFlashLight löst die Promise auf, fetchUser ruft den Callback auf und haveCameraFlash gibt sofort einen Wert zurück, startet aber einen Timer, der durch die generierte emitEvenListenerCalled-Methode zehn Ereignisse auslöst.
#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
Beachten Sie, dass diese iOS-Version von fetchUser key/value-Felder sendet, während die Spezifikation id und name vorsieht; passen Sie diese im echten Code an, da codegen die Form des Callback-Payloads zur Laufzeit nicht überprüft.
Implementierung des Moduls auf Android
Erstellen Sie das in codegenConfig deklarierte Paket com.nativeshowcase. Die Modulklasse erweitert die generierte NativeShowCaseSpec und überschreibt jede Methode: Sie schaltet ein Flag um und löst die Promise auf, führt einen CountDownTimer aus, der bei jedem Tick die verbleibenden Sekunden ausgibt, und erstellt ein Map für den Callback.
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
}
Eine auf BaseReactPackage basierende Paketklasse weist React Native an, wie das Modul instanziert werden soll. Das Flag isTurboModule = true in ihrer ReactModuleInfo sorgt dafür, dass es über das neue System geleitet wird.
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
)
)
}
}
Schließlich registrieren Sie das Paket in MainApplication.kt, indem Sie es der automatisch verknüpften Paketliste hinzufügen:
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)
}
}
Aufrufen des Moduls von React aus
Importieren Sie den Standardexport der Spezifikation und rufen Sie ihn wie jedes andere Objekt auf. Der Effekt abonniert sich beim Event-Emitter und gibt eine Funktion zur Bereinigung zurück, die den Zuhörer bei Entfernung deaktiviert; eine Schaltfläche ruft die synchrone Methode auf, die andere verwendet die Promise- und Callback-Methoden.
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;
Verwenden Sie synchrone Methoden sparsam: Sie blockieren den JavaScript-Thread, bis die native Funktion zurückgegeben wird, daher sollten sie nur für einfache Abfragen verwendet werden. Für einen alternativen Bindungsansatz sehen Sie sich an, wie Nitro Modules sich gegenüber Turbo Modules behaupten.
Kernpunkte
- Die TypeScript-Spezifikation ist der Vertrag; codegen leitet daraus native Schnittstellen für beide Plattformen ab.
- Namen Sie Spezifikationsdateien mit dem Präfix
Nativeund halten SiecodegenConfigin Einklang mit Ordnern und Paketen.
.mm-Dateien; Android-Pakete müssen die Module mit isTurboModule kennzeichnen.Zusätzliche Literatur
- Routing von Push-Benachrichtigungen über Deep Links in React Native — Einrichtung von Universal-Links und App-Links, Anbindung von Pusher Beams an eine React Native-Anwendung sowie Zuordnung von Web-URLs zu nativen Bildschirmen, damit ein Klick auf eine Benachrichtigung den richtigen Ort öffnet.
- Planung eines Expo SDK 58-Updates: iOS 27, React Native 0.88 und neue Werkzeuge — Eine praktische Übersicht über die Expo SDK 58 Beta: Welche Änderungen gibt es für iOS 27 und React Native 0.88, welche Funktionen sind noch experimentell und wie lässt sich das Update sicher testen.
- Native Verzeichnisse als Build-Ausgabe mit Expo Prebuild und CNG behandeln — Wie Continuous Native Generation es ermöglicht, dass eine Expo-App benutzerdefinierte native Module, Konfigurationsplugins sowie EAS-Secrets verwendet, ohne die ios- und android-Verzeichnisse manuell bearbeiten oder speichern zu müssen.
- React Native Notifications: Erlaubnisse, Kanäle und der FCM-Lebenszyklus — Erfahren Sie, wie Erlaubnisse, Android-Kanäle, FCM-Tokens sowie Handler für den Vordergrund-, Hintergrund- und Beendigungsmodus in einem React Native-Notificationssystem mit Notifee zusammenwirken.
- Eigenhostete OTA-Updates für Bare React Native mit Hot-Updater und Supabase — Konfigurieren Sie drahtlose JavaScript-Updates in einer Bare React Native-Anwendung mit Hot-Updater und Supabase: Initialisierung, ein stummer Update-Hook, Kanäle, Bereitstellungs-Skripte und Rollback-Möglichkeiten.