Главная / Статьи / Подключение модуля турбо внутри приложения от начала до конца с использованием React Native Codegen

Подключение модуля турбо внутри приложения от начала до конца с использованием React Native Codegen

Определите типизированное описание, запустите генерацию кода и реализуйте Turbo Module для iOS и Android с использованием синхронных методов, Promise, обратных вызовов и механизма событий.

1409 слов

Новая архитектура 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 с папками и пакетами.
  • Для реализации в iOS требуются файлы с расширением .mm; пакеты для Android должны помечать модули атрибутом isTurboModule.
  • Тщательно выбирайте стиль вызова: синхронные вызовы для простых операций чтения, Promises или обратные вызовы для более сложных задач, а эмиттеры — для работы с потоками данных.
  • Связанные материалы