Галоўная / Артыкулы / Падчыненне модуля Turbo ўнутрь аплікацыі з нагадкай пра канец-канец за дапамойкою кодгенеравання React Native

Падчыненне модуля Turbo ўнутрь аплікацыі з нагадкай пра канец-канец за дапамойкою кодгенеравання React Native

З’явіце типаваны спэк, запусціце кодгенераванне і рэалізуйце Turbo Module на iOS і Android з сінхроннымі, Promise, callback і методамі event-emitter.

1409 слоў

Новая архітектура 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++ каб было можна саўместна працаваць з шаром C++ JSI.

Загалоўка імпортуе ствараны модуль, успадковвае яго базовую класу і адмаўляе ствараны протакол:

#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 рашае задачу Promise, 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

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