Startseite / Artikel / Verkabelung eines In-App-Turbo-Moduls von Anfang bis Ende mit React Native Codegen

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.

1409 Wörter

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 Native und halten Sie codegenConfig in Einklang mit Ordnern und Paketen.
  • Die iOS-Implementierungen benötigen .mm-Dateien; Android-Pakete müssen die Module mit isTurboModule kennzeichnen.
  • Wählen Sie den Aufrufstil bewusst aus: Synchron für einfache Lesevorgänge, Promises oder Callbacks für aufwendigere Aufgaben sowie Emitter für Streams.
  • Zusätzliche Literatur