Підключення модуля Turbo всередині додатку від початку до кінця за допомогою React Native Codegen
Модуль Turbo всередині додатку для React Native — від початку до кінця: специфікація на TypeScript з підтримкою типів, генерація коду, а потім код для iOS та Android із синхронізацією, Promise, обробкою подій та керуванням подіями.
Джерела: React Native Turbo Modules: A Guide to Calling Native Code in the New Architecture
Нова архітектура React Native замінює асинхронний міст на Turbo Modules, які JavaScript викликає через JSI за допомогою типованих, генерованих зв’язків. Це призводить до зниження навантаження від викликів та до перевірки контракту за допомогою codegen замість умовних правил. У цьому посібнику створюється один модуль у межах додатку (без пакета npm) та розглядаються три стилі викликів, які ви будете використовувати на практиці: синхронне повернення значення, асинхронні результати через Promise чи callback та потік подій.
Складання типованої специфікації
Створіть папку 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
Додайте блок codegenConfig до файлу package.json. Поле 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
Відкрийте файл .xcworkspace у Xcode, створіть групу NativeShowCase та додайте клас на мові Objective-C під назвою RCTNativeShowCase. Перейменуйте файл .m у .mm: Turbo Modules потребують Objective-C++ для взаємодії з шаром JSI на C++.
Заголовок імпортує створений модуль та успадковує його від базового класу, створеного автоматично, водночас застосовуючи створений протокол:
#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>
@end
NS_ASSUME_NONNULL_END
Реалізація повертає назву модуля, передає React Native об’єкт JSI з функції getTurboModule та реалізує кожну методику, передбачену специфікацією. Функція toggleFlashLight вирішує обіцянку, 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
Створіть пакет com.nativeshowcase, оголошений у codegenConfig. Клас модуля успадковує генерований 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, як інстанціювати модуль. Флаг isTurboModule = true у його ReactModuleInfo визначає шлях обробки через нову систему.
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 та 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;
Використовуйте синхронні методи обережно: вони блокують потік JavaScript до моменту повернення результату від нативного коду, тому залиште їх для простих пошуків. Щодо альтернативного підходу до прив’язки, дивіться, як Nitro Modules порівнюються з Turbo Modules.
Ключові висновки
- Специфікація TypeScript є контрактом; codegen на основі неї генерує інтерфейси для обох платформ.
- Називайте файли специфікацій з префіксом
Nativeта підтримуйте синхронність міжcodegenConfig, папками та пакетами.
.mm; пакети для Android мають позначати модулі за допомогою isTurboModule.