Leçons tirées de la création d'une version mobile fiable
Les ingénieurs de PagerDuty sont obsédés par la fiabilité. Laisser tomber les clients après avoir été appelés est le pire. Dans cet esprit, nous concevons et réfléchissons toujours à des moyens de maintenir et de créer des systèmes qui maximisent la résilience, y compris nos applications mobiles.
Après la sortie de applications mobiles repensées En octobre dernier, nous avons lancé de nouvelles fonctionnalités et mises à jour qui améliorent la fiabilité de ces applications. Du point de vue de l'utilisateur, une application qui plante ou se bloque régulièrement est presque aussi grave que les services et les API derrière l'application qui sont complètement indisponibles.
Après une réécriture totale de l'application Javascript intégrée à nos applications mobiles, la manière dont nous avons construit et emballé nos applications mobiles nous empêchait de proposer rapidement et en toute sécurité des applications mobiles fiables à nos utilisateurs. Pour aller de l'avant, nous avons dû repenser la manière dont nous créons nos applications Android et iOS.
Utilisation d'artefacts logiciels pour simplifier les builds complexes
Les applications mobiles de PagerDuty sont hybrides : la plupart du code avec lequel les utilisateurs interagissent est une application Javascript à l'intérieur d'un navigateur intégré publié sur les magasins d'applications. Cela nous a permis de publier simultanément une nouvelle version de l'application mobile pour plusieurs plates-formes avec une réutilisation importante du code. Nous avons utilisé le projet Apache Cordova (vous en avez peut-être entendu parler sous le nom de PhoneGap) pour nous fournir une interface JavaScript-native cohérente qui gère les problèmes et les bugs étranges avec différentes implémentations de vues Web intégrées. Les applications Cordova peuvent également exploiter une bibliothèque de plugins qui exposent des fonctionnalités natives comme l'accès au carnet d'adresses.
Démarrer avec une application Cordova est simple pour quiconque a déjà écrit une application Web. Mais la mécanique de l'empaquetage d'une application Javascript préexistante dans un projet PhoneGap est considérablement plus complexe. Nous avons essayé plusieurs approches différentes, mais en empruntant finalement l'idée d'artefacts logiciels à d'autres langages de programmation (notamment Java), nous avons pu créer une manière moins sujette aux erreurs de créer nos applications.
« L’enfer des dépendances » est une réalité pour les applications hybrides
Par défaut, les projets Cordova demandent aux développeurs de placer un fichier HTML dans un répertoire « www » qui est le point d'entrée de l'application. Ce code est partagé entre plusieurs plates-formes à l'aide de divers scripts regroupés avec un projet Cordova créé à partir de la ligne de commande. Il s'agit d'un effort impressionnant d'ingénierie de construction de la part de l'équipe Cordova. Cependant, lorsqu'une application Javascript distincte est intégrée, qui utilise des outils de construction modernes comme grunt ou gulp et réside dans un référentiel de contrôle de source distinct, les choses se compliquent rapidement.
Pour avoir une idée de la complexité, voici un diagramme des dépendances mobiles de PagerDuty pour le code tiers :
Il y a aussi la complexité de l'outil de construction lui-même. Voici un diagramme des packages gulp.js dont notre tâche gulp par défaut a besoin pour construire une application PagerDuty. Le processus de construction (entre autres choses) compile à partir de coffeescript, concatène les fichiers et crée des modèles front-end :
Cette complexité de la construction est motivée par le besoin d'optimiser les performances. Pour créer une application mobile multiplateforme décente en Javascript, il est presque indispensable de disposer de bibliothèques spécialisées pour gérer la fragmentation extrême des appareils et les différentes particularités de la plateforme. Le code Javascript lui-même doit être transformé pour fonctionner de manière optimale dans les navigateurs également.
En fin de compte, tout ce qui nous intéresse est un fichier index.html autonome qui charge les CSS, le JavaScript, les images et les polices Web. Il fonctionne sur n'importe quel serveur Web statique ou localement sur un système de fichiers. C'est ce que nous voulons empaqueter et envoyer à différentes plateformes :
Construire une application Javascript de cette manière nous donne le plus de flexibilité, mais nous avons également dû répondre à toutes ces questions :
-
Quelles bibliothèques dois-je utiliser ? (jQuery vs Zepto, Hammer.js, Backbone vs Ember vs Angular vs React)
-
Quel langage de création de modèles dois-je utiliser ? (modèles de guidon, de soulignement et de moustache)
-
Dois-je utiliser un gestionnaire de dépendances de bibliothèque comme bower.js ou component.js (ou simplement npm) ?
-
Quel outil de construction ? (gulp vs grunt vs rake vs ant vs browserify)
-
Quel framework de test ? (qunit vs jasmine vs tap)
-
Dois-je prendre la peine d'écrire en Javascript (coffeescript, dart, typescript, clojurescript) ?
-
Comment allez-vous utiliser NPM ?
C'est un choix incroyable et cela reflète l'anarchie de la création d'applications Javascript front-end « à la manière moderne ». Nous avons appris quelques choses en cours de route lorsque nous avons essayé d'injecter une application conçue de cette manière dans des applications mobiles natives.
Leçon 1 : Veuillez ne pas enregistrer la source Javascript transformée dans VCS
Nous savons depuis très longtemps qu'il n'est pas idéal de vérifier les binaires dans le contrôle de source. Le contrôle de source fonctionne mieux pour les fichiers en texte brut qui changent de manière prévisible et des problèmes se produisent lorsque git essaie de résoudre un conflit de fusion avec deux fichiers javascript concaténés et minifiés. Les conflits de fusion constants sont très perturbateurs pour le flux de travail des développeurs (en particulier pour les équipes plus importantes). L'espace disque est bon marché, mais pour les référentiels de longue durée avec de grandes quantités de code Javascript
Il est cependant facile d'ignorer ce conseil. Vérifiez n'importe quelle bibliothèque JavaScript populaire sur GitHub et vous y trouverez probablement une sorte de dossier « dist/ ». C'est un moyen rapide d'itérer, mais les subtilités du développement d'une application en équipe à l'aide de Git avec un processus de construction compliqué deviennent un fardeau. Nous avons pensé qu'il devait y avoir une meilleure façon de procéder.
Leçon 2 : Tirez parti des outils que vous comprenez et savez utiliser, mais comprenez leurs limites
Nous avons bien sûr réalisé que l'archivage de notre code lourdement transformé dans git ne fonctionnait pas. Mais il n'existe pas de véritable gestionnaire de référentiel d'artefacts de build que les développeurs front-end utilisent couramment. Ivy, Maven et Archiva connaissent très bien ce problème, mais ils ne sont pas bien adaptés ou particulièrement familiers à quelqu'un qui écrit du Javascript.
Il y avait cependant une alternative alléchante. Qu'en est-il de npm ? Il a beaucoup de bons atouts, nous avons compris comment il fonctionne. Si nous placions un fichier package.json dans nos référentiels d'applications natives, il suffirait d'une rapide « installation npm » pour récupérer notre référentiel d'applications mobiles (qui était un référentiel CommonJS) et toutes les dizaines de dépendances dont il avait besoin pour se construire. Notre package.json ressemblait exactement à ceci :
Au lieu de récupérer un package publié, NPM a récupéré notre projet à partir d'un dépôt GitHub privé lors d'un commit particulier, ce qui nous a permis de pointer arbitrairement vers différentes révisions. Nous avions ensuite un autre script qui exécutait gulp.js et construisait l'application après sa récupération. Comme par magie, nous avons éliminé tout le code compilé et transformé enregistré dans GitHub. Nous avions juste besoin de générer l'application avant de l'intégrer dans des applications natives.
Localement, il était assez facile d'avoir une étape de construction dans XCode ou Gradle qui exécutait « npm install » et exécutait quelques tâches dans notre outil de construction Javascript. Cela est devenu un problème lorsque nos applications natives ont commencé à s'exécuter dans des environnements différents. Nous avons injecté toute la complexité et les subtilités d'une application Javascript complexe dans des environnements qui n'avaient besoin que de quelques fichiers statiques. XCode, par exemple, avait soudainement besoin de savoir (via un script shell) comment compiler coffeescript. Intégration continue en travaillant avec TravisCI, nous devrions également installer node, npm et toutes les autres dépendances de build sur leurs images Mac OS X.
NPM s'en est tellement rapproché, et peut-être qu'avec un dépôt npm hébergé en privé, nous aurions pu le faire fonctionner. Cependant, en utilisant GitHub comme source pour notre package d'application, la seule façon de réduire la complexité de nos builds natifs aurait été de vérifier le code source transformé. Nous devions utiliser quelque chose qui avait une idée concrète des artefacts de build.
Leçon 3 : Demandez-vous : « Connaissons-nous déjà la meilleure pratique ? » ou « Avons-nous déjà appris cela ailleurs ? »
En septembre 2013, GitHub a publié son API Release. Elle permet aux développeurs de créer un objet release avec une balise dans le référentiel Git. Plus important encore, elle contient également le concept d'un nombre arbitraire d'artefacts associés à chaque release. En d'autres termes, nous avions un référentiel de pseudo-artefacts construit sur notre système de contrôle des révisions qui faisait déjà partie de notre flux de travail quotidien.
En utilisant quelques scripts Ruby et Shell simples (ou directement l'interface utilisateur Github), nous pourrions créer une balise pointant vers un commit particulier, créer une nouvelle version à partir de cette balise, puis télécharger une archive zip de tout le HTML, Javascript et CSS qui alimentaient nos polices Web.
Toute build ou plateforme qui consommerait notre application aurait seulement besoin de savoir comment récupérer des éléments via HTTPS et d'être capable d'exécuter une application Javascript.
Bien que nous aurions pu utiliser un gestionnaire d'artefacts plus professionnel comme Ivy ou Nexus (ou même stocker les artefacts dans une sorte de stockage cloud comme Box ou S3), l'intégration de GitHub a rendu la tâche particulièrement facile.
Accepter l'anarchie des applications Javascript
Même si NPM a largement résolu ce problème pour le Javascript côté serveur, nous sommes en 2014 et nous n'avons toujours pas de méthode largement acceptée et prise en charge pour empaqueter des applications Javascript front-end qui ciblent plusieurs plates-formes. Il n'existe pas de véritable équivalent à une archive JAR Java. Cependant, je ne pense pas que ce soit un problème ou même souhaitable. Les bibliothèques et outils Javascript prospèrent dans le système actuel et il serait incroyablement difficile (voire impossible) d'amener la communauté actuelle ou les organismes de normalisation à se regrouper autour d'une sorte de « norme d'application » unique et empaquetée qui fonctionne sur Android, iOS, Firefox OS, Chrome OS ou d'autres plates-formes.
L'idée d'une page HTML unique qui charge du Javascript, du CSS, des polices Web et des images est fondamentalement portable. N'importe quel navigateur charge un fichier index.html. Lorsqu'il est empaqueté dans une archive ZIP et stocké dans une sorte de référentiel (idéalement un référentiel conçu pour gérer les artefacts de build), nous pouvons créer une build plus simple et plus fiable qui nous permet de publier plus rapidement avec plus de prévisibilité.