Skip to content

Latest commit

 

History

History
565 lines (415 loc) · 20.9 KB

File metadata and controls

565 lines (415 loc) · 20.9 KB

📱 Подробное объяснение Native Module с Codegen

🎯 Обзор

Мы создали нативный модуль 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

🔄 Схема работы Codegen и Turbo Modules

1️⃣ TypeScript спецификация (specs/NativeCalculator.ts)

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 Modules
  • TurboModuleRegistry.getEnforcing() - регистрирует модуль с именем 'NativeCalculator'
  • TypeScript типы автоматически конвертируются в нативные типы через Codegen

Типы методов:

  • Синхронные (add, multiply): возвращают значение напрямую
  • Асинхронные (factorial): возвращают Promise
  • Callback (squareRoot): принимают функцию обратного вызова

2️⃣ Конфигурация Codegen (package.json)

"codegenConfig": {
  "name": "NativeCalculatorSpec",
  "type": "modules",
  "jsSrcsDir": "specs",
  "ios": {
    "modulesProvider": {
      "NativeCalculator": "RCTNativeCalculator"
    }
  }
}

Параметры:

  • name: Имя спецификации (будет использоваться для генерации файлов)
  • type: "modules" - указывает, что генерируем модуль (не компонент)
  • jsSrcsDir: Директория с TypeScript спецификациями
  • modulesProvider: Связь между JS именем модуля и нативным классом
    • Ключ ("NativeCalculator") - имя из TurboModuleRegistry.getEnforcing()
    • Значение ("RCTNativeCalculator") - имя Objective-C класса

3️⃣ Процесс генерации кода (Codegen)

Когда мы запускаем pod install, происходит следующее:

┌─────────────────────────────┐
│ specs/NativeCalculator.ts   │  1. TypeScript спецификация
└──────────┬──────────────────┘
           │
           ↓
┌─────────────────────────────┐
│    React Native Codegen     │  2. Codegen анализирует спецификацию
│   (запускается при pod)     │
└──────────┬──────────────────┘
           │
           ↓
┌─────────────────────────────┐
│  Генерируются файлы:        │  3. Создается нативный код
│  • NativeCalculatorSpec.h   │     - Протокол для Objective-C
│  • NativeCalculatorSpecJSI.h│     - JSI интерфейс для C++
│  • .mm реализации           │     - Мост между JS и нативным кодом
└─────────────────────────────┘

Сгенерированные файлы:

NativeCalculatorSpec.h (Objective-C протокол)

@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;

@end

Codegen автоматически преобразует:

  • numberdouble / NSNumber *
  • stringNSString *
  • Promise<T>resolve и reject блоки
  • callbackRCTResponseSenderBlock

NativeCalculatorSpecJSI.h (C++ JSI интерфейс)

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++

4️⃣ iOS реализация (RCTNativeCalculator.mm)

@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-C NSNumber * (для return type)
  • Используйте @() для преобразования: @(result)

Ключевые моменты:

  1. getTurboModule: - главный связующий метод

    • Создает NativeCalculatorSpecJSI (сгенерированный Codegen)
    • Этот объект управляет вызовами между JS и нативным кодом
    • Вызывается автоматически при первом обращении к модулю
  2. Типы возвращаемых значений:

    • Синхронные методы: возвращают значение напрямую (double, NSString*)
    • Promise: используют resolve и reject блоки
    • Callback: используют RCTResponseSenderBlock
  3. + (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 быстрее

🔍 Типы методов в деталях

1️⃣ Синхронные методы

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)

2️⃣ Асинхронные методы (Promise)

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)

3️⃣ Методы с Callback

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

⚙️ Конфигурация и регистрация

Как React Native находит наш модуль?

  1. package.json указывает modulesProvider:

    "NativeCalculator": "RCTNativeCalculator"
  2. Codegen генерирует RCTModuleProviders.mm:

    - (NSArray<Class<RCTTurboModule>> *)getModuleClasses {
      return @[
        RCTNativeCalculator.class,
        // другие модули...
      ];
    }
  3. При запуске приложения React Native:

    • Сканирует все классы из getModuleClasses
    • Вызывает [RCTNativeCalculator moduleName]"NativeCalculator"
    • Регистрирует модуль в TurboModuleRegistry
  4. При первом вызове из JavaScript:

    • TurboModuleRegistry.getEnforcing('NativeCalculator')
    • Создает экземпляр RCTNativeCalculator
    • Вызывает getTurboModule: → возвращает JSI binding
    • Кэширует экземпляр (singleton)

🎨 Преимущества Turbo Modules + Codegen

✅ Безопасность типов

  • TypeScript спецификация проверяется на этапе компиляции
  • Codegen генерирует нативные типы автоматически
  • Невозможно вызвать метод с неправильными параметрами

⚡ Производительность

  • JSI обходит Legacy Bridge
  • Нет сериализации/десериализации JSON
  • Прямые вызовы между JS и Native

🔧 Удобство разработки

  • Одна спецификация → код для всех платформ
  • Автоматическая генерация boilerplate кода
  • TypeScript автодополнение в IDE

📦 Ленивая загрузка

  • Модули загружаются только при первом использовании
  • Быстрый старт приложения
  • Меньше потребление памяти

🔍 Отладка

Логирование в iOS

NSLog(@"[NativeCalculator] add called with a=%f, b=%f", a, b);

В Xcode Console вы увидите:

[NativeCalculator] add called with a=5.000000, b=3.000000

Логирование в JavaScript

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'

🚀 Расширение модуля

Добавление нового метода

  1. Обновите TypeScript спецификацию:
divide(a: number, b: number): Promise<number>;
  1. Запустите pod install (Codegen обновит протокол)

  2. Реализуйте метод в .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));
}
  1. Используйте в 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 ✅ Поддерживается

🎯 Практические рекомендации

✅ DO (Делайте)

  • Используйте TypeScript для спецификаций
  • Делайте синхронные методы короткими (< 16ms)
  • Используйте Promise для долгих операций
  • Логируйте вызовы методов для отладки
  • Проверяйте входные параметры на корректность

❌ DON'T (Не делайте)

  • Не используйте синхронные методы для долгих операций
  • Не забывайте вызывать resolve или reject в Promise
  • Не изменяйте сгенерированные файлы вручную
  • Не используйте сложные объекты (лучше сериализовать в JSON)

📚 Полезные ссылки


🎉 Заключение

Вы создали полноценный Turbo Native Module с:

  • ✅ TypeScript спецификацией
  • ✅ Автоматической генерацией кода через Codegen
  • ✅ iOS реализацией на Objective-C++
  • ✅ Тестовым UI в React Native
  • ✅ Поддержкой синхронных, асинхронных и callback методов

Это современный и рекомендуемый способ создания нативных модулей в React Native!