Laisser un commentaire

npm for everything™

npm était à l’origine le package manager pour Node.js. Cependant, son rôle s’est aujourd’hui élargit pour devenir le package manager du JavaScript tout court. Aussi bien en front qu’en back, de plus en plus de modules et bibliothèques utilisent aujourd’hui npm. On en fait même un task manager grâce aux scripts qu’il permet d’exécuter. Cela permet d’avoir un seul outil pour l’ensemble de nos process.

Voyons comment installer nos dépendance avec npm et ajoutons quelques tâches simples afin d’optimiser notre flux de développement.

Le pré-requis est d’installer Node et npm. Direction le site officiel. Sachez qu’il est possible d’installer Node via les package manager (même sur Mac !).

Ensuite, un petit coup de npm install xxx --save ou npm install xxx --save-dev nous permettra d’installer et de sauvegarder nos dépendances dans le package.json. Ainsi, la prochaine fois, vous n’aurez plus qu’à faire un npm install pour que tout s’installe comme par magie.

npm en tant que package manager

La partie package manager s’appréhende assez facilement. Elle permet d’installer automatiquement les dépendances de développement, tels que les linters (comme vu dans l’article sur l’environnement de travail) ou les transpileurs.

Il faut cependant comprendre que les dépendances du projet à proprement parler s’utilisent légèrement différemment. En effet, toutes les dépendances sont téléchargées dans le répertoire node_modules et sont souvent divisées en de nombreux fichiers. En outre, votre navigateur ne sera pas en mesure de résoudre les dépendances logées dans node_modules.

Pour tirer profit de la gestion des dépendances projet via npm, plusieurs solutions sont possibles. Dans les solutions modernes, les trois les plus utilisées sont Webpack, Rollup et Parcel. Browserify était un des premier mais perd en popularité car il ne supporte pas la syntaxe des modules ES6.

Webpack est très puissant mais assez complexe à configurer, on le réserve plutôt pour les grosses applications codées en React, Angular ou VueJS.

Pour s’occuper des assets d’un site classique, on aura donc plutôt recourt à Rollup ou Parcel. Parcel est plus récent et, contrairement à Rollup, ne s’appuie pas sur un système de plugin mais tente à l’inverse de tout inclure dans le core. Soyons honnête, j’ai mes habitudes avec Rollup – qui est très performant – et n’ai donc jamais utilisé Parcel.

Rollup permet le Tree Shaking [en] et produit ainsi des modules plus légers tout en utilisant le dernier standard pour les navigateurs (future proof). Je lui consacre un article entier, n’hésitez pas à le lire car c’est facile à mettre en place, performant et l’intégration avec npm est parfaite !

# après un npm install lodash --save, vous pourrez faire :
import _ from 'lodash';

# puis plus loin dans le code
_.partition([1, 2, 3, 4], n => n % 2);
// → [[1, 3], [2, 4]]

Gardez toujours à l’esprit qu’intégrer un module, c’est du poids supplémentaire (donc du temps de chargement). Pour mesure l’impact des différent modules, je vous conseille de faire un tour sur BundlePhobia. Vous lui donnez le nom du module et le site vous montre la taille et l’impact des performances du module en question !

NB : npm différencie les dépendances du projet (bibliothèque directement incluses au projet), des dépendances de développement (outils de build, linters…). Par conséquent, pour sauvegarder les dépendances dans le package.json lors de leur installation : --save (ou -S) pour les dépendances du projet et --save-dev (ou -D) pour les dépendances de développement.

Nous allons voir dans la partie suivante comment configurer tout cela.

npm en tant que task runner

Nous l’avons dit, npm permet d’exécuter des scripts. Pour un projet basique, mes besoins sont généralement les suivants :

*Petit point sur la transpilation du JavaScript. Il est courant de transpiler l’ES2015+ en ES5. Cependant, avec Babel, de la même manière que l’on préfixe le CSS seulement pour les navigateurs cibles, il est possible de convertir seulement les parties du JS qui ne seront pas comprises par ces derniers.

À vous de définir votre cible dans le .babelrc. Par exemple, tous les navigateurs qui représentent plus de 1% de parts de marché, les deux dernières versions des navigateurs courants et rien en dessous d’IE11. Exemple de .babelrc :

{
  "presets": [
    ["env", {
        "targets": {
            "browsers": ["> 1%", "last 2 versions", "not ie < 11"]
        }
    }]
  ]
}

Nous allons mettre à profit les scripts npm pour faire toutes les tâches sus-mentionnées ! Il nous faut avant tout installer les outils. Nous avons besoin des choses suivantes :

En plus de cela, j’installe stylelint, stylelint-config-standard et stylelint-order pour linter les styles (css, less ou sass), ainsi que eslint, eslint-config-airbnb-base et eslint-plugin-import pour linter le JS. Ces linters permettent de dynamiquement afficher les erreurs dans l’éditeur de code. Leur usage et leurs configurations sont détaillés dans mon article sur l’environnement de travail.

Bien entendu, tout cela est à installer avec --save-dev pour que ce soit ajouté dans le package.json. Détaillons maintenant notre partie scripts.

Bundling, transpiling et minification

Nous allons dans un premier temps nous concentrer sur le traitement du JavaScript et des styles afin de les rendre compréhensibles par les navigateurs et aussi compacts que possible.

"scripts": {
    // build du less vers css. On renseigne le fichier principal (celui qui inclue les autres)
    // on pipe la sortie vers autoprefixer en lui précisant qu'on cherche à être compatible avec les versions n-1
    // et on envoie le tout vers cssmin pour compression
    "css:build": "lessc styles/main.less | postcss --use autoprefixer -b 'last 1 versions' | cssmin > styles/styles.min.css",

    // le watch utilise nodemon et relance la task de build dès qu'un fichier less est modifié dans "styles"
    "css:watch": "nodemon -e less --watch styles -x 'npm run css:build'",

    // on bundle nos require avec browserify (on renseigne le fichier d'entrée)
    // puis on compile notre ES2015+ en fonction des options du .babelrc (babel recherche automatiquement le fichier)
    // et on pipe vers uglifyjs pour minification
    "js:build": "rollup --config",

    // le watch pour le JavaScript
    "js:watch": "rollup --config --watch",

    // méta task qui lance les différentes tâches de build
    "build": "npm run css:build && npm run js:build",

    // meta task qui lance les différentes tâches de watch
    "watch": "npm run css:watch && npm run js:watch"
},

Chaque partie s’exécute ensuite avec un simple coup de npm run xxx. Par exemple npm run css:build.

Cache busting

Ce n’est pas une nouveauté, les navigateurs mettent les assets (feuilles de style, fichiers JavaScript, images…) en cache. C’est une très bonne chose pour les performances des visiteurs récurrents et c’est pour cela qu’il est recommandé de mettre une durée de vie assez longue à vos assets.

Cependant, l’effet indésirable de cette mise en cache est que dans la plupart des configurations, si vous faites des modifications à l’un de ces fichiers, les navigateurs n’iront pas vérifier si celui-ci a changé et un fichier périmé sera donc utilisé par vos visiteurs.

Cela peut être assez embêtant, notamment si le HTML (peu mis en cache) a changé et qu’une ancienne version de CSS et/ou JS est utilisée. Votre page peut apparaître comme cassée ou non fonctionnelle.

Pour remédier à cela, il est courant de changer le nom du fichier lorsque l’on y effectue des modifications. On peut bien entendu charger npm de cette tâche, de cette manière on n’a pas à y penser.

Nous avons deux méthodes à notre disposition pour cela. La première et la plus pratique : utiliser la version du projet de npm afin de versioner les fichiers.

{
  "name": "buzut",
  "version": "1.0.0",
  …,
  "scripts": {
     "css:build": "lessc styles/main.less | postcss --use autoprefixer -b 'last 1 versions' | cssmin > styles/styles-$npm_package_version.min.css",
     …
  },
  …
}

$npm_packageversion sera ici remplacée par la version du projet, soit 1.0.0 l’avantage de cette solution est qu’elle offre les outils de changement de version de npm : npm version patch|minor|major. Cette commande permet de grimper d’une version et ajoute un tag Git.

Dans le cas où, pour une raison ou une autre, on ne veut gérer la version npm et la version des assets de manière séparée, on utilisera alors les variables de config.

{
  "name": "buzut",
  "version": "1.0.0",
  …,
  "config": {
    "assetsVersion": "1.5"
  },
  "scripts": {
    "css:build": "lessc styles/main.less | postcss --use autoprefixer -b 'last 1 versions' | cssmin > styles/styles-$npm_package_config_assetsVersion.min.css",
    …
  },
  …
}

La méthode est très similaire. La valeur est définie dans config, le nom de la variable reflète cette définition il faudra gérer manuellement les changements de versions.

Templating

Il nous manque ici la partie template. Elle repose sur l’utilisation des templates EJS. Il s’agit purement et simplement d’un langage de template utilisant le JavaScript, on a donc immédiatement nos repères.

"scripts": {
    …
    "template:build": "ejs-cli -b templates/ '**/*.ejs' -e partials/ --out public/",
    "template:watch": "nodemon -e ejs --watch templates -x 'npm run template:build'",
    …
    // MAJ de nos build et watch globaux
    "build": "npm run css:build && npm run js:build & npm run template:build",
    "watch": "npm run template:watch & npm run css:watch && npm run js:watch"
},

Il n’y a plus qu’à piper tout cela à html-minifier pour compression et à rediriger l’output à la racine du projet.

Cette technique m’a même permis de travailler sur des sites bilingues très facilement avec cette architecture de fichiers :

$ ls -R
templates/
  partials/
  other-language/
    partials/
img/
scripts/
styles/

Sans que vous n’ayez rien à faire, la langue par défaut sera accessible à la racine et la ou les autres langues dans un sous dossier : monsite.com/en/. Voir par exemple mon outil de récupération d’archives email.

La compression

Les serveurs tels qu’Apache ou Nginx procèdent à la volée à une compression des fichiers qu’ils envoient, afin d’en réduire la taille et d’en augmenter la vitesse de transfert. Comme dit dans mon article sur l’installation et l’optimisation d’Apache :

Il faut trouver le juste milieu entre taille du fichier et temps de compression. En effet, si le fichier est trop long à compresser, peut-être est-ce plus rapide de ne pas le compresser, ou de moins le compresser.

Nos fichiers étant ici statiques, le plus efficace est de les compresser lors du build en Gzip et Brotli, avec les taux de compression les plus élevés, et de servir les fichiers pré-compressés en évitant au serveur un long et couteux processus de compression.

Si Brotli ne vous dit rien, il s’agit d’un format de compression mis au point en 2015 par Google qui offre des taux de compressions bien supérieurs à Gzip et supporté par tous les navigateurs modernes. Pour servir des fichiers pré-compressés, lisez la section compression de mes articles sur Apache2 et Nginx.

Il y a ici deux méthodes. Soit nous installons nativement les deux bibliothèque sur la machine de build, soit nous utilisons les modules npm. J’ai une préférence pour la première solution, néanmoins, voici les modules nécessaires afin que vous puissiez mettre en place la seconde méthode :

La compression Gzip et Brotli est nativement supporté par Node depuis la version 12. Avant cela, j’installais la bibliothèque Brotli sur le système et compressais les fichiers directement via cette dernière (Gzip étant quant à lui nativement présent presque partout).

Ceci n’est dorénavant plus nécessaire et nous n’avons dès lors même plus à nous encombrer de l’installation d’une dépendance externe, npm et Node s’occupent absolument de tout !

Nous n’avons plus qu’à ajouter deux tâches de compression afin de générer des archives pour les fichiers JS, CSS, HTML et SVG. Vous pouvez également y ajouter les fichiers JSON et XML si c’est applicable à votre situation. En revanche, en aucun cas nous ne compressons des images, fichiers audio et vidéo qui sont déjà des formats compressés.

…
"compress:css": "gzip public/**/*.css --extension=gz --extension=br",
"compress:js": "gzip public/**/*.js --extension=gz --extension=br",
"compress:svg": "gzip public/**/*.svg --extension=gz --extension=br",
"compress:all": "npm run compress:css && npm run compress:js && npm run compress:svg",

// très utile si votre serveur de dev en local sert les fichiers compressés
// vous ne verriez pas vos modifs lors du watch
"compress:clean": "find public -name '*.gz' -exec rm '{}' \\; && find ./public -name '*.br' -exec rm '{}' \\;",
…
// MAJ du build, pas du watch, ce serait contre-productif
"build": "npm run css:build & npm run js:build & npm run template:build && npm run compress:all",

Conclusion

Le but de cet article est de montrer les possibilités de npm et non de configurer un compilateur. Nous n’avons pas expliqué la configuration de Rollup car il n’est pas le seul outil pouvant être utilisé et j’y dédié un article entier.

Chaque projet est différent. Néanmoins, avant chaque démarrage, on passe un temps non négligeable à installer et paramétrer tous nos outils. C’est pourquoi j’ai créé un micro-framework nommé Dopamine, disponible sur GitHub.

De cette manière, je commence par récupérer mon archive, un npm install et en voiture Simone ! De plus, Dopamine possède de nombreuses options dont nous n’avons pas parlé ici :

npm ne fait pas encore le café et il ne gère ici pas le deploy. Pour le deploy, j’utilise Ansible – auquel j’ai consacré deux articles. N’hésitez pas à y jeter un œil.

N’hésitez pas à partager votre manière de travailler !

Commentaires

Rejoignez la discussion !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```