Podłączanie modułu Turbo w aplikacji od początku do końca za pomocą React Native Codegen
Zdefiniuj typową specyfikację, uruchom proces generowania kodu i zaimplementuj moduł Turbo na iOS oraz Android przy użyciu metod synchronicznych, Promise, callback oraz event-emitter.
Nowa architektura React Native zastępuje asynchroniczną mostówkę Turbo Modules, które JavaScript wykorzystuje poprzez JSI z typowanymi, generowanymi powiązaniami. Dzięki temu zmniejsza się nakład pracy związany z wywołaniami, a kontrakt jest sprawdzany przez codegen zamiast na podstawie konwencji. W tym przewodniku stworzymy jeden moduł w aplikacji (bez pakietu npm) i omówimy trzy style wywołań, których będziesz używał w praktyce: wartość zwracana synchronicznie, wyniki asynchroniczne za pomocą Promise lub callbacka oraz strumień zdarzeń.
Pisanie typowanej specyfikacji
Stwórz folder specs w korzeniu projektu, aby przechowywać schematy modułów:
/your-app
/specs
Wewnątrz niego dodaj plik NativeShowCase.ts. Narzędzie Codegen identyfikuje tylko pliki specyfikacji, których nazwy zaczynają się od Native. Podana poniżej specyfikacja definiuje po jednej metodzie na każdy styl: haveCameraFlash zwraca wartość synchronicznie, toggleFlashLight zwraca obiekt Promise, fetchUser przyjmuje funkcję callback, a evenListenerCalled to emiter zdarzeń przenoszący pary klucz/wartość. Funkcja getEnforcing rzuca błąd przy uruchamianiu, jeśli brakuje komponentu natywnego, co pozwala wcześnie wykryć błędy w łączeniu komponentów. Typowane emitory zdarzeń w specyfikacjach to nowsza funkcjonalność, więc upewnij się, że twoja wersja React Native obsługuje 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");
Ten plik jest jedynym źródłem prawdy; bindingi obu platform są generowane na jego podstawie.
Konfiguracja i uruchamianie Codegen
Dodaj blok codegenConfig do pliku package.json. Pole jsSrcsDir musi odpowiadać folderowi z specyfikacjami, a javaPackageName określa miejsce generowania klas dla Androida:
"codegenConfig": {
"name": "NativeShowCase",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativeshowcase"
}
}
Następnie wygeneruj pliki wynikowe. W przypadku Androida uruchom zadanie Gradle:
cd android && ./gradlew generateCodegenArtifactsFromSchema
W przypadku iOS proces generowania kodu odbywa się w trakcie instalacji pakietów pods:
cd ios && pod install
Masz teraz wygenerowane interfejsy, pliki nagłówkowe oraz kod łączący dla obu platform.
Wdrażanie modułu na iOS
Otwórz plik .xcworkspace w Xcode, utwórz grupę NativeShowCase i dodaj klasę w języku Objective-C o nazwie RCTNativeShowCase. Przeimieńuj plik .m na .mm – moduły Turbo wymagają języka Objective-C++ do współpracy z warstwą JSI w C++.
Tytuł importuje wygenerowany moduł i dziedziczy od wygenerowanej klasy bazowej, jednocześnie przyjmując wygenerowany protokół:
#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>
@end
NS_ASSUME_NONNULL_END
Implementacja zwraca nazwę modułu, przekazuje React Native’owi obiekt JSI z getTurboModule oraz wdraża każdą metodę określoną w specyfikacji. toggleFlashLight realizuje obietnicę (Promise), fetchUser wywołuje funkcję zwrotną, a haveCameraFlash zwraca wynik natychmiastowo, ale uruchamia timer, który emituje dziesięć zdarzeń za pośrednictwem wygenerowanej metody 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
Należy zauważyć, że ta wersja dla iOS w fetchUser wysyła pola key/value, podczas gdy specyfikacja wymaga id i name; należy je dostosować w rzeczywistym kodzie, ponieważ codegen nie sprawdza struktury danych przekazywanych do funkcji zwrotnych w czasie wykonywania.
Wdrażanie modułu na Androidzie
Stwórz pakiet com.nativeshowcase zadeklarowany w codegenConfig. Klasa modułu rozszerza wygenerowaną klasę NativeShowCaseSpec i przejmuje kontrolę nad każdą metodą: zmienia wartość flagi oraz realizuje obietnicę (Promise), uruchamia CountDownTimer, który przy każdym odliczeniu wysyła informację o pozostałych sekundach, oraz tworzy mapę dla funkcji zwrotnej.
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
}
Klasa pakietu oparta na BaseReactPackage informuje React Native, jak utworzyć instancję modułu. Flaga isTurboModule = true w jej obiektu ReactModuleInfo sprawia, że moduł jest przekierowywany przez nowy system.
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
)
)
}
}
Na koniec zarejestruj pakiet w pliku MainApplication.kt, dodając go do listy pakietów automatycznie powiązanych:
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)
}
}
Wywoływanie modułu z React
Imporтуj domyślny eksport specyfikacji i używaj go jak zwykłego obiektu. Efekt ten subskrybuje emiter zdarzeń i zwraca funkcję do czyszczenia, która usuwa słuchacz po odmontowaniu; jeden przycisk wywołuje metodę synchroniczną, a drugi metody typu Promise i callback.
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;
Używaj metod synchronicznych z umiarem: blokują one wątek JavaScriptu do chwili zwrotu wartości przez bibliotekę natywną, więc zachowaj je do prostych zapytań. Aby zapoznać się z alternatywnym podejściem do wiązania, sprawdź, jak Nitro Modules konkurują z Turbo Modules.
Główne wnioski
- Specyfikacja TypeScript jest umową; narzędzie codegen wyprowadza z niej interfejsy natywne dla obu platform.
- Nazwij pliki specyfikacji przedrostkiem
Nativei utrzymujcodegenConfigw synchronizacji z folderami oraz pakietami.
.mm; pakiety Android muszą oznaczyć moduły za pomocą isTurboModule.Literatura pokrewna
- Routing Push Notification Taps Through Deep Links in React Native — Konfiguracja uniwersalnych linków i linków aplikacji, integracja Pusher Beams z aplikacją React Native oraz mapowanie adresów URL internetowych na karty natywne, aby kliknięcie w powiadomienie otwierało właściwe miejsce.
- Planowanie aktualizacji do Expo SDK 58: iOS 27, React Native 0.88 oraz nowe narzędzia — Praktyczny przegląd wersji beta Expo SDK 58: jakie zmiany dotyczą iOS 27 i React Native 0.88, które funkcje są eksperymentalne oraz jak bezpiecznie przetestować aktualizację.
- Traktowanie folderów natywnych jako wyniku budowy za pomocą Expo Prebuild i CNG — Jak Continuous Native Generation umożliwia aplikacjom Expo korzystanie z dostosowanych modułów natywnych, pluginów konfiguracyjnych oraz sekretów EAS bez konieczności zapisywania lub ręcznego edytowania folderów ios i android.
- Notyfikacje w React Native: uprawnienia, kanały oraz life-cycle FCM — Dowiedz się, jak uprawnienia, kanały Androida, tokeny FCM oraz mechanizmy obsługi stanów pierwszego planu, tła i zamykania aplikacji współpracują ze sobą w systemie notyfikacji React Native z Notifee.
- Samodzielne aktualizacje OTA dla bare React Native z hot-updater i Supabase — Skonfiguruj aktualizacje JavaScript przez sieć w aplikacji bare React Native przy użyciu hot-updater i Supabase: inicjalizacja, cichy mechanizm aktualizacji, kanały, skrypty wdrażania oraz możliwość cofnięcia zmian.