Przejdź do treści głównej
Wersja: 0.77

Komunikacja między natywnym kodem a React Native

Nieoficjalne Tłumaczenie Beta

Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →

W przewodnikach Integracja z istniejącymi aplikacjami i Natywne komponenty UI uczymy się osadzać React Native w komponentach natywnych i odwrotnie. Gdy mieszamy natywne komponenty z React Native, w końcu pojawi się potrzeba komunikacji między tymi dwoma światami. Niektóre sposoby osiągnięcia tego zostały już wspomniane w innych przewodnikach. Ten artykuł podsumowuje dostępne techniki.

Wprowadzenie

React Native jest inspirowany Reactem, więc podstawowa koncepcja przepływu informacji jest podobna. Przepływ w React jest jednokierunkowy. Utrzymujemy hierarchię komponentów, w której każdy komponent zależy wyłącznie od swojego rodzica i własnego stanu wewnętrznego. Osiągamy to za pomocą właściwości: dane są przekazywane od rodzica do dzieci w sposób odgórny. Jeśli komponent przodka zależy od stanu potomka, należy przekazać funkcję zwrotną (callback), którą potomek użyje do aktualizacji przodka.

Ta sama koncepcja dotyczy React Native. Dopóki budujemy aplikację wyłącznie w ramach frameworka, możemy sterować aplikacją za pomocą właściwości i funkcji zwrotnych. Jednak gdy mieszamy komponenty React Native z natywnymi, potrzebujemy specyficznych, międzyjęzykowych mechanizmów, które umożliwią przekazywanie informacji między nimi.

Zatrzymuje działającą animację i resetuje wartość do oryginalnej.

Właściwości to najbardziej bezpośredni sposób komunikacji międzykomponentowej. Potrzebujemy więc sposobu przekazywania właściwości zarówno z natywnego do React Native, jak i z React Native do natywnego.

Przekazywanie właściwości z natywnego do React Native

Aby osadzić widok React Native w komponencie natywnym, używamy RCTRootView. RCTRootView to UIView przechowujący aplikację React Native. Stanowi też interfejs między natywną częścią a hostowaną aplikacją.

RCTRootView posiada inicjalizator pozwalający przekazać dowolne właściwości do aplikacji React Native. Parametr initialProperties musi być instancją NSDictionary. Słownik jest wewnętrznie konwertowany na obiekt JSON, do którego może odwołać się najwyższy komponent JS.

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ffffff/000000.png",
@"https://dummyimage.com/600x400/000000/ffffff.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
tsx
import React from 'react';
import {View, Image} from 'react-native';

export default class ImageBrowserApp extends React.Component {
renderImage(imgURI) {
return <Image source={{uri: imgURI}} />;
}
render() {
return <View>{this.props.images.map(this.renderImage)}</View>;
}
}

RCTRootView udostępnia też właściwość do odczytu i zapisu appProperties. Po ustawieniu appProperties aplikacja React Native jest renderowana ponownie z nowymi właściwościami. Aktualizacja następuje tylko wtedy, gdy nowe właściwości różnią się od poprzednich.

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ff0000/000000.png",
@"https://dummyimage.com/600x400/ffffff/ff0000.png"];

rootView.appProperties = @{@"images" : imageList};

Możesz aktualizować właściwości w dowolnym momencie. Jednak aktualizacje muszą być wykonywane w wątku głównym. Odczyt możesz wykonywać w dowolnym wątku.

uwaga

Obecnie istnieje znany problem, gdzie ustawienie appProperties podczas inicjalizacji mostka może spowodować utratę zmian. Więcej informacji: https://github.com/facebook/react-native/issues/20115.

Nie ma możliwości aktualizacji tylko części właściwości. Sugerujemy zaimplementowanie własnego opakowania.

Przekazywanie właściwości z React Native do natywnego

Problem udostępniania właściwości komponentów natywnych został szczegółowo omówiony w tym artykule. Krótko mówiąc: eksportuj właściwości za pomocą makra RCT_CUSTOM_VIEW_PROPERTY we własnym komponencie natywnym, a następnie używaj ich w React Native tak, jakby komponent był zwykłym komponentem React Native.

Ograniczenia właściwości

Główną wadą właściwości międzyjęzykowych jest brak wsparcia dla funkcji zwrotnych, które umożliwiłyby obsługę wiązań danych w kierunku od dołu do góry. Wyobraź sobie, że masz mały widok RN, który chcesz usunąć z natywnego widoku nadrzędnego w wyniku akcji JS. Nie da się tego osiągnąć za pomocą właściwości, ponieważ informacja musiałaby płynąć od dołu do góry.

Choć istnieje odmiana funkcji zwrotnych międzyjęzykowych (opisana tutaj), nie zawsze są one tym, czego potrzebujemy. Główny problem polega na tym, że nie są przeznaczone do przekazywania jako właściwości. Ten mechanizm pozwala raczej wywołać akcję natywną z JS i obsłużyć jej wynik w JS.

Inne sposoby interakcji międzyjęzykowej (zdarzenia i moduły natywne)

Jak wspomniano w poprzednim rozdziale, używanie właściwości ma pewne ograniczenia. Czasami właściwości nie wystarczą do sterowania logiką naszej aplikacji i potrzebujemy rozwiązania, które zapewni większą elastyczność. Ten rozdział omawia inne techniki komunikacji dostępne w React Native. Można je wykorzystać zarówno do komunikacji wewnętrznej (pomiędzy warstwami JS i natywnymi w RN), jak i zewnętrznej (pomiędzy RN a „czysto natywną” częścią aplikacji).

React Native umożliwia wykonywanie wywołań funkcji międzyjęzykowych. Możesz wykonywać niestandardowy kod natywny z poziomu JS i odwrotnie. Niestety, w zależności od strony, na której pracujemy, ten sam cel osiągamy w różny sposób. Dla kodu natywnego używamy mechanizmu zdarzeń do zaplanowania wykonania funkcji obsługi w JS, podczas gdy dla React Native bezpośrednio wywołujemy metody eksportowane przez moduły natywne.

Wywoływanie funkcji React Native z kodu natywnego (zdarzenia)

Zdarzenia zostały szczegółowo opisane w tym artykule. Pamiętaj, że użycie zdarzeń nie daje gwarancji co do czasu wykonania, ponieważ zdarzenie jest obsługiwane w osobnym wątku.

Zdarzenia są potężne, ponieważ pozwalają nam zmieniać komponenty React Native bez konieczności posiadania do nich referencji. Istnieją jednak pewne pułapki, w które można wpaść podczas ich używania:

  • Ponieważ zdarzenia mogą być wysyłane z dowolnego miejsca, mogą wprowadzać do projektu zależności w stylu spaghetti.

  • Zdarzenia współdzielą przestrzeń nazw, co oznacza, że możesz napotkać kolizje nazw. Kolizje nie są wykrywane statycznie, co utrudnia ich debugowanie.

  • Jeśli używasz kilku instancji tego samego komponentu React Native i chcesz je rozróżnić z perspektywy zdarzenia, prawdopodobnie będziesz musiał wprowadzić identyfikatory i przekazywać je wraz ze zdarzeniami (możesz użyć reactTag natywnego widoku jako identyfikatora).

Typowym wzorcem przy osadzaniu kodu natywnego w React Native jest uczynienie menedżera widoków RCTViewManager delegatem widoków, który wysyła zdarzenia z powrotem do JavaScript przez most. To pozwala skupić powiązane wywołania zdarzeń w jednym miejscu.

Wywoływanie funkcji natywnych z React Native (moduły natywne)

Moduły natywne to klasy Objective-C dostępne w JS. Zazwyczaj tworzona jest jedna instancja każdego modułu na most JS. Mogą eksportować dowolne funkcje i stałe do React Native. Zostały szczegółowo omówione w tym artykule.

Fakt, że moduły natywne są singletonami, ogranicza ten mechanizm w kontekście osadzania. Załóżmy, że mamy komponent React Native osadzony w natywnym widoku i chcemy zaktualizować natywny widok nadrzędny. Korzystając z mechanizmu modułów natywnych, eksportowalibyśmy funkcję, która przyjmuje nie tylko oczekiwane argumenty, ale także identyfikator natywnego widoku nadrzędnego. Identyfikator ten byłby używany do pobrania referencji do widoku nadrzędnego w celu jego aktualizacji. Oznacza to, że musielibyśmy przechowywać w module mapowanie identyfikatorów do widoków natywnych.

Chociaż to rozwiązanie jest złożone, jest używane w RCTUIManager, który jest wewnętrzną klasą React Native zarządzającą wszystkimi widokami React Native.

Moduły natywne mogą również służyć do udostępniania istniejących bibliotek natywnych w JS. Biblioteka Geolokalizacji jest żywym przykładem tej idei.

ostrzeżenie

Wszystkie moduły natywne współdzielą tę samą przestrzeń nazw. Uważaj na kolizje nazw podczas tworzenia nowych.

Przepływ obliczania układu

Integrując kod natywny z React Native, potrzebujemy również sposobu na połączenie dwóch różnych systemów układu. Ta sekcja omawia typowe problemy z układem i zawiera krótki opis mechanizmów służących do ich rozwiązania.

Układ natywnego komponentu osadzonego w React Native

Ten przypadek został omówiony w tym artykule. Podsumowując, ponieważ wszystkie nasze natywne widoki react są podklasami UIView, większość atrybutów stylu i rozmiaru będzie działać zgodnie z oczekiwaniami bez dodatkowej konfiguracji.

Układ komponentu React Native osadzonego w kodzie natywnym

Zawartość React Native o stałym rozmiarze

Ogólnym scenariuszem jest aplikacja React Native o stałym rozmiarze, znanym stronie natywnej. Szczególnie pełnoekranowy widok React Native wpada w ten przypadek. Jeśli potrzebujemy mniejszego widoku głównego, możemy jawnie ustawić ramkę (frame) dla RCTRootView.

Na przykład, aby utworzyć aplikację RN o wysokości 200 (logicznych) pikseli i szerokości równej widokowi hostującemu, moglibyśmy zrobić:

SomeViewController.m
- (void)viewDidLoad
{
[...]
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:appName
initialProperties:props];
rootView.frame = CGRectMake(0, 0, self.view.width, 200);
[self.view addSubview:rootView];
}

Gdy mamy stały rozmiar widoku głównego, musimy respektować jego granice po stronie JS. Innymi słowy, musimy zapewnić, że treść React Native mieści się w widoku głównym o stałym rozmiarze. Najprostszym sposobem jest użycie układu Flexbox. Jeśli używasz pozycjonowania absolutnego, a komponenty React są widoczne poza granicami widoku głównego, dojdzie do nakładania się na natywne widoki, powodując nieoczekiwane zachowania niektórych funkcji. Na przykład, 'TouchableHighlight' nie będzie podświetlał dotknięć poza granicami widoku głównego.

Można dynamicznie aktualizować rozmiar widoku głównego przez ponowne ustawienie jego właściwości frame. React Native zajmie się układem treści.

Treść React Native o elastycznym rozmiarze

W niektórych przypadkach chcemy renderować treść o początkowo nieznanym rozmiarze. Załóżmy, że rozmiar będzie definiowany dynamicznie w JS. Mamy dwa rozwiązania tego problemu.

  1. Możesz opakować swój widok React Native w komponent ScrollView. To gwarantuje, że twoja treść będzie zawsze dostępna i nie będzie nakładać się na natywne widoki.

  2. React Native pozwala określić w JS rozmiar aplikacji RN i przekazać go właścicielowi hostującego RCTRootView. Właściciel jest wtedy odpowiedzialny za ponowne rozmieszczenie widoków podrzędnych i utrzymanie spójności interfejsu. Osiągamy to za pomocą trybów elastyczności RCTRootView.

RCTRootView obsługuje 4 różne tryby elastyczności rozmiaru:

RCTRootView.h
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};

RCTRootViewSizeFlexibilityNone to wartość domyślna, która ustala stały rozmiar widoku głównego (ale nadal można go aktualizować przez setFrame:). Pozostałe trzy tryby pozwalają śledzić aktualizacje rozmiaru treści React Native. Na przykład, ustawienie trybu na RCTRootViewSizeFlexibilityHeight spowoduje, że React Native zmierzy wysokość treści i przekaże tę informację do delegata RCTRootView. W delegacie można wykonać dowolną akcję, w tym ustawienie ramki widoku głównego, aby treść się zmieściła. Delegat jest wywoływany tylko przy zmianie rozmiaru treści.

ostrzeżenie

Uczynienie wymiaru elastycznym zarówno w JS, jak i natywnie prowadzi do niezdefiniowanego zachowania. Na przykład - nie używaj elastycznej szerokości komponentu React najwyższego poziomu (z flexbox) podczas korzystania z RCTRootViewSizeFlexibilityWidth na hostującym RCTRootView.

Spójrzmy na przykład.

FlexibleSizeExampleView.m
- (instancetype)initWithFrame:(CGRect)frame
{
[...]

_rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"FlexibilityExampleApp"
initialProperties:@{}];

_rootView.delegate = self;
_rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
_rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
CGRect newFrame = rootView.frame;
newFrame.size = rootView.intrinsicContentSize;

rootView.frame = newFrame;
}

W przykładzie mamy widok FlexibleSizeExampleView przechowujący widok główny. Tworzymy widok główny, inicjujemy go i ustawiamy delegata. Delegat będzie obsługiwał aktualizacje rozmiaru. Następnie ustawiamy elastyczność rozmiaru widoku głównego na RCTRootViewSizeFlexibilityHeight, co oznacza, że metoda rootViewDidChangeIntrinsicSize: będzie wywoływana przy każdej zmianie wysokości treści React Native. Na koniec ustawiamy szerokość i pozycję widoku głównego. Zauważ, że ustawiamy tam również wysokość, ale nie ma to efektu, ponieważ wysokość zależy od RN.

Pełny kod źródłowy przykładu możesz sprawdzić tutaj.

Można dynamicznie zmieniać tryb elastyczności rozmiaru widoku głównego. Zmiana trybu zaplanuje ponowne obliczenie układu, a metoda delegata rootViewDidChangeIntrinsicSize: zostanie wywołana, gdy rozmiar treści będzie znany.

uwaga

Obliczanie układu React Native odbywa się w osobnym wątku, podczas gdy aktualizacje natywnego interfejsu użytkownika wykonuje się w wątku głównym. Może to powodować tymczasowe niespójności interfejsu między natywnym a React Native. To znany problem i nasz zespół pracuje nad synchronizacją aktualizacji interfejsu z różnych źródeł.

uwaga

React Native nie wykonuje żadnych obliczeń układu, dopóki widok główny nie stanie się widokiem podrzędnym innego widoku. Jeśli chcesz ukryć widok React Native, dopóki jego wymiary nie będą znane, dodaj widok główny jako widok podrzędny i ustaw go początkowo jako ukryty (użyj właściwości hidden klasy UIView). Następnie zmień jego widoczność w metodzie delegata.