- PagerDuty /
- Blog /
- Nicht kategorisiert /
- Erkenntnisse aus der Entwicklung eines zuverlässigen mobilen Builds
Blog
Erkenntnisse aus der Entwicklung eines zuverlässigen mobilen Builds
Die Ingenieure von PagerDuty legen größten Wert auf Zuverlässigkeit. Für sie ist es das Schlimmste, Kunden im Stich zu lassen, wenn sie gerufen wurden. Deshalb entwickeln und optimieren wir ständig unsere Systeme – einschließlich unserer mobilen Apps – um maximale Ausfallsicherheit zu gewährleisten.
Nach der Veröffentlichung von neu gestaltete mobile Anwendungen Seit Oktober letzten Jahres veröffentlichen wir neue Funktionen und Updates, die die Zuverlässigkeit dieser Apps verbessern. Aus Nutzersicht ist eine App, die ständig abstürzt oder sich aufhängt, fast genauso ärgerlich wie der vollständige Ausfall der zugrundeliegenden Dienste und APIs.
Nach einer kompletten Neuentwicklung der in unseren mobilen Apps eingebetteten JavaScript-Anwendung zeigte sich, dass unsere bisherige Vorgehensweise beim Erstellen und Verpacken unserer mobilen Anwendungen uns daran hinderte, unseren Nutzern schnell und sicher zuverlässige mobile Anwendungen bereitzustellen. Um voranzukommen, mussten wir unsere Vorgehensweise bei der Entwicklung unserer Android- und iOS-Apps überdenken.
Verwendung von Softwareartefakten zur Vereinfachung komplexer Builds
Die mobilen Apps von PagerDuty sind hybrid: Der Großteil des Codes, mit dem Nutzer interagieren, ist eine JavaScript-Anwendung innerhalb eines eingebetteten Browsers, die in den App Stores veröffentlicht wird. Dadurch konnten wir gleichzeitig neue Versionen der mobilen Anwendung für mehrere Plattformen mit umfangreicher Code-Wiederverwendung herausbringen. Wir nutzten das Apache Cordova-Projekt (möglicherweise unter dem Namen PhoneGap bekannt), um eine konsistente JavaScript-zu-native-Schnittstelle zu erhalten, die ungewöhnliche Probleme und Fehler bei unterschiedlichen Implementierungen eingebetteter Webansichten behebt. Cordova-Apps können außerdem eine Plugin-Bibliothek nutzen, die native Funktionen wie den Zugriff auf das Adressbuch bereitstellt.
Der Einstieg in die Entwicklung einer Cordova-App ist für jeden, der bereits Webanwendungen programmiert hat, unkompliziert. Die Integration einer bestehenden JavaScript-Anwendung in ein PhoneGap-Projekt gestaltet sich jedoch deutlich komplexer. Wir haben verschiedene Ansätze ausprobiert und konnten schließlich, indem wir uns an Softwareartefakten aus anderen Programmiersprachen (insbesondere Java) orientierten, eine weniger fehleranfällige Methode zum Erstellen unserer Apps entwickeln.
„Abhängigkeitshölle“ ist ein reales Problem für hybride Anwendungen
Cordova-Projekte sehen standardmäßig vor, dass Entwickler eine HTML-Datei im Verzeichnis „www“ ablegen, die als Einstiegspunkt der Anwendung dient. Dieser Code wird mithilfe verschiedener Skripte, die mit einem über die Kommandozeile erstellten Cordova-Projekt gebündelt sind, plattformübergreifend genutzt. Das ist eine beeindruckende Leistung des Cordova-Teams im Bereich Build-Engineering. Wird jedoch eine separate JavaScript-Anwendung eingebettet, die moderne Build-Tools wie Grunt oder Gulp verwendet und in einem separaten Quellcodeverwaltungssystem verwaltet wird, wird es schnell kompliziert.
Um einen Eindruck von der Komplexität zu bekommen, folgt hier ein Diagramm der mobilen Abhängigkeiten von PagerDuty für Drittanbietercode:
Hinzu kommt die Komplexität des Build-Tools selbst. Hier ist ein Diagramm der gulp.js-Pakete, die unser Standard-Gulp-Task zum Erstellen einer PagerDuty-App benötigt. Der Build-Prozess kompiliert (unter anderem) aus CoffeeScript, verknüpft Dateien und erstellt Frontend-Templates:
Diese Komplexität im Entwicklungsprozess ist dem Bedürfnis nach optimaler Performance geschuldet. Um eine brauchbare plattformübergreifende mobile App in JavaScript zu erstellen, sind spezialisierte Bibliotheken nahezu unerlässlich, um die extreme Gerätefragmentierung und diverse Plattformbesonderheiten zu bewältigen. Auch der JavaScript-Code selbst muss so angepasst werden, dass er in Browsern optimal läuft.
Letztendlich benötigen wir jedoch nur eine eigenständige index.html-Datei, die CSS, JavaScript, Bilder und Webfonts lädt. Sie funktioniert auf jedem statischen Webserver oder lokal im Dateisystem. Genau diese Datei möchten wir verpacken und an verschiedene Plattformen senden.
Diese Vorgehensweise beim Erstellen einer Javascript-Anwendung bietet uns die größte Flexibilität, aber wir mussten auch all diese Fragen beantworten:
-
Welche Bibliotheken sollte ich verwenden? (jQuery vs. Zepto, Hammer.js, Backbone vs. Ember vs. Angular vs. React)
-
Welche Template-Sprache sollte ich verwenden? (Handlebars vs. Underscore Templates 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 überhaupt in Javascript (Coffeescript, Dart, Typescript, Clojurescript) programmieren?
-
Wie werden Sie NPM einsetzen?
Die Auswahl ist schier unglaublich groß und spiegelt die Anarchie beim Entwickeln von Frontend-JavaScript-Anwendungen nach dem „modernen Weg“ wider. Wir haben dabei einiges gelernt, als wir versucht haben, eine so erstellte Anwendung in native mobile Anwendungen einzubinden.
Lektion 1: Bitte checken Sie transformierten JavaScript-Quellcode nicht in die Versionskontrolle ein.
Es ist schon lange bekannt, dass das Einchecken von Binärdateien in die Versionsverwaltung nicht optimal ist. Versionsverwaltung eignet sich am besten für Klartextdateien, die sich vorhersehbar ändern. Probleme treten auf, wenn Git versucht, einen Merge-Konflikt mit zwei zusammengefügten und minimierten JavaScript-Dateien zu lösen. Ständige Merge-Konflikte stören den Arbeitsablauf von Entwicklern erheblich (insbesondere in größeren Teams). Speicherplatz ist zwar günstig, aber für langlebige Repositories mit großen Mengen an JavaScript-Code ist er ungeeignet.
Diesen Rat kann man leicht ignorieren. Schaut man sich gängige JavaScript-Bibliotheken auf GitHub an, findet man dort meist einen Ordner namens „dist/“. Das ermöglicht zwar schnelle Iterationen, doch die Komplexität der Anwendungsentwicklung im Team mit Git und einem aufwendigen Build-Prozess wird schnell zur Belastung. Wir waren überzeugt, dass es einen besseren Weg geben muss.
Lektion 2: Nutzen Sie Werkzeuge, die Sie verstehen und beherrschen, aber kennen Sie auch deren Grenzen.
Uns wurde natürlich klar, dass das Einchecken unseres stark veränderten Codes in Git nicht funktionierte. Es gibt jedoch keinen allgemein anerkannten Build-Artefakt-Repository-Manager für Frontend-Entwickler. Ivy, Maven und Archiva kennen dieses Problem zwar, sind aber für JavaScript-Entwickler weder besonders geeignet noch besonders vertraut.
Es gab jedoch eine verlockende Alternative. Was wäre mit npm? Es hatte viele Vorteile, und wir verstanden seine Funktionsweise. Wenn wir eine package.json-Datei in unsere nativen Anwendungs-Repositories einfügten, genügte ein kurzer Befehl wie „npm install“, um unser mobiles Anwendungs-Repository (ein CommonJS-Repository) und alle Dutzenden von Abhängigkeiten, die zum Kompilieren benötigt wurden, abzurufen. Unsere package.json sah folgendermaßen aus:
Anstatt ein veröffentlichtes Paket zu verwenden, lud NPM unser Projekt aus einem privaten GitHub-Repository von einem bestimmten Commit herunter, sodass wir beliebig auf verschiedene Revisionen verweisen konnten. Anschließend führten wir ein weiteres Skript aus, das gulp.js startete und die Anwendung nach dem Abruf erstellte. Auf magische Weise eliminierten wir den gesamten eingecheckten, kompilierten und transformierten Code in GitHub. Wir mussten die Anwendung lediglich generieren, bevor wir sie in native Anwendungen einbinden konnten.
Lokal war es recht einfach, in Xcode oder Gradle einen Build-Schritt zu haben, der „npm install“ ausführte und einige Aufgaben in unserem JavaScript-Build-Tool erledigte. Das wurde jedoch problematisch, als unsere nativen Anwendungen in verschiedenen Umgebungen liefen. Wir injizierten die gesamte Komplexität und alle Feinheiten einer komplexen JavaScript-Anwendung in Umgebungen, die nur wenige statische Dateien benötigten. Xcode musste beispielsweise plötzlich (über ein Shell-Skript) wissen, wie CoffeeScript kompiliert wird. Um eine kontinuierliche Integration Bei der Arbeit mit TravisCI müssten wir außerdem Node, npm und alle anderen Build-Abhängigkeiten auf ihren Mac OS X-Images installieren.
NPM war so nah dran, und mit einem privat gehosteten NPM-Repository hätten wir es vielleicht geschafft. Da wir GitHub als Quelle für unser App-Paket nutzten, hätten wir die Komplexität unserer nativen Builds nur reduzieren können, indem wir den transformierten Quellcode überprüften. Wir brauchten etwas, das eine konkrete Vorstellung von Build-Artefakten lieferte.
Lektion 3: Fragen Sie sich: „Kennen wir die beste Vorgehensweise bereits?“ oder „Haben wir das schon woanders gelernt?“
Im September 2013 veröffentlichte GitHub seine Release-API. Diese ermöglicht es Entwicklern, Release-Objekte mit Tags in Git-Repositorys zu erstellen. Entscheidender ist jedoch, dass sie auch die Möglichkeit bietet, jedem Release eine beliebige Anzahl von Artefakten zuzuordnen. Anders ausgedrückt: Wir hatten ein Pseudo-Artefakt-Repository, das auf unserem bereits in unseren täglichen Arbeitsablauf integrierten Versionskontrollsystem aufbaute.
Mithilfe einiger einfacher Ruby- und Shell-Skripte (oder direkt über die GitHub-Benutzeroberfläche) konnten wir ein Tag erstellen, das auf einen bestimmten Commit verweist, aus diesem Tag eine neue Version erstellen und dann ein ZIP-Archiv mit dem gesamten HTML-, JavaScript- und CSS-Code hochladen, der unsere Webfonts ermöglicht.
Jede Plattform oder jedes System, das unsere App nutzt, müsste lediglich wissen, wie man Daten über HTTPS abruft und in der Lage sein, eine Javascript-Anwendung auszuführen.
Wir hätten zwar auch einen professionelleren Artefaktmanager wie Ivy oder Nexus verwenden können (oder die Artefakte sogar in einem Cloud-Speicher wie Box oder S3 speichern können), aber die GitHub-Integration hat es uns besonders leicht gemacht.
Die Anarchie von Javascript-Apps annehmen
Während NPM dieses Problem für serverseitiges JavaScript weitgehend gelöst hat, gibt es im Jahr 2014 immer noch keine allgemein akzeptierte und unterstützte Methode, um Frontend-JavaScript-Anwendungen für mehrere Plattformen zu verpacken. Es existiert kein wirkliches Äquivalent zu einem Java-JAR-Archiv. Ich halte das jedoch weder für problematisch noch wünschenswert. JavaScript-Bibliotheken und -Tools florieren im aktuellen System, und es wäre äußerst schwierig (wenn nicht gar unmöglich), die bestehende Community oder Standardisierungsgremien dazu zu bewegen, sich auf eine Art einheitlichen, 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 portabel. Jeder Browser lädt eine index.html-Datei. Wenn diese in einem ZIP-Archiv verpackt und in einem Repository (idealerweise einem für Build-Artefakte ausgelegten) gespeichert wird, können wir einen einfacheren und zuverlässigeren Build erstellen, der schnellere und besser planbare Releases ermöglicht.



