Підключення модуля Turbo всередині додатку від початку до кінця за допомогою React Native Codegen
Визначте типову специфікацію, запустіть генерацію коду та реалізуйте Turbo Module для iOS та Android із синхронними, Promise-, callback- та event-emitter-методами.
Нова архітектура 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.Пов’язана література
- Направлення сповіщень через глибокі посилання в React Native — налаштування універсальних та додаткових посилань, інтеграція Pusher Beams у додаток React Native та прив’язка веб-адрес до нативних екранів, щоб клік по сповіщенню відкривав потрібне місце.
- Планування оновлення Expo SDK 58: iOS 27, React Native 0.88 та нові інструменти — практичний огляд бета-версії Expo SDK 58: які зміни у iOS 27 та React Native 0.88, які функції є експериментальними та як безпечно протестувати оновлення.
- Використання нативних папок як результату збірки за допомогою Expo Prebuild та CNG — як функція Continuous Native Generation дозволяє додатку Expo використовувати власні нативні модулі, плагіни налаштувань та секрети EAS без необхідності зберігати чи редагувати вручну папки ios та android.
- Повідомлення в React Native: дозволи, канали та життєвий цикл FCM — Дізнайтеся, як дозволи, канали Android, токени FCM та обробники подій у фоновому, передньому та стані завершення праці поєднуються в системі повідомлень React Native з Notifee.
- Самостійне хостування оновлень OTA для чистого React Native з hot-updater та Supabase — Налаштуйте оновлення JavaScript через мережу у додатку чистого React Native з hot-updater та Supabase: ініціалізація, безшумний гак для оновлень, канали, скрипти розгортання та відкату.