Anfang dieses Jahres haben wir React Native für iOS eingeführt. React Native bringt das, was Entwickler von React im Web gewohnt sind — deklarative, in sich geschlossene UI-Komponenten und schnelle Entwicklungszyklen — auf die mobile Plattform, während die Geschwindigkeit, Wiedergabetreue und das Gefühl nativer Anwendungen erhalten bleiben. Heute freuen wir uns, React Native für Android zu veröffentlichen.
Bei Facebook verwenden wir React Native seit über einem Jahr in der Produktion. Vor fast genau einem Jahr machte sich unser Team daran, die Ads Manager App zu entwickeln. Unser Ziel war es, eine neue App zu erstellen, mit der Millionen von Menschen, die auf Facebook werben, ihre Konten verwalten und unterwegs neue Anzeigen erstellen können. Es war nicht nur die erste vollständig native App von Facebook, sondern auch die erste plattformübergreifende. In diesem Beitrag möchten wir Ihnen mitteilen, wie wir diese App erstellt haben, wie React Native es uns ermöglicht hat, schneller zu werden, und welche Lektionen wir gelernt haben.
Wahl von React Native
Vor nicht allzu langer Zeit war React Native noch eine neue Technologie, die sich in der Produktion nicht bewährt hatte. Während die Entwicklung einer neuen App, die auf dieser Technologie basiert, ein gewisses Risiko mit sich brachte, wurden die potenziellen Vorteile aufgewogen.
Zunächst war unser erstes Team von drei Produktingenieuren bereits mit React vertraut. Zweitens musste die App eine Menge komplexer Geschäftslogik enthalten, um Unterschiede in Anzeigenformaten, Zeitzonen, Datumsformaten, Währungen, Währungskonventionen usw. genau zu handhaben. Vieles davon wurde bereits in JavaScript geschrieben. Die Aussicht, all diesen Code in Objective-C zu schreiben, um ihn später in Java für die Android—Version der App zu schreiben, war weder ansprechend noch effizient. Drittens wäre es in React Native einfach, die meisten UI—Oberflächen zu implementieren, die wir erstellen wollten, und viele Daten in Form von Listen, Tabellen oder Diagrammen anzuzeigen. Produktingenieure konnten diese Ansichten sofort produktiv umsetzen, solange sie React kannten.Natürlich stellten einige Funktionen eine Herausforderung für diese neue Plattform dar — zum Beispiel der Bildeditor, mit dem Werbetreibende ein Foto zoomen und zuschneiden können, und die Kartenansicht, mit der Werbetreibende Personen in einem bestimmten Umkreis eines Standorts ansprechen können. Ein weiteres Beispiel ist die Breadcrumb-Navigation, mit der Werbetreibende die Hierarchie der Anzeigen in ihren Konten visualisieren können. Dies bot uns die Möglichkeit, die Plattform weiter voranzutreiben.
Zuerst den Anzeigenmanager für iOS erstellen
Unser Team hat beschlossen, zuerst eine iOS-Version der App zu entwickeln, die sehr gut mit React Native übereinstimmt, das auch zuerst für iOS entwickelt wird. In den folgenden Monaten haben wir das Team von drei auf acht Ingenieure vergrößert. Die neuen Mitarbeiter waren mit React nicht vertraut — und einige von ihnen waren nicht mit JavaScript vertraut -, aber sie waren bestrebt, ein großartiges mobiles Erlebnis für unsere Werbetreibenden zu schaffen, und sie stiegen schnell ein.
Erfahrene iOS-Ingenieure im React Native-Team halfen uns, Funktionen zu überbrücken, die in React Native noch nicht verfügbar waren, z. B. den Zugriff auf die Kamerarolle des Telefons. Facebook hat uns auch dabei geholfen, die App mit einigen der vorhandenen iOS-Bibliotheken von Facebook zu bündeln, die bereits in anderen Facebook-Apps verwendet wurden, um Authentifizierung, Analysen, Absturzberichte, Netzwerke und Push-Benachrichtigungen durchzuführen. Dadurch konnte sich unser Team darauf konzentrieren, nur das Produkt zu entwickeln.
Wie oben erwähnt, konnten wir viele unserer bereits vorhandenen JavaScript-Bibliotheken wiederverwenden. Eine solche Bibliothek ist Relay, Facebooks Framework zur Bereitstellung von Daten an React-Anwendungen über GraphQL. Eine weitere Reihe von Bibliotheken befasste sich mit Internationalisierung und Lokalisierung, was bei Zeitzonen und Währungen schwierig sein kann. Normalerweise laden diese Bibliotheken die richtige Konfiguration von einem JSON-Endpunkt auf der Website. Wir haben Skripte geschrieben, um die JSON-Dateien für alle unterstützten Gebietsschemas zu exportieren, die Dateien mithilfe der lokalisierten iOS-Bundles in die App aufgenommen und dann die JSON-Daten mit einigen Zeilen nativem Code für JavaScript verfügbar gemacht. Dadurch konnten unsere Bibliotheken nahezu unverändert arbeiten.
Eine der grösseren Herausforderungen, vor denen wir standen, waren die Navigationsflüsse. Für die Navigation in den vorhandenen Anzeigen und Kampagnen eines Werbetreibenden wollten wir eine Breadcrumb-Navigationsleiste. Für den Anzeigenerstellungsfluss benötigten wir eine Navigationsleiste im Assistentenstil. Darüber hinaus war es auch wichtig, die Übergangsanimationen und Berührungsgesten richtig zu machen, da sich die App sonst eher wie eine verherrlichte mobile Website als wie eine native App angefühlt hätte.
Unsere Lösung war die Navigator-Komponente, die zusammen mit React Native im Verzeichnis CustomComponents zur Verfügung gestellt wurde. Im Wesentlichen handelt es sich um eine Reaktionskomponente, die eine Reihe anderer Reaktionskomponenten in einem Stapel verfolgt. Es kann eine dieser Komponenten anzeigen und basierend auf Tastendrücken oder Berührungsgesten zwischen ihnen animieren. Es verfügt auch über eine steckbare Navigationsleistenkomponente, mit der wir eine iOS-ähnliche Navigationsleiste für die meisten regulären Ansichten, Breadcrumbs zum Navigieren in Anzeigen und Kampagnen und einen assistentenähnlichen Schritt für den Erstellungsfluss implementieren können. Die Navigationsleistenkomponente wird über den Fortschritt der Animation informiert und kann das erforderliche Animationsinkrement entsprechend ausführen. Dies bedeutet, dass alle Animationen, sowohl für die Ansichten als auch für die Navigationsleisten, in JavaScript berechnet werden, aber Tests haben gezeigt, dass wir sie immer noch mit 60 fps ausführen konnten.
Es gibt nur einen Weg, wie Navigationsanimationen stottern könnten, und das ist, wenn der JavaScript-Thread während einer großen Operation blockiert wurde. Als wir auf dieses Szenario stießen, lag es fast ausschließlich an der Verarbeitung großer Mengen neu abgerufener Daten. Natürlich ist es sinnvoll, dass beim Navigieren zu einer neuen Ansicht mehr Daten geladen und verarbeitet werden müssen. In einem ausreichend schnellen Netzwerk könnte dieser Prozess leicht eine noch laufende Navigationsanimation stören. Unsere Lösung bestand darin, die Datenverarbeitung mithilfe der InteractionManager-Komponente, die ebenfalls als Teil von React Native ausgeliefert wird, explizit zu verzögern, bis die Animationen abgeschlossen sind. Wir würden zuerst zu einer Ansicht animieren, die Platzhalter enthielt, und dann Relay die Datenverarbeitung durchführen lassen, wodurch automatisch die erforderlichen React-Komponenten neu gerendert wurden.
Versand einer Android-Version
Als Ads Manager für iOS kurz vor dem Versand stand, haben wir begonnen, eine Android-Version derselben App zu erstellen. Ein React Native-Port für Android schien der beste Weg zu sein, damit dies funktioniert. Glücklicherweise hat das React Native-Team bereits hart daran gearbeitet, genau das zu schaffen. Natürlich wollten wir so viel App-Code wie möglich wiederverwenden. Nicht nur die Geschäftslogik, sondern auch der UI-Code, da die meisten Ansichten bis auf ein gewisses Styling weitgehend identisch waren. Natürlich gab es Stellen, an denen die Android-Version anders aussehen und sich anders anfühlen musste als die iOS-Version, zum Beispiel in Bezug auf die Navigation oder die Verwendung nativer UI-Elemente für Datumsauswahl, Schalter usw.Glücklicherweise haben uns die Blocklistenfunktion des React Native Packagers und der Abstraktionsmechanismus von React sehr geholfen, die Wiederverwendung von Code auf den beiden Plattformen zu maximieren und die Notwendigkeit expliziter Plattformprüfungen zu minimieren. Unter iOS haben wir dem Packager mitgeteilt, dass er alle Dateien ignorieren soll, die mit enden .Android.js. Für die Android-Entwicklung wurden alle Dateien ignoriert, die auf .ios.js. Jetzt könnten wir dieselbe Komponente einmal für Android und einmal für iOS implementieren, während der verbrauchende Code die Plattform nicht berücksichtigt. Anstatt explizite if / else-Prüfungen für die Plattform einzuführen, haben wir versucht, plattformspezifische Teile der Benutzeroberfläche in separate Komponenten mit einer Android- und iOS-Implementierung umzugestalten. Zum Zeitpunkt der Auslieferung von Ads Manager für Android ergab dieser Ansatz eine Wiederverwendung von App-Code von rund 85 Prozent.
Eine größere Herausforderung war die Verwaltung des Quellcodes. Android- und iOS-Codebasen wurden in zwei verschiedenen Repositorys bei Facebook verwaltet. Der Quellcode für Ads Manager für iOS lebte natürlich im iOS-Repository, während der Code für die Android-Version aus verschiedenen Gründen im Android-Repository gespeichert werden musste. Zum Beispiel, ähnlich wie bei der iOS-Version, wollten wir einige der Android-Bibliotheken von Facebook verwenden, die im Android-Repository lebten. Darüber hinaus wurden alle Build-Tools, die Automatisierung und die kontinuierliche Integration für Android-Apps an das Android-Repository angeschlossen. Angesichts der Tatsache, dass der Android-Port der App ein Refactoring des vorhandenen iOS-Codes erforderte, um plattformspezifische Komponenten in ihre eigenen Dateien zu abstrahieren, hätten wir im Wesentlichen ständig zwei Versionen derselben Codebasis gegabelt und zusammengeführt. Das schien uns eine inakzeptable Situation zu sein.
Am Ende haben wir beschlossen, das iOS-Repository als Quelle der Wahrheit zu bezeichnen, vor allem, weil es bereits vorhanden war und die iOS-Version der App am ausgereiftesten war. Wir haben einen Cronjob eingerichtet, der den gesamten JavaScript-Code mehrmals täglich vom iOS- zum Android-Repository synchronisiert. Das Festschreiben von JavaScript in das Android-Repository wurde nicht empfohlen und war nur zulässig, wenn ein begleitendes Commit in das iOS-Repository folgte. Wenn das Synchronisierungsskript eine Diskrepanz feststellte, reichte es eine Aufgabe zur weiteren Untersuchung ein.
Wir haben es dem JavaScript Packager Server auch ermöglicht, Android-Code aus dem iOS-Repository auszuführen. Auf diese Weise konnten unsere Produktentwickler, die hauptsächlich JavaScript und keinen nativen Code berührten, ihre Änderungen sowohl auf iOS als auch auf Android direkt aus dem iOS-Repository heraus entwickeln und testen. Aber das erforderte immer noch, dass sie die nativen Teile der Android—App aus dem Android-Repository erstellt haben, und das gleiche für die iOS-App – eine große Steuer beim Testen von Änderungen auf zwei Plattformen. Um den Fluss für reine JavaScript-Entwickler zu beschleunigen, haben wir auch ein Skript erstellt, das die entsprechende native Binärdatei von unseren Continuous Integration-Servern heruntergeladen hat. Dies machte es für die meisten Entwickler unnötig, einen Klon des Android—Repositorys zu behalten – sie konnten ihre gesamte JavaScript-Entwicklung von der Quelle der Wahrheit im iOS-Repository aus durchführen und so schnell oder schneller iterieren als auf dem Web-Stack von Facebook.
Was wir gelernt haben
Das React Native-Team entwickelte die Plattform zusammen mit unserer App und stellte die nativen Komponenten und APIs zur Verfügung, die wir dafür benötigten. Diese Komponenten werden in Zukunft allen zugute kommen, die eine App erstellen. Selbst wenn wir ein paar Komponenten selbst aufbauen müssten, hätte sich die Verwendung von React Native gegenüber Pure Native gelohnt. Wir hätten diese Komponenten sowieso schreiben müssen, und sie wären wahrscheinlich nicht von anderen Teams auf der Straße wiederverwendbar gewesen.
Eine Lektion, die wir gelernt haben, war, dass es schwierig ist, über separate iOS- und Android-Code-Repositorys hinweg zu arbeiten, selbst mit vielen Tools und Automatisierung. Als wir die App entwickelten, verwendete Facebook dieses Modell, und alle unsere Build-Automatisierungs- und Entwicklerprozesse wurden um dieses Modell herum eingerichtet. Es funktioniert jedoch nicht gut für ein Produkt, das größtenteils über eine einzige gemeinsame JavaScript-Codebasis verfügt. Glücklicherweise wechselt Facebook für beide Plattformen zu einem einheitlichen Repository – nur eine Kopie des gängigen JavaScript-Codes ist erforderlich, und Synchronisierungen gehören der Vergangenheit an.
Eine weitere Lektion, die wir gelernt haben, betraf das Testen. Bei Änderungen muss jeder Ingenieur darauf achten, auf beiden Plattformen zu testen, und der Prozess ist anfällig für menschliche Fehler. Dies ist jedoch nur eine unvermeidliche Folge der Entwicklung einer plattformübergreifenden App aus derselben Codebasis. Die Kosten für ein gelegentliches Missgeschick aufgrund unzureichender Tests werden jedoch bei weitem durch die Entwicklungseffizienz aufgewogen, die durch die Verwendung von React Native und die Wiederverwendung von Code auf beiden Plattformen erzielt wird. Beachten Sie, dass diese Lektion nicht nur für Produktingenieure gilt; Es gilt auch für die React Native Platform Engineers, die in Objective-C und Java arbeiten. Ein Großteil der Arbeit dieser Ingenieure beschränkt sich nicht nur auf die jeweiligen Muttersprachen. Es kann sich auch auf JavaScript auswirken, z. B. Komponenten-APIs oder teilweise gemeinsam genutzte Implementierungen. Native iOS-Ingenieure sind es normalerweise nicht gewohnt, Änderungen auf Android testen zu müssen, und das Gegenteil gilt für Android-Ingenieure. Dies ist hauptsächlich eine kulturelle Lücke, deren Schließung Zeit und Mühe gekostet hat, und infolgedessen hat unsere Stabilität im Laufe der Zeit zugenommen.
Wir haben das Problem auch angegangen, indem wir Integrationstests erstellt haben, die bei jeder Revision ausgeführt werden. Während dies sofort funktionierte, um iOS-Probleme unter iOS und ebenfalls unter Android zu beheben, wurden unsere Continuous Integration-Systeme nicht für die Ausführung von Android-Tests unter iOS-Revisionen und umgekehrt eingerichtet. Dies erforderte technische Anstrengungen, und es gibt immer noch eine ausreichend große Fehlerquote, um die App gelegentlich zu beschädigen.
Letztendlich hat sich unsere Wette gelohnt — wir konnten Facebooks erste vollständig native React-App auf zwei Plattformen mit nativem Look and Feel ausliefern, die von demselben Team von JavaScript-Ingenieuren erstellt wurde. Nicht alle Ingenieure waren mit React vertraut, als sie dem Team beitraten, aber sie haben in nur fünf Monaten eine iOS-App mit nativem Look and Feel erstellt. Und nach weiteren drei Monaten haben wir die Android-Version der App veröffentlicht.
In dem Bemühen, in unserer Sprache inklusiver zu sein, haben wir diesen Beitrag bearbeitet, um die schwarze Liste durch eine Sperrliste zu ersetzen.