Strona główna / Artykuły / Podłączanie modułu Turbo w aplikacji od początku do końca za pomocą React Native Codegen

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.

1409 słów

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 Native i utrzymuj codegenConfig w synchronizacji z folderami oraz pakietami.
  • Implementacje dla iOS wymagają plików .mm; pakiety Android muszą oznaczyć moduły za pomocą isTurboModule.
  • Wybierz styl wywołania starannie: synchroniczny do prostych operacji odczytu, Promises lub callbacki do złożonych zadań, a emiterzy do przepływów danych.
  • Literatura pokrewna