Dlaczego Codegen czyni podejście oparte na specyfikacji obowiązkowym w React Native TurboModules
Zobacz, jak React Native Codegen przekształca specyfikację TurboModule w języku TypeScript na kontrakt używany podczas budowania, co się psuje, gdy go pominiesz, oraz gdzie reguła przestaje obowiązywać.
Źródła: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It
Wiele zespołów, które utrzymują biblioteki React Native, ma w swoich przewodnikach dla współpracowników zasadę: należy zmienić specyfikację TurboModule przed dotknięciem jakiegokolwiek kodu natywnego. Brzmi to jak preferencja stylistyczna, taka konwencja, od której oczekuje się, że współpracownicy będą jej przestrzegać nawet pod presją terminów. W przypadku nowej architektury nie jest to wcale tak. Codegen generuje most natywny na podstawie specyfikacji, więc to sam proces budowania określa kolejność działań. Ten artykuł wyjaśnia, w jaki sposób egzekwowany jest ten kontrakt, co tak naprawdę dzieje się na iOS i Android, gdy ktoś próbuje skrótu, oraz jakie rodzaje modułów są poza jego zasięgiem.
Konwencja, która okazuje się ograniczeniem
Rozważmy osobę odpowiedzialną za utrzymanie, która postanawia zaoszczędzić czas: najpierw dodaje metodę natywną, a potem wraca i aktualizuje specyfikację. W module opartym na Codegen ten plan zawodzi już na wczesnym etapie, zanim może przynieść jakąkolwiek korzyść. Nie trzeba, aby linter czy recenzent się sprzeciwiali – kompilator i wygenerowany kod to załatwiają. Kluczowym elementem jest uświadomienie sobie, że ta zasada nigdy nie była zwyczajem zespołu, który akurat jest tolerowany przez narzędzia. To właściwość samego zestawu narzędzi, którą zespół akurat zapisał.
Różnica wykracza poza sam sformułowanie. Preferencje można pogodzić z terminem realizacji, natomiast umowa z czasu budowania nie daje takiej możliwości, a świadomość tego, z czym mamy do czynienia, zmienia sposób planowania zmian w bibliotece.
Plik specyfikacji w centrum TurboModule
Każdy nowy TurboModule w architekturze posiada jeden plik typu TypeScript, zwykle o nazwie src/Native<Name>>.ts, który deklaruje każdą metodę przekraczającą granicę pomiędzy JavaScript a kodem natywnym. Wrapper otaczający SDK do logowania w mediach społecznościowych może mieć nazwę src/NativeNaverLogin.ts; wrapper do recenzji w sklepie aplikacji może używać nazwy src/NativeInAppReview.ts. Wskaźnik codegenConfig w pliku package.json pakietu informuje Codegen, gdzie znaleźć ten plik oraz jaki rodzaj artefaktu ma zostać z niego wygenerowany.
Jako zasada, kolejność jest prosta: każda metoda, którą JavaScript powinien móc wywołać, jest najpierw deklarowana w specyfikacji, a dopiero potem implementowana w formie kodu natywnego. Najpierw specyfikacja, potem kod natywny – nigdy odwrotnie. To, co sprawia, że ta kolejność jest niepodważalna, to mechanizm, który przetwarza specyfikację.
Jak Codegen przekształca specyfikację w wymagania typu native
Codegen to transformacja, która odbywa się w czasie budowania. Analizuje specyfikację TypeScript i tworzy odpowiednią warstwę pośredniczącą dla każdej platformy:
- Android: klasa abstrakcyjna, na przykład
NativeNaverLoginSpec. Twój moduł napisany w Kotlin lub Java rozszerza tę wygenerowaną klasę zamiast bezpośrednioReactContextBaseJavaModule, a każda metoda z specyfikacji staje się metodą abstrakcyjną, którą musisz zaimplementować. - iOS: narzędzie JSI w języku Objective-C++, obejmujące jednostkę tłumaczeniową
NativeNaverLoginSpecJSIoraz protokół, któremu musi odpowiadać twoja implementacja w formacie.mm.
Kluczowym elementem jest kierunek zależności. Kod natywny nie opisuje mostu; dziedziczy most wygenerowany na podstawie specyfikacji. JavaScript z kolei może dotrzeć do kodu natywnego jedynie przez tę wygenerowaną warstwę. To właśnie ten fakt decyduje o tym, co dzieje się w obu przypadkach, gdy strony mogą się od siebie oddalić.
Metoda natywna, której nie deklaruje specyfikacja
Jeśli dodasz metodę po stronie natywnej, nie dodając jej do specyfikacji, Codegen nic dla niej nie wygeneruje. Na Androidzie nie ma abstrakcyjnej metody do przejęcia, a na iOS brakuje wpisu protokołu.
Na Androidzie najczęstszym sposobem napisania takiej metody jest użycie słowa kluczowego override, co powoduje błąd kompilacji, ponieważ wygenerowana klasa nadrzędna nie posiada odpowiadającego elementu. Nawet jeśli usuniesz ten modyfikator i plik się skompiluje, metoda nie jest częścią wygenerowanej warstwy, więc JavaScript nie ma do niej dostępu.
W systemie iOS błąd jest mniej widoczny. Metoda istnieje w pliku .mm i kompiluje się bez problemów, ale nie znajduje się w protokole używanym przez silnik w czasie wykonywania do eksponowania metod, więc JavaScript nigdy nie może do niej dotrzeć. Taka niewidoczna, bezużyteczna metoda jest chyba gorsza od błędu kompilacji – nic nie wskazuje na to, że coś jest nie tak, dopóki ktoś nie wywoła jej z JavaScripta i odkryje, że jej brakuje.
Metoda z specyfikacji bez implementacji w języku natywnym
Odwrotny scenariusz również kończy się niepowodzeniem. Jeśli specyfikacja deklaruje metodę, ale strona języka natywnego jej nie implementuje, Codegen nadal generuje odpowiedni most komunikacyjny, a proxy w JavaScriptie z przyjemnością eksponuje tę metodę.
W systemie Android klasa, która rozszerza wygenerowany abstrakcyjny szablon, nie implementując żadnej z jego metod abstrakcyjnych, jest odrzucana przez kompilator Kotlin lub Java – co stanowi najlepszy scenariusz, ponieważ błąd zostaje wykryty wcześnie. W systemie iOS brak wymaganej metody protokołu zazwyczaj powoduje jedynie ostrzeżenie kompilatora; prawdziwy błąd pojawia się w czasie wykonywania jako awaria typu unrecognised-selector, gdy JavaScript próbuje wywołać tę metodę.
Niezależnie od sytuacji obie części są ze sobą powiązane podczas generowania kodu. Nie można nadać JavaScript nowej funkcjonalności, edytując tylko jedną z nich. Przeszkodą nie jest komentarz recenzenta, lecz system typów oraz wygenerowany „klej”, który nie chce się dopasować.
Dlaczego ta zasada dotyczy każdej biblioteki Codegen
Gdyby podejście „spec-first” było jedynie stylem określonym przez konkretną społeczność, obowiązywałoby ono tylko w repozytoriach, których administratorzy je wybrali. Jednak egzekwowanie tego zasady odbywa się za pośrednictwem Codegen, a Codegen należy do React Native. Każda biblioteka, która umożliwia użycie Codegen za pomocą bloku codegenConfig w ramach nowej architektury, podlega tym samym zasadom, niezależnie od tego, czy jej twórcy kiedykolwiek myśleli w kategoriach „spec first”, czy nie.
Oficjalna dokumentacja React Native odzwierciedla to w swoim sformułowaniu. Opisując folder wyjściowy, do którego zapisuje się kod generowany przez Codegen, przedstawia stworzoną klasę abstrakcyjną jako coś, co moduł Turbo Native musi zaimplementować. Słowo „musi” jest kluczowe: chodzi o obowiązek sprawdzany podczas kompilacji kodu, a nie o zalecenie. Przewodniki społeczności opisują to samo zachowanie z zewnątrz: po skonfigurowaniu codegenConfig pojawia się utworzony plik Native...Spec, nazwany zgodnie z tym, co określa codegenConfig w pliku package.json. Różni autorzy, różne repozytoria – identyczna specyfikacja.
Istnieje również przydatne potwierdzenie z przeciwnego kierunku. Jeśli całkowicie pominąć pole codegenConfig w pliku package.json, uruchomienie instalacji CocoaPods na iOS wykryje tę brakującą informację:
[Codegen] The "codegenConfig" field is not defined in package.json
Codegen pomija wtedy całkowicie ten pakiet. Proces generowania jest zaprojektowany tak, by się zatrzymać, zamiast próbować znaleźć rozwiązanie pośrednie. Specyfikacja nie jest dokumentem opcjonalnym, który może stać się przestarzały; to właśnie ona stanowi podstawę całego procesu generowania.
Podsumowując, podejście „specyfikacja na pierwszym miejscu” jest konsekwencją strukturalną sposobu, w jaki proces generowania odczytuje swoje dane wejściowe. Biblioteki go przestrzegają, ponieważ są zgodne ze strukturą Codegen. Biblioteka, która wydaje się go ignorować, albo nadal korzysta z architektury starszej generacji, albo w ogóle nie skonfigurowała codegenConfig.
Gdzie umowa przestaje obowiązywać
Określenie tego jako umowy w ramach zestawu narzędzi jest dokładne, ale łatwo przesadzić z tym stwierdzeniem. Granice tego rozumienia warto jasno określić:
- Moduły mostkowe z przeszłości. Moduły stworzone na bazie mostka przed JSI, wykorzystujące
RCTBridgeModulew iOS lubReactContextBaseJavaModulew Android, bez żadnych specyfikacji, istniały przed Codegenem. Nie mają one generowanego źródła prawdy, więc żaden mechanizm nie gwarantuje zachowania określonej kolejności. Jedyne, co może to kontrolować, to dyscyplina. - Pakiety bez
codegenConfig. Bez tego bloku nie są generowane żadne pliki, a łańcuch od specyfikacji do wymogów w języku natywnym nigdy nie zostaje uruchomiony.
type: „components” również przechodzą przez Codegen, więc można oczekiwać podobnej dyscypliny. Jednak powyższe rozumowanie opiera się na Turbo Native Modules (type: „modules”), a generowane pliki dla komponentów różnią się, więc należy traktować ten przypadek jako prawdopodobny, a nie ustalony, i sprawdzić go w aktualnej dokumentacji swojej wersji React Native.Twierdzenie, które pozostaje mocne, jest wąskie: w przypadku New Architecture TurboModules generowanych przez Codegen na podstawie specyfikacji w JavaScript lub TypeScript – co jest oficjalnie dokumentowanym sposobem ich tworzenia – zasada specyfikacji na pierwszym miejscu jest egzekwowana podczas procesu budowania.
Warto również zaznaczyć, że TurboModules oparte na Codegen nie są jedynym sposobem na tworzenie modułów natywnych obecnie. Alternatywy takie jak Nitro Modules wykorzystują własny model generowania kodu i łączenia; artykuł na temat jak statyczne powiązania w Nitro porównują się z TurboModules omawia ten kompromis.
Proces pracy, który faktycznie jest wymuszany
Gdy mechanizm staje się jasny, kolejność działań przestaje być kwestią subiektywnej oceny. Dodawanie metody do TurboModule odbywa się według określonego schematu:
- Zmodyfikuj specyfikację w pliku
src/Native<Name>.ts, aby zadeklarować nową metodę oraz jej typy. - Zrób użytek z Codegen: polecenie
pod installregeneruje pliki niezbędne dla iOS, a synchronizacja lub budowa w Gradle tworzy pliki potrzebne dla Androida.
Z tego wynikają kilka praktycznych zasad:
- Przeglądaj zmiany w specyfikacji wraz ze zmianami w API. Specyfikacja stanowi w rzeczywistości publiczną umowę między użytkownikiem a biblioteką, więc zmiany w niej wymagają takiej samej uwagi jak zmiany w eksportowanych typach JavaScript.
- Regeneruj kod przed debugowaniem. Gdy wydaje się, że metoda natywna nie istnieje, częstą przyczyną jest przestarzały kod wygenerowany po pominięciu kroku
pod installlub synchronizacji Gradle. - Zawieraj budowanie na obu platformach w środowisku CI. Ponieważ iOS może zawieść się bez żadnych objawów, podczas gdy Android daje wyraźne błędy, sam udany proces budowania na Androidzie nie dowodzi, że most komunikacyjny jest kompletny.
Zakończenie
Wiedza o tym, kto „włada” zasadą priorytetu specyfikacji, nie jest pedanterią. Gdy zrozumiesz, że chodzi o aspekt mechaniczny, przestaniesz traktować to jako radę do rozważenia w kontekście terminu realizacji. Nie istnieje wersja „tylko tym razem” polegająca na najpierw dodaniu metody natywnej, a późniejszym poprawieniu specyfikacji, ponieważ taka skrótka droga nie prowadzi do działającego kodu.
- Codegen generuje mostek natywny na podstawie specyfikacji TypeScript, więc kod natywny implementuje tę specyfikację, zamiast definiować mostek.
- Jakikolwiek odchylenie w dowolnym kierunku kończy się niepowodzeniem: czasami podczas kompilacji, czasami w tle na iOS, a czasami jako awaria w czasie wykonywania.
- Umowa obejmuje TurboModules wspierane przez Codegen; starsze moduły mostkowe oraz pakiety bez
codegenConfigpozostają poza jej zasięgiem, a komponenty Fabric wymagają własnej weryfikacji.