Мы создали нативный модуль NativeCalculator для iOS, который демонстрирует работу React Native Turbo Modules и системы Codegen. Этот модуль показывает различные способы взаимодействия между JavaScript и нативным кодом.
Matthew3dgNewArcTest/
├── specs/
│ └── NativeCalculator.ts # TypeScript спецификация
├── ios/
│ ├── NativeCalculator/
│ │ ├── RCTNativeCalculator.h # Заголовочный файл
│ │ └── RCTNativeCalculator.mm # Реализация для iOS
│ └── build/generated/ios/ # Автоматически генерируемые файлы
│ ├── NativeCalculatorSpec.h # Сгенерированный протокол
│ └── NativeCalculatorSpecJSI.h # JSI интерфейс
├── App.tsx # UI для тестирования
└── package.json # Конфигурация Codegen
export interface Spec extends TurboModule {
add(a: number, b: number): number;
multiply(a: number, b: number): number;
factorial(n: number): Promise<number>;
squareRoot(value: number, callback: (result: number) => void): void;
getModuleInfo(): string;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeCalculator');Что здесь происходит:
- Мы описываем интерфейс модуля на TypeScript
TurboModule- базовый интерфейс для всех Turbo Native ModulesTurboModuleRegistry.getEnforcing()- регистрирует модуль с именем'NativeCalculator'- TypeScript типы автоматически конвертируются в нативные типы через Codegen
Типы методов:
- Синхронные (
add,multiply): возвращают значение напрямую - Асинхронные (
factorial): возвращаютPromise - Callback (
squareRoot): принимают функцию обратного вызова
"codegenConfig": {
"name": "NativeCalculatorSpec",
"type": "modules",
"jsSrcsDir": "specs",
"ios": {
"modulesProvider": {
"NativeCalculator": "RCTNativeCalculator"
}
}
}Параметры:
name: Имя спецификации (будет использоваться для генерации файлов)type:"modules"- указывает, что генерируем модуль (не компонент)jsSrcsDir: Директория с TypeScript спецификациямиmodulesProvider: Связь между JS именем модуля и нативным классом- Ключ (
"NativeCalculator") - имя изTurboModuleRegistry.getEnforcing() - Значение (
"RCTNativeCalculator") - имя Objective-C класса
- Ключ (
Когда мы запускаем pod install, происходит следующее:
┌─────────────────────────────┐
│ specs/NativeCalculator.ts │ 1. TypeScript спецификация
└──────────┬──────────────────┘
│
↓
┌─────────────────────────────┐
│ React Native Codegen │ 2. Codegen анализирует спецификацию
│ (запускается при pod) │
└──────────┬──────────────────┘
│
↓
┌─────────────────────────────┐
│ Генерируются файлы: │ 3. Создается нативный код
│ • NativeCalculatorSpec.h │ - Протокол для Objective-C
│ • NativeCalculatorSpecJSI.h│ - JSI интерфейс для C++
│ • .mm реализации │ - Мост между JS и нативным кодом
└─────────────────────────────┘
Сгенерированные файлы:
@protocol NativeCalculatorSpec <RCTBridgeModule, RCTTurboModule>
- (NSNumber *)add:(double)a b:(double)b;
- (NSNumber *)multiply:(double)a b:(double)b;
- (void)factorial:(double)n
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;
- (void)squareRoot:(double)value
callback:(RCTResponseSenderBlock)callback;
- (NSString *)getModuleInfo;
@endCodegen автоматически преобразует:
number→double/NSNumber *string→NSString *Promise<T>→resolveиrejectблокиcallback→RCTResponseSenderBlock
class NativeCalculatorCxxSpecJSI : public TurboModule {
virtual double add(jsi::Runtime &rt, double a, double b) = 0;
virtual double multiply(jsi::Runtime &rt, double a, double b) = 0;
virtual jsi::Value factorial(jsi::Runtime &rt, double n) = 0;
// ...
};JSI (JavaScript Interface) - это прямой мост между JavaScript и C++, который:
- Работает синхронно без сериализации JSON
- Имеет низкую задержку вызовов
- Позволяет напрямую работать с JavaScript объектами из C++
@interface RCTNativeCalculator : NSObject <NativeCalculatorSpec>
@end
@implementation RCTNativeCalculator
// Связующий метод для Turbo Module
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeCalculatorSpecJSI>(params);
}
// Реализации методов
// ⚠️ ВАЖНО: Возвращаем NSNumber *, НЕ double!
- (NSNumber *)add:(double)a b:(double)b {
return @(a + b); // @() преобразует double в NSNumber
}
// ...
@end- TypeScript
number→ Objective-CNSNumber *(для return type) - Используйте
@()для преобразования:@(result)
Ключевые моменты:
-
getTurboModule:- главный связующий метод- Создает
NativeCalculatorSpecJSI(сгенерированный Codegen) - Этот объект управляет вызовами между JS и нативным кодом
- Вызывается автоматически при первом обращении к модулю
- Создает
-
Типы возвращаемых значений:
- Синхронные методы: возвращают значение напрямую (
double,NSString*) - Promise: используют
resolveиrejectблоки - Callback: используют
RCTResponseSenderBlock
- Синхронные методы: возвращают значение напрямую (
-
+ (NSString *)moduleName- Регистрирует имя модуля в React Native
- Должно совпадать с именем в
TurboModuleRegistry.getEnforcing()
Рассмотрим, что происходит при вызове NativeCalculator.add(5, 3):
┌──────────────────────────────────────────────────────────────┐
│ 1. JavaScript (App.tsx) │
│ const sum = NativeCalculator.add(5, 3); │
└────────────────────┬─────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. TurboModuleRegistry │
│ - Проверяет, что модуль существует │
│ - Находит зарегистрированный 'NativeCalculator' │
└────────────────────┬─────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. JSI Bridge (NativeCalculatorSpecJSI) │
│ - Преобразует JavaScript параметры в C++ типы: │
│ jsi::Value(5) → double(5.0) │
│ jsi::Value(3) → double(3.0) │
│ - Вызывает нативный метод через JSI │
└────────────────────┬─────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. Objective-C++ (RCTNativeCalculator.mm) │
│ - (double)add:(double)a b:(double)b { │
│ NSLog(@"add called with a=%f, b=%f", a, b); │
│ return a + b; // 8.0 │
│ } │
└────────────────────┬─────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 5. Обратно через JSI │
│ - double(8.0) → jsi::Value(8) │
│ - Возвращается в JavaScript │
└────────────────────┬─────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 6. JavaScript получает результат │
│ sum = 8 │
└──────────────────────────────────────────────────────────────┘
Время выполнения: ~10-50 микросекунд (в зависимости от устройства)
Сравнение с Legacy Bridge:
- Legacy Bridge: JS → JSON сериализация → Native Thread → JSON десериализация → метод → сериализация → JS
- Turbo Module + JSI: JS → JSI (прямой вызов) → метод → JSI → JS
- Ускорение: 2-10x быстрее
TypeScript:
add(a: number, b: number): number;Objective-C:
- (double)add:(double)a b:(double)b {
return a + b;
}JavaScript использование:
const result = NativeCalculator.add(5, 3); // 8Особенности:
- ✅ Возвращают результат немедленно
- ✅ Блокируют JS thread на время выполнения
⚠️ Не подходят для долгих операций (> 16ms)
TypeScript:
factorial(n: number): Promise<number>;Objective-C:
- (void)factorial:(double)n
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
if (n < 0) {
reject(@"INVALID_INPUT", @"Negative numbers not allowed", nil);
return;
}
long long result = 1;
for (NSInteger i = 2; i <= (NSInteger)n; i++) {
result *= i;
}
resolve(@(result));
}JavaScript использование:
try {
const result = await NativeCalculator.factorial(5); // 120
console.log(result);
} catch (error) {
console.error(error.message);
}Особенности:
- ✅ Не блокируют JS thread
- ✅ Можно обрабатывать ошибки через
reject - ✅ Идеальны для долгих операций
- 📝 Параметры ошибки:
(code, message, error)
TypeScript:
squareRoot(value: number, callback: (result: number) => void): void;Objective-C:
- (void)squareRoot:(double)value callback:(RCTResponseSenderBlock)callback {
if (value < 0) {
callback(@[@(NAN)]);
return;
}
double result = sqrt(value);
callback(@[@(result)]); // Всегда массив!
}JavaScript использование:
NativeCalculator.squareRoot(16, result => {
console.log(result); // 4
});Особенности:
⚠️ Callback всегда принимает массив аргументов⚠️ Может вызываться только один раз- 📝 Legacy подход, предпочтительнее использовать Promise
-
package.json указывает
modulesProvider:"NativeCalculator": "RCTNativeCalculator"
-
Codegen генерирует
RCTModuleProviders.mm:- (NSArray<Class<RCTTurboModule>> *)getModuleClasses { return @[ RCTNativeCalculator.class, // другие модули... ]; }
-
При запуске приложения React Native:
- Сканирует все классы из
getModuleClasses - Вызывает
[RCTNativeCalculator moduleName]→"NativeCalculator" - Регистрирует модуль в
TurboModuleRegistry
- Сканирует все классы из
-
При первом вызове из JavaScript:
TurboModuleRegistry.getEnforcing('NativeCalculator')- Создает экземпляр
RCTNativeCalculator - Вызывает
getTurboModule:→ возвращает JSI binding - Кэширует экземпляр (singleton)
- TypeScript спецификация проверяется на этапе компиляции
- Codegen генерирует нативные типы автоматически
- Невозможно вызвать метод с неправильными параметрами
- JSI обходит Legacy Bridge
- Нет сериализации/десериализации JSON
- Прямые вызовы между JS и Native
- Одна спецификация → код для всех платформ
- Автоматическая генерация boilerplate кода
- TypeScript автодополнение в IDE
- Модули загружаются только при первом использовании
- Быстрый старт приложения
- Меньше потребление памяти
NSLog(@"[NativeCalculator] add called with a=%f, b=%f", a, b);В Xcode Console вы увидите:
[NativeCalculator] add called with a=5.000000, b=3.000000
console.log('[JS] Add result:', sum);В Metro Bundler или React Native Debugger:
[JS] Add result: 8
import { NativeModules } from 'react-native';
console.log('Available modules:', Object.keys(NativeModules));
// Должен включать 'NativeCalculator'- Обновите TypeScript спецификацию:
divide(a: number, b: number): Promise<number>;-
Запустите
pod install(Codegen обновит протокол) -
Реализуйте метод в
.mm:
- (void)divide:(double)a
b:(double)b
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
if (b == 0) {
reject(@"DIVISION_BY_ZERO", @"Cannot divide by zero", nil);
return;
}
resolve(@(a / b));
}- Используйте в JavaScript:
const result = await NativeCalculator.divide(10, 2); // 5| Характеристика | Legacy Bridge | Turbo Modules |
|---|---|---|
| Время вызова | 100-500 μs | 10-50 μs |
| Типы | Runtime проверка | Compile-time |
| Загрузка | Все сразу | Lazy (по требованию) |
| Синхронные методы | ❌ Сложно | ✅ Легко |
| Codegen | ❌ Нет | ✅ Да |
| Будущее | Deprecated | ✅ Поддерживается |
- Используйте TypeScript для спецификаций
- Делайте синхронные методы короткими (< 16ms)
- Используйте Promise для долгих операций
- Логируйте вызовы методов для отладки
- Проверяйте входные параметры на корректность
- Не используйте синхронные методы для долгих операций
- Не забывайте вызывать
resolveилиrejectв Promise - Не изменяйте сгенерированные файлы вручную
- Не используйте сложные объекты (лучше сериализовать в JSON)
Вы создали полноценный Turbo Native Module с:
- ✅ TypeScript спецификацией
- ✅ Автоматической генерацией кода через Codegen
- ✅ iOS реализацией на Objective-C++
- ✅ Тестовым UI в React Native
- ✅ Поддержкой синхронных, асинхронных и callback методов
Это современный и рекомендуемый способ создания нативных модулей в React Native!