Startseite / Artikel

Warum Codegen die Spezifikationsorientierung für React Native TurboModules unverzichtbar macht

Sehen Sie, wie React Native Codegen die TurboModule TypeScript-Spezifikation in einen Build-Zeit-Vertrag umwandelt, was schiefgeht, wenn man darauf verzichtet, und wo die Regel nicht mehr gilt.

Verlag: REACTAPP.TOP8 Min. Lesezeit1819 Wörter

Quellen: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It

Viele Teams, die React Native-Bibliotheken warten, legen in ihren Beitragsrichtlinien eine Regel fest: Man soll die TurboModule-Spezifikation ändern, bevor man irgendeinen nativen Code anfasst. Das klingt nach einer stilistischen Präferenz, einer Konvention, von der man hofft, dass die Mitwirkenden sie auch unter Zeitdruck einhalten. Bei der neuen Architektur ist das jedoch ganz anders. Codegen erzeugt die native Brücke aus der Spezifikation, sodass die Build-Prozesse selbst die Reihenfolge der Arbeiten bestimmen. Dieser Artikel erklärt, wie dieses Konzept durchgesetzt wird, was tatsächlich auf iOS und Android passiert, wenn jemand diesen Abkürzungsweg versucht, sowie welche Arten von Modulen davon ausgenommen sind.

Eine Konvention, die sich als Einschränkung erweist

Betrachten wir einen Wartungsbeauftragten, der beschließt, Zeit zu sparen: Zuerst fügt er die native Methode hinzu und kehrt anschließend zurück, um die Spezifikation zu aktualisieren. Bei einem von Codegen unterstützten Modul scheitert dieser Plan bereits, bevor er irgendeinen Nutzen bringt. Kein Linter oder Prüfer muss Einwände erheben; der Compiler sowie der generierte Code übernehmen diese Aufgabe. Die daraus resultierende Erkenntnis ist der wichtige Punkt: Die Regel war niemals eine Teamgewohnheit, die zufällig von der Toolchain toleriert wird. Es handelt sich vielmehr um eine Eigenschaft der Toolchain, die das Team zufällig aufgeschrieben hat.

Der Unterschied liegt nicht nur im Wortlaut. Eine Präferenz kann gegen einen Termin abgewogen werden. Ein Vertrag zur Build-Zeit hingegen nicht, und das Wissen darüber, mit welchem Vertrag man es zu tun hat, verändert die Art und Weise, wie man Änderungen an einer Bibliothek plant.

Die Spezifikationsdatei im Zentrum eines TurboModules

Jedes neue Architecture TurboModule verfügt über eine einzige TypeScript-Datei, üblicherweise src/Native<Name>.ts, in der jede Methode definiert wird, die die Grenze zwischen JavaScript und nativem Code überschreitet. Ein Wrapper um ein Social-Login-SDK könnte sie src/NativeNaverLogin.ts nennen; ein Wrapper für Bewertungen im In-App-Store könnte src/NativeInAppReview.ts verwenden. Ein Eintrag codegenConfig in der package.json-Datei des Pakets weist Codegen an, wo diese Datei zu finden ist und welche Art von Artefakt daraus generiert werden soll.

Als Regel lässt sich sagen, dass die Reihenfolge einfach ist: Jede Methode, die von JavaScript aufgerufen werden soll, wird zunächst in der Spezifikation definiert und erst danach im nativen Code implementiert. Zuerst die Spezifikation, dann der native Code – niemals umgekehrt. Was diese Reihenfolge unverhandelbar macht, ist die Mechanik, die die Spezifikation verarbeitet.

Wie Codegen die Spezifikation in native Anforderungen umwandelt

Codegen ist eine Transformation, die zur Zeit der Kompilierung ausgeführt wird. Sie analysiert die TypeScript-Spezifikation und erzeugt für jede Plattform eine Brücke:

  • Android: eine abstrakte Klasse, beispielsweise NativeNaverLoginSpec. Ihr Kotlin- oder Java-Modul erweitert diese generierte Klasse anstelle von ReactContextBaseJavaModule direkt, und jede Methode in der Spezifikation wird zu einer abstrakten Methode, die Sie implementieren müssen.
  • iOS: Objective-C++ JSI-Brücke, einschließlich einer Übersetzungseinheit NativeNaverLoginSpecJSI sowie eines Protokolls, dem Ihre .mm-Implementierung entsprechen muss.

Der entscheidende Aspekt ist die Richtung der Abhängigkeit. Der Native-Code beschreibt keine Brücke; er erbt vielmehr eine Brücke, die aus der Spezifikation generiert wurde. JavaScript erreicht wiederum den Native-Code nur über diese generierte Schnittstelle. Genau dieser Umstand bestimmt, was geschieht, wenn sich die beiden Seiten auf unterschiedliche Weise voneinander entfernen.

Eine Native-Methode, die in der Spezifikation nicht deklariert ist

Fügen Sie auf der Native-Seite eine Methode hinzu, ohne sie in die Spezifikation aufzunehmen, dann hat Codegen nichts dafür generiert. Es gibt weder eine abstrakte Methode zum Überschreiben auf Android noch einen Protokoll-Eintrag auf iOS.

Auf Android ist die gängigste Methode, eine solche Methode zu schreiben, die Verwendung von override; dies führt jedoch zu einem Kompilierfehler, da die generierte Elternklasse kein entsprechendes Mitglied besitzt. Selbst wenn Sie den Modifikator weglassen und die Datei kompiliert, gehört die Methode nicht zur generierten Schnittstelle, sodass JavaScript keinen Zugang zu ihr hat.

Auf iOS ist das Problem weniger auffällig. Die Methode existiert in Ihrer .mm-Datei und wird problemlos kompiliert, befindet sich jedoch nicht im Protokoll, das der Laufzeitumgebung zur Bereitstellung von Methoden dient, sodass JavaScript niemals darauf zugreifen kann. Eine stille, nutzlose Methode ist vermutlich schlimmer als ein Kompilierfehler: Es wird einem nichts mitgeteilt, dass sie fehlt, bis jemand sie aus JavaScript aufruft und feststellt, dass sie nicht vorhanden ist.

Eine Spezifikationsmethode ohne native Implementierung

Auch das Gegenteil funktioniert nicht richtig. Wenn die Spezifikation eine Methode definiert, diese aber auf der nativen Seite nicht implementiert wird, generiert Codegen dennoch die entsprechende Brückenmethode, und der JavaScript-Proxy stellt die Methode problemlos zur Verfügung.

Auf Android wird eine Klasse, die die generierte abstrakte Spezifikation erweitert, ohne eine ihrer abstrakten Methoden zu implementieren, vom Kotlin- oder Java-Kompilator abgelehnt – was der beste Fall ist, da der Fehler frühzeitig auftritt. Auf iOS führt das Fehlen einer erforderlichen Protokollmethode in der Regel nur zu einem Kompilierwarnhinweis; der eigentliche Fehler tritt zur Laufzeit als „unrecognised-selector“-Fehler auf, wenn JavaScript die Methode aufruft.

In jedem Fall werden die beiden Komponenten bei der Codegenerierung miteinander verknüpft. Man kann JavaScript keine neue Funktionalität verleihen, indem man nur eine Seite bearbeitet. Was einem im Weg steht, ist nicht ein Kommentar zur Überprüfung, sondern das Typensystem sowie die generierten Verknüpfungen, die nicht übereinstimmen.

Warum diese Regel auf jede Codegen-Bibliothek zutrifft

Falls „Spec-First“ lediglich ein Stilvorgabe wäre, würde sie nur in den Repositorien gelten, deren Betreuer sie gewählt haben. Doch die Durchsetzung erfolgt über Codegen, und Codegen gehört zu React Native. Jede Bibliothek, die mit einem codegenConfig-Block in der New Architecture Codegen ermöglicht, unterliegt denselben Regeln – unabhängig davon, ob ihre Entwickler jemals in Begriffen von „Spec-First“ gedacht haben oder nicht.

In der offiziellen React Native-Dokumentation spiegelt sich dies in der Formulierung wider. Bei der Beschreibung des Ausgabeverzeichnisses, in das Codegen schreibt, wird die generierte Abstraktklasse als etwas dargestellt, das ein Turbo Native Module implementieren muss. „Muss“ ist hier das entscheidende Wort: Es handelt sich um eine Voraussetzung, die bei der Kompilierung des Codes überprüft wird, nicht um eine Empfehlung. Community-Anleitungen beschreiben dieses Verhalten aus der Perspektive des Benutzers genauso: Sobald codegenConfig konfiguriert ist, erscheint eine generierte Native...Spec-Datei, deren Name dem entspricht, was codegenConfig in package.json angibt. Verschiedene Autoren, verschiedene Repositorien – derselbe Vertrag.

Eine nützliche Bestätigung kommt auch aus der entgegengesetzten Richtung. Wenn man das Feld codegenConfig gänzlich aus package.json weglässt, zeigt ein CocoaPods-Install auf iOS diesen Mangel an:

[Codegen] The "codegenConfig" field is not defined in package.json

Codegen überspringt in diesem Fall das gesamte Paket. Der Pipeline-Workflow ist so konzipiert, dass er an dieser Stelle stoppt, anstatt eine Lösung zu erfinden. Die Spezifikation ist kein optionaler Dokument, das veralten kann; sie ist vielmehr die Grundlage, um den gesamten Generierungsprozess zu organisieren.

Insgesamt ist das Konzept „Spezifikation zuerst“ eine strukturelle Folge der Art und Weise, wie die Pipeline ihre Eingaben verarbeitet. Bibliotheken befolgen dieses Prinzip, weil sie der Struktur von Codegen folgen. Eine Bibliothek, die dieses Prinzip zu ignorieren scheint, befindet sich entweder noch in der alten Architektur oder hat codegenConfig überhaupt nicht konfiguriert.

Wo der Vertrag nicht mehr gilt

Es ist zutreffend, dies als Vertrag innerhalb eines Toolchains-Ökosystems zu bezeichnen, doch diese Aussage lässt sich leicht übertrieben auslegen. Die Grenzen sollten daher klar definiert werden:

  • Legacy-Brückenmodule. Module, die auf der vor-JSI-Brücke basieren und auf iOS RCTBridgeModule sowie auf Android ReactContextBaseJavaModule verwenden, verfügen über keine Spezifikationen und stammen aus einer Zeit vor Codegen. Da es keine generierten Quelldaten gibt, wird die Reihenfolge durch nichts Mechanisches vorgegeben – Disziplin ist hier alles, was zählt.
  • Pakete ohne codegenConfig. Ohne diesen Block werden keine Artefakte erzeugt, und die Kette von der Spezifikation bis zur nativen Umsetzung beginnt nie.
  • Komponenten im Fabric-Modus. Komponenten, die mit type: „components“ deklariert werden, durchlaufen ebenfalls Codegen, weshalb man von einer ähnlichen Disziplin ausgehen kann. Allerdings basiert die obige Argumentation auf Turbo Native Modules (type: „modules“), und die von den Komponenten erzeugten Artefakte unterscheiden sich, daher sollte man den Fall der Komponenten eher als wahrscheinlich denn als etabliert betrachten und dies anhand der aktuellen Dokumentation für Ihre React Native-Version überprüfen.
  • Die eindeutig gültige Aussage ist begrenzt: Bei der neuen Architektur werden durch Codegen aus einer JavaScript- oder TypeScript-Spezifikation, die die offiziell dokumentierte Methode zur Erstellung darstellt, TurboModules erzeugt, wobei bei dieser Vorgehensweise die Spezifikation Vorrang hat und dies durch den Build durchgesetzt wird.

    Auch ist zu beachten, dass Codegen-basierte TurboModules heute nicht die einzige Möglichkeit sind, native Module zu schreiben. Alternativen wie Nitro Modules verwenden ihr eigenes Modell für Codegenerierung und Bindung; der Artikel zu wie Nitros statische Bindungen sich gegenüber TurboModules verhalten behandelt diesen Kompromiss.

    Der Arbeitsablauf, den der Build tatsächlich vorschreibt

    Sobald das Prinzip klar ist, hängt die Reihenfolge der Arbeiten nicht mehr vom Ermessen ab. Das Hinzufügen einer Methode zu einem TurboModule folgt einem bestimmten Ablauf:

    1. Ändern Sie die Spezifikation in src/Native<Name>.ts, um die neue Methode sowie ihre Typen anzugeben.
    2. Führen Sie Codegen aus: pod install erzeugt die iOS-Komponenten, und eine Gradle-Synchronisierung oder -Kompilierung erzeugt die Android-Artefakte.
  • Führen Sie die frisch generierten Signaturen auf jeder Plattform ein. Die Schritte für Android und iOS sind unabhängig und können parallel durchgeführt werden, beides hängt jedoch von Schritt 2 ab.
  • Exportieren Sie die Methode erneut über die öffentliche JavaScript-API der Bibliothek.
  • Daraus ergeben sich einige praktische Gewohnheiten:

    • Prüfen Sie Änderungen an den Spezifikationen genauso wie Änderungen an der API. Die Spezifikation stellt im Grunde den öffentlichen nativen Vertrag der Bibliothek dar, weshalb Änderungen an ihr denselben Prüfungsgrad wie Änderungen an exportierten JavaScript-Typen verdienen.
    • Erstellen Sie die Signaturen erneut, bevor Sie debuggen. Wenn eine native Methode nicht vorhanden zu sein scheint, liegt oft veralteter generierter Code von einem übersprungenen pod install oder einer fehlgeschlagenen Gradle-Synchronisierung vor.
    • Führen Sie die Builds für beide Plattformen im CI aus. Da iOS fehlerlos versagen kann, während Android deutlich Fehlermeldungen ausgibt, beweist ein erfolgreicher Android-Build allein nicht, dass die Brücke vollständig funktioniert.

    Zusammenfassung

    Das Wissen darüber, wer die regelbasierte Vorgehensweise bestimmt, ist keine Pedanterie. Sobald man erkennt, dass es sich um einen mechanischen Prozess handelt, behandelt man dies nicht mehr als Ratschlag, den im Hinblick auf Fristen abzuwägen. Es gibt keine Variante „nur dieses eine Mal“, bei der man zuerst die native Methode hinzufügt und die Spezifikation später korrigiert, denn dieser Abkürzungsweg führt nicht zu funktionsfähigem Code.

    • Codegen erzeugt die native Brücke aus der TypeScript-Spezifikation, sodass der Native-Code die Spezifikation umsetzt anstatt die Brücke zu definieren.
    • Jede Abweichung in eine der Richtungen führt zum Scheitern: manchmal zur Kompilierzeit, manchmal stillschweigend auf iOS, manchmal als Laufzeitschlussfehler.
    • Der Vertrag gilt für von Codegen unterstützte TurboModules; herkömmliche Brückenmodule sowie Pakete ohne codegenConfig fallen darunter nicht, und Fabric-Komponenten benötigen ihre eigene Überprüfung.
  • Behalten Sie im Leitfaden für Beiträge den Grundsatz „Zuerst die Spezifikationen“, formulieren Sie ihn jedoch als Beschreibung der Funktionsweise des Builds und nicht als Forderung nach gutem Verhalten. Das Build wird diesen Grundsatz unabhängig von dem, was im Dokument steht, durchsetzen.