JavaScript est un langage qui a connu une évolution fulgurante et ce n’est pas près de s’arrêter. Mais à l’heure où nous parlons de frameworks ultra-modernes tels que React, Angular 2, Aurelia et de la norme ECMAScript 2017, il ne faut pas oublier qu’il existe toujours des missions et des projets portant sur AngularJS 1 ; que j’appellerais simplement “Angular” dans le reste de cet article.
Comme beaucoup de langages interprétés, Angular a besoin d’un écosystème particulier si on souhaite développer des applications de qualité professionnelle. Contrairement à des projets .NET, lorsqu’on démarre un projet Angular, on ne dispose pas de gestionnaire de configuration avec constantes prédéfinies, ni de tâches de construction et aucune manière de générer différents livrables, selon l’environnement de destination.
C’est cet aspect “packaging et livraison” que nous allons aborder aujourd’hui. Après avoir lu cet article, vous aurez connaissance d’un certain nombre d’outils qui vous permettront de livrer une application Angular de taille réduite, contenant un minimum de fichiers et prête à être déployée en production.
Gulp, encore et toujours
Nous allons utiliser Gulp tout au long de cet article. Gulp est un automatiseur de tâches (comme MSBuild ou Ant), basé sur Node.js, capable d’exécuter des opérations très variées que vous aurez définies. Quelques exemples de tâches :
- Minifier et concaténer du CSS ou du JavaScript,
- Copier des fichiers,
- Et bien plus encore
Il est préférable d’avoir un peu d’expérience avec Gulp afin de mieux appréhender les paragraphes qui suivent.
Annotation automatique des dépendences
Un des principes de base d’Angular est l’injection de dépendances. Le framework arrive à résoudre les dépendances requises en se basant sur le nom des variables injectées ($q, $http, etc.). C’est pourquoi il est nécessaire qu’il puisse toujours les résoudre une fois le code JavaScript minifié, grâce aux annotations :
angular.module('MyApp').controller('MyCtrl', ['$scope', '$timeout', '$http', function($scope, $timeout, $http) {
// ...
}]);
En plus d’être verbeuse, cette pratique est souvent source d’erreurs, car l’ajout ou le retrait de dépendances est très courant pendant le développement et on a vite fait de se tromper, ou tout simplement d’oublier. C’est là où gulp-ng-annotate intervient.
En ajoutant une instruction particulière dans votre code et en exécutant une tâche Gulp spécifique, les annotations seront automatiquement générées pendant le processus de packaging :
angular.module('MyApp').controller('MyCtrl', function($scope, $timeout, $http) {
'ngInject'; // <- Ligne à ajouter
// ...
});
var ngAnnotate = require('gulp-ng-annotate');
gulp.src('**/*.js')
.pipe(ngAnnotate()) // Ajout des annotations
.pipe(gulp.dest('./build/')); // Copie des fichiers modifiés dans un répertoire de sortie
Concaténation de templates Angular
Prenons comme exemple une application Angular qui contient 50 templates. Le navigateur du client pourrait potentiellement effectuer 50 requêtes HTTP afin de les récupérer. C’est énorme et de plus, chaque chargement de template pourrait ralentir l’application. Il est heureusement possible de concaténer tous les templates Angular HTML en un seul fichier JavaScript grâce au plugin Gulp gulp-angular-templatecache et son utilisation du service Angular $templateCache.
Petit rappel de l’utilité du service $templateCache, il s’agit du service qui va mettre en cache le contenu des templates Angular. On peut l’appeler directement :
angular.module('MyApp').run(function($templateCache) {
'ngInject';
// Équivaut à créer un fichier 'app/dashboard.html' avec le contenu passé en second argument
$templateCache.put('app/dashboard.html', '<section><h2>{{title}}</h2> ... </section>');
});
Voici comment utiliser le plugin gulp-angular-templatecache :
var templateCache = require('gulp-angular-templatecache');
gulp.src('templates/**/*.html')
.pipe(templateCache({module: 'MyApp'}))
.pipe(gulp.dest('./build/'));
Le résultat est donc un fichier JavaScript avec autant d’appels à $templateCache.put(…) qu’il y a de templates Angular.
Minification et concaténation de fichiers JavaScript et CSS
Les plugins Gulp gulp-uglify et gulp-clean-css permettent respectivement de minifier les fichiers JavaScript et CSS. Leur utilisation est très simple :
var uglify = require('gulp-uglify');
var cleanCss = require('gulp-clean-css');
gulp.src('**/*.js')
.pipe(uglify())
.pipe(gulp.dest('./build/'));
gulp.src('**/*.css')
.pipe(cleanCss())
.pipe(gulp.dest('./build/'));
Minification des templates HTML
La minification d’un fichier HTML consiste principalement à supprimer les espaces et sauts de lignes superflus. Le plugin Gulp gulp-htmlmin permet de réaliser cette tâche :
var htmlmin = require('gulp-htmlmin');
gulp.src('**/*.html')
.pipe(htmlmin({collapseWhitespace: true}))
.pipe(gulp.dest('./build/'));
Concaténer des fichiers référencés dans du code HTML
Le plugin Gulp gulp-useref est un peu plus compliqué que ceux vu précédemment. Il permet de concaténer du CSS ou du JavaScript référencé dans vos fichiers HTML. Avec un exemple, c’est plus clair :
<html>
<head>
<!-- build:css build/css/combined.css -->
<link href="src/css/one.css" rel="stylesheet">
<link href="src/css/two.css" rel="stylesheet">
<!-- endbuild -->
</head>
<body>
<!-- build:js build/scripts/combined.js -->
<script type="text/javascript" src="src/scripts/one.js"></script>
<script type="text/javascript" src="src/scripts/two.js"></script>
<!-- endbuild -->
</body>
</html>
Après utilisation du plugin, le résultat est le suivant :
<html>
<head>
<link rel="stylesheet" href="build/css/combined.css"/>
</head>
<body>
<script src="build/scripts/combined.js"></script>
</body>
</html>
Comme vous pouvez le voir, gulp-useref ne se contente pas de concaténer les fichiers, il met également à jour les fichiers HTML qui ont servi à retrouver les fichiers :
var useref = require('gulp-useref');
gulp.src('**/*.html')
// Recherche les fichiers à concaténer dans /src/**
// Le flux retourné contient tous les fichiers trouvés,
// et on peut enchaîner d'autres appels de plugins (tels que la minification)
// avant de les copier dans le répertoire de sortie !
// Ce sera le sujet du paragraphe suivant
.pipe(useref({ searchPath: './src/' }))
.pipe(gulp.dest('./build/'));
Filtrer des streams par type de fichier
Un stream – au sens Node JS – est un flux pouvant contenir des fichiers en mémoire, que l’on peut transformer avant de les écrire sur un disque.
gulp-useref, tel que montré dans le paragraphe précédent, retourne un stream contenant tous les fichiers qui seront concaténés, conformément aux instructions laissées dans l’HTML. Ce stream pourrait contenir, par exemple, du JavaScript et du CSS. Il serait intéressant de pouvoir “réduire” le stream temporairement à un type de fichier en particulier, de travailler sur ces fichiers, puis de les ré-intégrer au stream original.
C’est exactement la raison pour laquelle le plugin gulp-filter a été créé. Il s’utilise de la façon suivante :
var filter = require('gulp-filter');
var uglify = require('gulp-uglify');
var cleanCSS = require('gulp-clean-css');
var jsFilter = filter('**/*.js', {restore: true});
var cssFilter = filter('**/*.css', {restore: true});
gulp.src(['**/*.js', '**/*.css'])
// Ici, le flux pourrait provenir de gulp-useref
// Travaillons sur les fichiers JavaScript seulement
.pipe(jsFilter)
.pipe(uglify())
.pipe(jsFilter.restore)
// Ensuite sur les fichiers CSS
.pipe(cssFilter)
.pipe(cleanCss())
.pipe(cssFilter.restore)
// Copie de tous les fichiers dans le répertoire de sortie
.pipe(gulp.dest('./build/'));
Utiliser tous les plugins à la fois
Chaque plugin Gulp que nous avons vu a une fonction bien particulière, mais l’important ici est de réussir à les combiner dans un seul script afin d’obtenir notre application Angular packagée pour la production. Afin de ne pas recopier un gros pavé de code en cette fin d’article, je vous invite plutôt à regarder le dépôt Git qui contient le résultat : https://github.com/asimmon/Angular-Web-Starter.
Un fichier Readme.md récapitule une bonne partie des étapes qu’on aura vues jusqu’ici. Vous y trouverez aussi la commande qui permet de récupérer les packages Node.js et celle qui lance le serveur web : npm install et npm start. Bonne livraison !