Der Blog

Erkenntnisse aus der Erstellung eines zuverlässigen mobilen Builds

von Ton Smith 2. Mai 2014 | 9 min lesen

Die Ingenieure von PagerDuty legen großen Wert auf Zuverlässigkeit. Kunden im Stich zu lassen, wenn sie per Pager benachrichtigt werden, ist das Schlimmste. Vor diesem Hintergrund entwickeln und entwickeln wir ständig neue Möglichkeiten, Systeme zu warten und zu bauen, die die Ausfallsicherheit maximieren – einschließlich unserer mobilen Apps.

Nach der Veröffentlichung von neu gestaltete mobile Anwendungen Im vergangenen Oktober haben wir neue Funktionen und Updates ausgeliefert, die die Zuverlässigkeit dieser Apps verbessern. Aus der Sicht eines Benutzers ist eine App, die ständig abstürzt oder einfriert, fast so schlimm, wie wenn die Dienste und APIs hinter der App völlig nicht verfügbar sind.

Nach einer vollständigen Neuentwicklung der in unsere mobilen Apps eingebetteten Javascript-Anwendung hinderte uns die Art und Weise, wie wir unsere mobilen Anwendungen erstellten und verpackten, daran, unseren Benutzern schnell und sicher zuverlässige mobile Anwendungen bereitzustellen. Um voranzukommen, mussten wir die Art und Weise überdenken, wie wir unsere Android- und iOS-Apps erstellten.

Verwendung von Softwareartefakten zur Vereinfachung komplizierter Builds

Die mobilen Apps von PagerDuty sind hybrid – der meiste Code, mit dem Benutzer interagieren, ist eine Javascript-Anwendung innerhalb eines eingebetteten Browsers, der in den App Stores veröffentlicht wird. Dadurch konnten wir gleichzeitig eine neue Version der mobilen Anwendung für mehrere Plattformen mit erheblicher Codewiederverwendung veröffentlichen. Wir haben das Apache Cordova-Projekt genutzt (vielleicht haben Sie davon unter dem Namen PhoneGap gehört), um eine konsistente Javascript-zu-native-Schnittstelle bereitzustellen, die seltsame Probleme und Fehler bei unterschiedlichen Implementierungen eingebetteter Webansichten behebt. Cordova-Apps können auch eine Bibliothek von Plugins nutzen, die native Funktionen wie den Zugriff auf das Adressbuch bereitstellen.

Der Einstieg in eine Cordova-App ist für jeden, der schon einmal eine Webanwendung geschrieben hat, ganz einfach. Die Mechaniken beim Verpacken einer bereits vorhandenen Javascript-Anwendung in ein PhoneGap-Projekt sind jedoch erheblich komplexer. Wir haben mehrere verschiedene Ansätze ausprobiert, konnten aber letztendlich durch die Übernahme der Idee von Softwareartefakten aus anderen Programmiersprachen (insbesondere Java) eine weniger fehleranfällige Methode zum Erstellen unserer Apps entwickeln.

„Abhängigkeitshölle“ ist bei Hybridanwendungen eine reale Sache

Bei Cordova-Projekten legen Entwickler standardmäßig eine HTML-Datei in einem „www“-Verzeichnis ab, das den Einstiegspunkt der App darstellt. Dieser Code wird mithilfe verschiedener Skripte, die mit einem über die Befehlszeile erstellten Cordova-Projekt gebündelt sind, zwischen mehreren Plattformen gemeinsam genutzt. Dies ist eine beeindruckende Leistung des Cordova-Teams in Sachen Build-Engineering. Wenn jedoch eine separate Javascript-Anwendung eingebettet wird, die moderne Build-Tools wie Grunt oder Gulp verwendet und in einem separaten Quellcodeverwaltungs-Repository gespeichert ist, wird es schnell chaotisch.

Um eine Vorstellung von der Komplexität zu bekommen, finden Sie hier ein Diagramm der mobilen Abhängigkeiten von PagerDuty für Code von Drittanbietern:

mobile-web-dependencies

Hinzu kommt die Komplexität des Build-Tools selbst. Hier ist ein Diagramm der gulp.js-Pakete, die unsere Standard-Gulp-Aufgabe benötigt, um eine PagerDuty-App zu erstellen. Der Build-Prozess (unter anderem) kompiliert aus Coffeescript, verkettet Dateien und erstellt Front-End-Vorlagen:

gulp

Diese Komplexität des Builds wird durch die Notwendigkeit der besten Leistung getrieben. Um eine anständige plattformübergreifende mobile App in Javascript zu erstellen, sind fast zwingend spezielle Bibliotheken erforderlich, um mit extremer Gerätefragmentierung und verschiedenen Plattform-Macken umgehen zu können. Der Javascript-Code selbst muss transformiert werden, um auch in Browsern optimal zu laufen.

Letztendlich ist uns aber nur eine eigenständige index.html-Datei wichtig, die CSS, JavaScript, Bilder und Webfonts lädt. Sie funktioniert auf jedem statischen Webserver oder lokal auf einem Dateisystem. Das ist es, was wir verpacken und an verschiedene Plattformen senden möchten:

gulp-dependencies

Das Erstellen einer Javascript-Anwendung auf diese Weise bietet uns die größte Flexibilität, wir mussten jedoch auch alle diese Fragen beantworten:

  • Welche Bibliotheken sollte ich verwenden? (jQuery vs. Zepto, Hammer.js, Backbone vs. Ember vs. Angular vs. React)

  • Welche Vorlagensprache soll ich verwenden? (Handlebars- vs. Unterstrich-Vorlagen vs. Mustache)

  • Sollte ich einen Bibliotheksabhängigkeitsmanager wie bower.js oder component.js (oder einfach npm) verwenden?

  • Welches Build-Tool? (Gulp vs. Grunt vs. Rake vs. Ant vs. Browserify)

  • Welches Testframework? (Qunit vs. Jasmine vs. Tap)

  • Soll ich mir die Mühe machen, in Javascript (Coffeescript, Dart, Typescript, Clojurescript) zu schreiben?

  • Wie werden Sie NPM verwenden?

Es ist eine unglaubliche Auswahl und spiegelt die Anarchie der Entwicklung von Front-End-Javascript-Apps auf „moderne Weise“ wider. Wir haben dabei einiges gelernt, als wir versuchten, eine auf diese Weise erstellte App in native mobile Anwendungen einzufügen.

Lektion 1: Bitte checken Sie keine transformierten Javascript-Quellen in VCS ein

Wir wissen schon lange, dass es nicht ideal ist, Binärdateien in die Quellcodeverwaltung einzuchecken. Die Quellcodeverwaltung funktioniert am besten für reine Textdateien, die sich auf vorhersehbare Weise ändern, und es passieren schlimme Dinge, wenn Git versucht, einen Zusammenführungskonflikt mit zwei verketteten und minimierten JavaScript-Dateien zu lösen. Ständige Zusammenführungskonflikte stören den Arbeitsablauf der Entwickler sehr (insbesondere bei größeren Teams). Speicherplatz ist billig, aber für langlebige Repositories mit großen Mengen an JavaScript-Code

Es ist jedoch leicht, diesen Rat zu ignorieren. Schauen Sie sich jede beliebte JavaScript-Bibliothek auf GitHub an und Sie werden dort wahrscheinlich eine Art „dist/“-Ordner finden. Das ist eine schnelle Möglichkeit zum Iterieren, aber die Feinheiten der Entwicklung einer Anwendung in einem Team mit Git und einem komplizierten Build-Prozess werden zur Belastung. Wir dachten, es müsse einen besseren Weg geben.

Lektion 2: Nutzen Sie Tools, die Sie verstehen und deren Verwendung Sie beherrschen, aber kennen Sie deren Grenzen.

Wir haben natürlich festgestellt, dass das Einchecken unseres stark transformierten Codes in Git nicht funktionierte. Aber es gibt keinen echten Build-Artefakt-Repository-Manager, den Front-End-Entwickler üblicherweise verwenden. Ivy, Maven und Archiva sind mit diesem Problem sehr vertraut, aber sie sind für jemanden, der Javascript schreibt, nicht gut geeignet oder besonders vertraut.

Es gab jedoch eine verlockende Alternative. Was ist mit npm? Es hat viele tolle Dinge zu bieten, wir haben verstanden, wie es funktioniert. Wenn wir eine package.json-Datei in unsere nativen Anwendungs-Repositorys einfügen, ist nur eine schnelle „npm-Installation“ erforderlich, um unser mobiles Anwendungs-Repository (das ein CommonJS-Repository war) und alle Dutzenden von Abhängigkeiten abzurufen, die es zum Erstellen benötigte. Unser package.json sah genau so aus:

pagerduty_mobile_cordova-style

Anstatt ein veröffentlichtes Paket abzurufen, holte NPM unser Projekt bei einem bestimmten Commit aus einem privaten GitHub-Repository, sodass wir beliebig auf verschiedene Revisionen verweisen konnten. Wir hatten dann ein weiteres Skript, das gulp.js ausführte und die App erstellte, nachdem sie abgerufen worden war. Wie durch Zauberei eliminierten wir den gesamten eingecheckten, kompilierten und in GitHub transformierten Code. Wir mussten die App nur generieren, bevor wir sie in native Anwendungen bündelten.

Lokal war es einfach genug, einen Build-Schritt in XCode oder Gradle zu haben, der „npm install“ ausführte und einige Aufgaben in unserem Javascript-Build-Tool ausführte. Es wurde jedoch zu einem Problem, als unsere nativen Anwendungen in unterschiedlichen Umgebungen ausgeführt wurden. Wir haben die gesamte Komplexität und Feinheiten einer komplexen Javascript-Anwendung in Umgebungen eingebracht, die nur ein paar statische Dateien benötigten. XCode zum Beispiel musste plötzlich wissen (über ein Shell-Skript), wie Coffeescript kompiliert wird. Um ein kontinuierliche Integration Bei der Arbeit mit TravisCI müssten wir auch Node, NPM und alle anderen Build-Abhängigkeiten von ihren Mac OS X-Images installieren.

NPM kam dem so nahe, und vielleicht hätten wir es mit einem privat gehosteten NPM-Repository hinbekommen. Wenn wir GitHub als Quelle für unser App-Paket verwenden, hätten wir die Komplexität unserer nativen Builds nur reduzieren können, indem wir den transformierten Quellcode überprüft hätten. Wir mussten etwas verwenden, das eine konkrete Vorstellung von Build-Artefakten hatte.

Lektion 3: Fragen Sie: „Kennen wir die Best Practice bereits?“ oder „Haben wir das bereits woanders gelernt?“

Im September 2013 veröffentlichte GitHub seine Release-API. Sie ermöglicht es Entwicklern, ein Release-Objekt mit einem Tag im Git-Repository zu erstellen. Noch wichtiger ist, dass sie auch das Konzept oder eine beliebige Anzahl von Artefakten enthält, die mit jedem Release verknüpft sind. Mit anderen Worten: Wir hatten ein Pseudo-Artefakt-Repository, das auf unserem Revisionskontrollsystem aufgebaut war, das bereits Teil unseres täglichen Arbeitsablaufs war.

Mithilfe einiger einfacher Ruby- und Shell-Skripte (oder direkt der Github-Benutzeroberfläche) konnten wir ein Tag erstellen, das auf ein bestimmtes Commit verwies, aus diesem Tag eine neue Version erstellen und dann ein ZIP-Archiv mit dem gesamten HTML, Javascript und CSS hochladen, das unsere Webfonts antreibt.

Jeder Build oder jede Plattform, die unsere App nutzt, müsste nur wissen, wie Dinge über HTTPS abgerufen werden und in der Lage sein, eine Javascript-Anwendung auszuführen.

Wir hätten zwar einen professionelleren Artefakt-Manager wie Ivy oder Nexus verwenden können (oder die Artefakte sogar in einer Art Cloud-Speicher wie Box oder S3 speichern können), aber die GitHub-Integration machte es besonders einfach.

Die Anarchie von Javascript-Apps akzeptieren

Obwohl NPM dieses Problem für serverseitiges Javascript weitgehend gelöst hat, haben wir im Jahr 2014 immer noch keine allgemein akzeptierte und unterstützte Methode, um Front-End-Javascript-Anwendungen zu verpacken, die auf mehrere Plattformen abzielen. Es gibt kein echtes Äquivalent zu so etwas wie einem Java-JAR-Archiv. Ich denke jedoch nicht, dass das ein Problem oder gar wünschenswert ist. Javascript-Bibliotheken und -Tools florieren im aktuellen System und es wäre unglaublich schwierig (wenn nicht unmöglich), die aktuelle Community oder Standardisierungsgremien dazu zu bringen, sich auf eine Art einzigen, verpackten „App-Standard“ zu einigen, der unter Android, iOS, Firefox OS, Chrome OS oder anderen Plattformen läuft.

Die Idee einer einzelnen HTML-Seite, die Javascript, CSS, Webfonts und Bilder lädt, ist grundsätzlich portierbar. Jeder Browser lädt eine index.html-Datei. Wenn sie in einem ZIP-Archiv verpackt und in einer Art Repository gespeichert wird (idealerweise einem, das für die Verarbeitung von Build-Artefakten ausgelegt ist), können wir einen einfacheren, zuverlässigeren Build erstellen, der uns eine schnellere und vorhersehbarere Veröffentlichung ermöglicht.