Gulp : l'arme secrète d'un développeur Web pour optimiser la vitesse du site

Publié: 2022-03-11

Beaucoup d'entre nous doivent gérer des projets Web qui sont utilisés dans la production, qui fournissent divers services au public. Lorsque l'on traite de tels projets, il est important de pouvoir construire et déployer notre code rapidement. Faire quelque chose rapidement conduit souvent à des erreurs, surtout si un processus est répétitif, c'est donc une bonne pratique d'automatiser un tel processus autant que possible.

Gulp : l'arme secrète d'un développeur Web pour optimiser la vitesse du site

Chers collègues développeurs : il n'y a aucune excuse pour envoyer du courrier indésirable à votre navigateur.
Tweeter

Dans cet article, nous examinerons un outil qui peut faire partie de ce qui nous permettra de réaliser une telle automatisation. Cet outil est un package npm appelé Gulp.js. Afin de vous familiariser avec la terminologie de base de Gulp.js utilisée dans cet article, veuillez vous référer à "Une introduction à l'automatisation JavaScript avec Gulp" qui a été précédemment publiée sur le blog par Antonios Minas, l'un de nos collègues développeurs Toptal. Nous supposerons une connaissance de base de l'environnement npm, car il est largement utilisé tout au long de cet article pour installer des packages.

Servir les ressources frontales

Avant de continuer, revenons un peu en arrière pour avoir un aperçu du problème que Gulp.js peut résoudre pour nous. De nombreux projets Web comportent des fichiers JavaScript frontaux qui sont servis au client afin de fournir diverses fonctionnalités à la page Web. Habituellement, il existe également un ensemble de feuilles de style CSS qui sont également fournies au client. Parfois, en regardant le code source d'un site Web ou d'une application Web, nous pouvons voir un code comme celui-ci :

 <link href="css/main.css" rel="stylesheet"> <link href="css/custom.css" rel="stylesheet"> <script src="js/jquery.min.js"></script> <script src="js/site.js"></script> <script src="js/module1.js"></script> <script src="js/module2.js"></script>

Il y a quelques problèmes avec ce code. Il contient des références à deux feuilles de style CSS distinctes et à quatre fichiers JavaScript distincts. Cela signifie que le serveur doit faire un total de six requêtes au serveur, et chaque requête doit charger séparément une ressource avant que la page ne soit prête. C'est moins un problème avec HTTP/2 car HTTP/2 introduit le parallélisme et la compression des en-têtes, mais c'est toujours un problème. Cela augmente le volume total de trafic requis pour charger cette page et réduit la qualité de l'expérience utilisateur car le chargement des fichiers prend plus de temps. Dans le cas de HTTP 1.1, il monopolise également le réseau et réduit le nombre de canaux de requête disponibles. Il aurait été bien préférable de combiner les fichiers CSS et JavaScript en un seul ensemble pour chacun. De cette façon, il n'y aurait qu'un total de deux demandes. Il aurait également été agréable de proposer des versions réduites de ces fichiers, qui sont généralement beaucoup plus petites que les originaux. Notre application Web pourrait également tomber en panne si l'un des actifs est mis en cache, et le client recevrait une version obsolète.

Surcharge

Une approche primitive pour résoudre certains de ces problèmes consiste à combiner manuellement chaque type d'actif dans un ensemble à l'aide d'un éditeur de texte, puis à exécuter le résultat via un service de minification, tel que http://jscompress.com/. Cela s'avère très fastidieux à faire en continu pendant le processus de développement. Une amélioration légère mais discutable serait d'héberger notre propre serveur de minificateur, en utilisant l'un des packages disponibles sur GitHub. Ensuite, nous pourrions faire des choses qui ressembleraient un peu à ce qui suit :

 <script src="min/f=js/site.js,js/module1.js"></script>

Cela servirait des fichiers minifiés à notre client, mais cela ne résoudrait pas le problème de la mise en cache. Cela entraînerait également une charge supplémentaire sur le serveur puisque notre serveur devrait essentiellement concaténer et minifier tous les fichiers source de manière répétitive à chaque demande.

Automatiser avec Gulp.js

Nous pouvons sûrement faire mieux que l'une ou l'autre de ces deux approches. Ce que nous voulons vraiment, c'est automatiser le regroupement et l'inclure dans la phase de construction de notre projet. Nous voulons nous retrouver avec des ensembles d'actifs pré-construits qui sont déjà minifiés et prêts à servir. Nous souhaitons également forcer le client à recevoir les versions les plus récentes de nos actifs groupés à chaque demande, mais nous souhaitons toujours tirer parti de la mise en cache si possible. Heureusement pour nous, Gulp.js peut gérer cela. Dans le reste de l'article, nous allons construire une solution qui tirera parti de la puissance de Gulp.js pour concaténer et minifier les fichiers. Nous utiliserons également un plugin pour casser le cache lorsqu'il y aura des mises à jour.

Nous allons créer le répertoire et la structure de fichiers suivants dans notre exemple :

 public/ |- build/ |- js/ |- bundle-{hash}.js |- css/ |- stylesheet-{hash}.css assets/ |- js/ |- vendor/ |- jquery.js |- site.js |- module1.js |- module2.js |- css/ |- main.css |- custom.css gulpfile.js package.json
npm fait de la gestion des packages dans les projets Node.js un vrai bonheur. Gulp offre une énorme extensibilité en tirant parti de l'approche de packaging simple de npm pour fournir des plugins modulaires et puissants.

Le fichier gulpfile.js est l'endroit où nous définirons les tâches que Gulp effectuera pour nous. Le package.json est utilisé par npm pour définir le package de notre application et suivre les dépendances que nous allons installer. Le répertoire public est ce qu'il faut configurer pour faire face au web. Le répertoire assets est l'endroit où nous allons stocker nos fichiers source. Pour utiliser Gulp dans le projet, nous devrons l'installer via npm et l'enregistrer en tant que dépendance de développeur pour le projet. Nous voudrons également commencer par le plugin concat pour Gulp, qui nous permettra de concaténer plusieurs fichiers en un seul.

Pour installer ces deux éléments, nous allons exécuter la commande suivante :

 npm install --save-dev gulp gulp-concat

Ensuite, nous voudrons commencer à écrire le contenu de gulpfile.js.

 var gulp = require('gulp'); var concat = require('gulp-concat'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);

Ici, nous chargeons la bibliothèque gulp et son plugin concat. Nous définissons ensuite trois tâches.

Chargement de la bibliothèque gulp et de son plugin concat

La première tâche ( pack-js ) définit une procédure pour compresser plusieurs fichiers source JavaScript en un seul ensemble. Nous listons les fichiers source, qui seront globés, lus et concaténés dans l'ordre spécifié. Nous envoyons cela dans le plugin concat pour obtenir un fichier final appelé bundle.js . Enfin, nous disons à gulp d'écrire le fichier dans public/build/js .

La deuxième tâche ( pack-css ) fait la même chose que ci-dessus, mais pour les feuilles de style CSS. Il indique à Gulp de stocker la sortie concaténée en tant que stylesheet.css dans public/build/css .

La troisième tâche ( default ) est celle que Gulp exécute lorsque nous l'invoquons sans arguments. Dans le deuxième paramètre, nous passons la liste des autres tâches à exécuter lorsque la tâche par défaut est exécutée.

Collons ce code dans gulpfile.js en utilisant n'importe quel éditeur de code source que nous utilisons normalement, puis enregistrons le fichier à la racine de l'application.

Ensuite, nous allons ouvrir la ligne de commande et exécuter :

 gulp

Si nous regardons nos fichiers après avoir exécuté cette commande, nous trouverons deux nouveaux fichiers : public/build/js/bundle.js et public/build/css/stylesheet.css . Ce sont des concaténations de nos fichiers source, ce qui résout une partie du problème d'origine. Cependant, ils ne sont pas minifiés et il n'y a pas encore de contournement du cache. Ajoutons une minification automatisée.

Optimisation des actifs construits

Nous aurons besoin de deux nouveaux plugins. Pour les ajouter, nous allons exécuter la commande suivante :

 npm install --save-dev gulp-clean-css gulp-minify

Le premier plugin est pour minifier CSS, et le second est pour minifier JavaScript. Le premier utilise le package clean-css et le second utilise le package UglifyJS2. Nous allons d'abord charger ces deux packages dans notre gulpfile.js :

 var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css');

Nous devrons ensuite les utiliser dans nos tâches juste avant d'écrire la sortie sur le disque :

 .pipe(minify()) .pipe(cleanCss())

Le fichier gulpfile.js devrait maintenant ressembler à ceci :

 var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify()) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);

Exécutons à nouveau gulp. Nous verrons que le fichier stylesheet.css est enregistré au format minifié, et le fichier bundle.js est toujours enregistré tel quel. Nous remarquerons que nous avons maintenant aussi bundle-min.js, qui est minifié. Nous ne voulons que le fichier minifié, et nous voulons qu'il soit enregistré sous bundle.js , nous allons donc modifier notre code avec des paramètres supplémentaires :

 .pipe(minify({ ext:{ min:'.js' }, noSource: true }))

Selon la documentation du plugin gulp-minify (https://www.npmjs.com/package/gulp-minify), cela définira le nom souhaité pour la version minifiée et indiquera au plugin de ne pas créer la version contenant la source d'origine. Si nous supprimons le contenu du répertoire de construction et exécutons à nouveau gulp à partir de la ligne de commande, nous nous retrouverons avec seulement deux fichiers minifiés. Nous venons de terminer la mise en œuvre de la phase de minification de notre processus de construction.

Casse du cache

Ensuite, nous voudrons ajouter le cache busting, et nous devrons installer un plugin pour cela :

 npm install --save-dev gulp-rev

Et exigez-le dans notre fichier gulp :

 var rev = require('gulp-rev');

L'utilisation du plugin est un peu délicate. Nous devons d'abord diriger la sortie minifiée via le plugin. Ensuite, nous devons appeler à nouveau le plugin après avoir écrit les résultats sur le disque. Le plugin renomme les fichiers afin qu'ils soient étiquetés avec un hachage unique, et il crée également un fichier manifeste. Le fichier manifeste est une carte qui peut être utilisée par notre application pour déterminer les derniers noms de fichiers auxquels nous devons nous référer dans notre code HTML. Après avoir modifié le fichier gulp, il devrait ressembler à ceci :

 var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('default', ['pack-js', 'pack-css']);
Avec une bonne suppression du cache en place, vous pouvez devenir fou avec un long délai d'expiration pour vos fichiers JS et CSS et les remplacer de manière fiable par des versions plus récentes chaque fois que nécessaire.

Supprimons le contenu de notre répertoire de construction et exécutons à nouveau gulp. Nous constaterons que nous avons maintenant deux fichiers avec des hashtags apposés sur chacun des noms de fichiers, et un manifest.json enregistré dans public/build . Si nous ouvrons le fichier manifeste, nous verrons qu'il ne contient qu'une référence à l'un de nos fichiers minifiés et étiquetés. Ce qui se passe, c'est que chaque tâche écrit un fichier manifeste distinct, et l'une d'entre elles finit par écraser l'autre. Nous devrons modifier les tâches avec des paramètres supplémentaires qui leur indiqueront de rechercher le fichier manifeste existant et d'y fusionner les nouvelles données s'il existe. La syntaxe pour cela est un peu compliquée, alors regardons à quoi devrait ressembler le code, puis parcourons-le :

 var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('default', ['pack-js', 'pack-css']);

Nous dirigeons d'abord la sortie vers rev.manifest() . Cela crée des fichiers balisés au lieu des fichiers que nous avions auparavant. Nous fournissons le chemin souhaité de notre rev-manifest.json et demandons à rev.manifest() de fusionner dans le fichier existant, s'il existe. Ensuite, nous disons à gulp d'écrire le manifeste dans le répertoire courant, qui à ce stade sera public/build. Le problème de chemin est dû à un bogue qui est discuté plus en détail sur GitHub.

Nous avons maintenant une minification automatisée, des fichiers marqués et un fichier manifeste. Tout cela nous permettra de livrer les fichiers plus rapidement à l'utilisateur et de vider leur cache à chaque fois que nous apporterons nos modifications. Il ne reste cependant que deux problèmes.

Le premier problème est que si nous apportons des modifications à nos fichiers source, nous obtiendrons des fichiers nouvellement étiquetés, mais les anciens y resteront également. Nous avons besoin d'un moyen de supprimer automatiquement les anciens fichiers minifiés. Résolvons ce problème en utilisant un plugin qui nous permettra de supprimer des fichiers :

 npm install --save-dev del

Nous allons l'exiger dans notre code et définir deux nouvelles tâches, une pour chaque type de fichier source :

 var del = require('del'); gulp.task('clean-js', function () { return del([ 'public/build/js/*.js' ]); }); gulp.task('clean-css', function () { return del([ 'public/build/css/*.css' ]); });

Nous nous assurerons ensuite que la nouvelle tâche se termine avant nos deux tâches principales :

 gulp.task('pack-js', ['clean-js'], function () { gulp.task('pack-css', ['clean-css'], function () {

Si nous gulp à nouveau gulp après cette modification, nous n'aurons que les derniers fichiers minifiés.

Le deuxième problème est que nous ne voulons pas continuer à exécuter gulp chaque fois que nous apportons un changement. Pour résoudre ce problème, nous devrons définir une tâche d'observateur :

 gulp.task('watch', function() { gulp.watch('assets/js/**/*.js', ['pack-js']); gulp.watch('assets/css/**/*.css', ['pack-css']); });

Nous allons également modifier la définition de notre tâche par défaut :

 gulp.task('default', ['watch']);

Si nous exécutons maintenant gulp à partir de la ligne de commande, nous constaterons qu'il ne construit plus rien lors de l'invocation. En effet, il appelle désormais la tâche d'observation qui surveillera nos fichiers source pour tout changement et ne générera que lorsqu'il détectera un changement. Si nous essayons de modifier l'un de nos fichiers source et que nous regardons à nouveau notre console, nous verrons que les tâches pack-js et pack-css s'exécutent automatiquement avec leurs dépendances.

Maintenant, tout ce que nous avons à faire est de charger le fichier manifest.json dans notre application et d'obtenir les noms de fichiers balisés à partir de celui-ci. La manière dont nous procédons dépend de notre langage back-end et de notre pile technologique, et serait assez simple à mettre en œuvre, nous n'allons donc pas en parler en détail. Cependant, l'idée générale est que nous pouvons charger le manifeste dans un tableau ou un objet, puis définir une fonction d'assistance qui nous permettra d'appeler des actifs versionnés à partir de nos modèles d'une manière similaire à ce qui suit :

 gulp('bundle.js')

Une fois que nous aurons fait cela, nous n'aurons plus jamais à nous soucier des balises modifiées dans nos noms de fichiers, et nous pourrons nous concentrer sur l'écriture de code de haute qualité.

Le code source final de cet article, ainsi que quelques exemples de ressources, se trouvent dans ce référentiel GitHub.

Conclusion

Dans cet article, nous avons expliqué comment implémenter l'automatisation basée sur Gulp pour notre processus de construction. J'espère que cela vous sera utile et vous permettra de développer des processus de construction plus sophistiqués dans vos propres applications.

N'oubliez pas que Gulp n'est qu'un des outils pouvant être utilisés à cette fin, et qu'il en existe de nombreux autres tels que Grunt, Browserify et Webpack. Ils varient dans leurs objectifs et dans l'étendue des problèmes qu'ils peuvent résoudre. Certains peuvent résoudre des problèmes que Gulp ne peut pas, tels que le regroupement de modules JavaScript avec des dépendances pouvant être chargées à la demande. C'est ce qu'on appelle le "fractionnement de code", et c'est une amélioration par rapport à l'idée de servir un gros fichier avec toutes les parties de notre programme sur chaque page. Ces outils sont assez sophistiqués mais pourraient être couverts à l'avenir. Dans un article suivant, nous verrons comment automatiser le déploiement de notre application.

En relation : Gulp Under the Hood : Création d'un outil d'automatisation des tâches basé sur les flux