Подключение модуля турбо внутри приложения от начала до конца с использованием React Native Codegen
Определите типизированное описание, запустите генерацию кода и реализуйте Turbo Module для iOS и Android с использованием синхронных методов, Promise, обратных вызовов и механизма событий.
Новая архитектура React Native заменяет асинхронный мост на Turbo Modules, к которым JavaScript обращается через JSI с типизированными, генерируемыми связями. В результате снижается нагрузка от вызовов, а контракт проверяется с помощью кодгенерации, а не на основе традиций. В этом руководстве создается один модуль внутри приложения (без пакета npm) и рассматриваются три способа вызова, которые будут использоваться на практике: синхронное возвращение значения, асинхронные результаты через Promise или обратный вызов, и поток событий.
Написание типизированного описания
Создайте папку 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
Обратите внимание, что функция fetchUser для iOS отправляет поля 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 является контрактом; кодген на её основе генерирует интерфейсы для обеих платформ.
- Называйте файлы спецификаций с префиксом
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 — Как функция непрерывной генерации нативного кода позволяет приложению Expo использовать пользовательские нативные модули, плагины конфигурации и секреты EAS без необходимости сохранения или ручного редактирования папок ios и android.
- Уведомления в React Native: разрешения, каналы и жизненный цикл FCM — Узнайте, как разрешения, каналы Android, токены FCM и обработчики состояний переднего плана, фона и выхода взаимодействуют друг с другом в системе уведомлений React Native с использованием Notifee.
- Самостоятельное хостинговое обновление через OTA для чистого React Native с использованием hot-updater и Supabase — Настройте обновления JavaScript через кабель в приложении чистого React Native с использованием hot-updater и Supabase: инициализация, скрытый хук обновления, каналы, скрипты развертывания и возврат к предыдущей версии.