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, il n’y a qu’un mot : Browserify ! Ce dernier va nous permettre d’utiliser require directement dans nos fichiers destinés au front et il se chargera de bundler tout cela afin que les navigateurs puissent tout comprendre.

# après un npm install lodash --save, vous pourrez faire :
const _ = require('lodash');

_.partition([1, 2, 3, 4], n => n % 2);
// → [[1, 3], [2, 4]]

Cool, n’est-ce pas ? D’autres outils existent, notamment Webpack. Bien que très puissant, ce dernier est plus complexe et nécessite une configuration conséquente. Je le réserve donc aux projets d’applications web.

Gardez toujours à l’esprit qu’intégrer un module, c’est du poids supplémentaire (donc du temps de chargement). Inutile donc de charger intégralement jQuery simplement pour utiliser ajax. Premièrement, il y a de meilleurs alternatives, deuxièmement, de nombreux modules permettent de n’en charger qu’une partie. Par exemple, avec lodash, vous pouvez ne requérir – ouais ça fait bizarre traduit en français – que la partie sur les array :

# _ ne contiendra qu'un sous-ensemble des méthodes de la bibliothèque
cont _ = require('lodash/array');

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 pour les dépendances du projet et --save-dev 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 :

  • s’assurer que mes propriétés CSS sont bien organisées,
  • compiler Less en CSS et le rendre compatible avec le maximum de navigateurs,
  • compiler mon JavaScript pour les navigateurs cibles* (et bundler les éventuels require),
  • minifier le tout (HTML, CSS & JS),
  • accessoirement pouvoir inclure des templates de HTML à d’autres endroits (DRY, pas besoin de faire des copier-coller du header et du footer partout par exemple !),
  • pré-compresser les fichiers statiques afin de profiter des meilleur taux de compression tout en réduisant le time to first byte [en] (temps de réponse du serveur).

*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’IE10. Exemple de .babelrc :

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

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 :

  • postcss-cli et autoprefixer pour s'épargner l'écriture des préfixes de certaines propriétés et les rajouter pour les navigateurs cibles,
  • less pour compiler nos feuilles de styles Less,
  • browserify pour utiliser require dans nos scripts,
  • babel-cli et babel-preset-env pour compiler le JS,
  • cssmin pour minifier le CSS,
  • html-minifier pour minifier le HTML,
  • uglify-js pour minifier le JS,
  • nodemon si on veut un watcher qui re-compile tout quand on effectue des changements dans nos fichiers.

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ées 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éhensible 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+ vers de l'ES5+
    // et on pipe vers uglifyjs pour minification
    "js:build": 'browserify js/main.js | babel --presets=env | uglifyjs -o js/scripts.min.js",

    // si vous n'utilisez pas les modules node et require pour importer des modules ou bibliothèques
    // vous pouvez vous passer de browserify et remplacer la ligne précédente par celle-ci 
    "js:build": "babel js/main.js --presets=env | uglifyjs -o js/scripts.min.js",

    // idem à css:watch pour la task js
    "js:watch": "nodemon -e js --watch js --ignore js/scripts.min.js -x 'npm run js:build'",

    // 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.

Templating

Il nous manque ici la partie template. Elle repose sur l'utilisation de php en tant que langage de template. Ce dernier est installé par défaut sur macOS et la majorité des distribution Linux, autant en profiter. J'utilise aussi les boucles for de bash. Mac et Linux ready, à voir pour Windows (sous-système Ubuntu sur Win10+ ?).

"scripts": {
    …
    "phptpl:build": "for f in templates/*.php; do nf=`echo $(basename $f) | cut -d '.' -f 1`; php $f | html-minifier > $nf.html; done",
    "phptpl:watch": "nodemon -e php --watch templates -x 'npm run phptpl:build'",
    …
    // MAJ de notre build et watch global
    "build": "npm run css:build && npm run js:build & npm run phptpl:build",
    "watch": "npm run css:watch && npm run js:watch & npm run phptpl:watch"

Explications. La boucle récupère tous les fichiers php qui sont dans templates – pas dans les sous-répertoires, on pourra donc utiliser un templates/parts pour les fichiers à inclure.

Les fichiers parts sont souvent du simple html. Cependant, comme on voudra certainement placer *.html dans le .gitignore afin de ne pas versionner les fichiers générés, nos partials seront invisibles de git si on leur attribue cette extension. Je les nomme donc en .shtml. L'extension est valide et fait référence à du html contenant du SSI.

Pour chaque fichier, on lui donne un nom en .html puis il est passé à l'interpréteur php. Là, vous avez accès à toute la puissance du php et on obtient du pur html en sortie.

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 un site builingue relativement facilement avec cette architecture de fichiers :

$ ls -R
templates-en/
  parts/

templates-fr/
  parts/

img/	
scripts/
styles/	

Ensuite, on retravaille un peu la ligne de phptpl pour qu'elle aille dans templates-fr et templates-en et qu'elle outut le html du site anglais dans un sous dossier. Comme cela, toute la partie anglaise du site est dispo dans http://monsite.net/en/

for f in templates-fr/*.php; do nf=`echo $(basename $f) | cut -d '.' -f 1`; php $f | html-minifier > $nf.html; done && for f in templates-en/*.php; do nf=`echo $(basename $f) | cut -d '.' -f 1`; php $f | html-minifier > en/$nf.html; done

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 (voir mon article sur Apache pour la mise en œuvre). Si vous Brotli [en] 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. Il est supporté par la quasi totalité des navigateurs modernes.

Gzip est installé par défaut sur la majorité des distributions Linux ainsi que sur macOS. Pour brotli, npm ne peut malheureusement pas s'en charger, il n'y a cependant qu'une commande à exécuter via la gestionnaire de paquets du système. Une fois de plus, pour Windows, je ne saurai trop vous dire (avis aux développeur Windowsiens, je suis preneur de feedback en commentaires).

# Debian et dérivés
apt install brotli

# macOS via homebrew
brew install brotli

Nous n'avons plus qu'à ajouter deux tâches de compressions 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:gz": "for f in *.html; do gzip -9 -k -f $f; done && for f in img/*.svg; do gzip -9 -k -f $f; done && for f in js/*.js; do gzip -9 -k -f $f; done && for f in styles/*.css; do gzip -9 -k -f $f; done",
"compress:br": "for f in *.html; do brotli -Zf $f; done && for f in img/*.svg; do brotli -Zf $f; done && for f in js/*.js; do brotli -Zf $f; done && for f in styles/*.css; do brotli -Zf $f; done",
…
// MAJ du build, pas du watch, ce serait contre-productif
"build": "npm run css:build & npm run js:build & npm run phptpl:build && npm run compress:gz && npm run compress:br",

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 réuni tout ce que nous venons de voir dans un repo Github. De cette manière, je commence par récupérer mon archive, un npm install et en voiture Simone !

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

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

Déjà 2 réponses, rejoignez la discussion !

    • Buzut

      dit :

      Ah, c’est l’influence de la créativité du web qui déteint sur nous je pense 🙈
      PS : en vrai je ne vois pas en quoi NPM est une usine à gaz. Quand je vois Omninus, ça c’est de l’usine à gaz pour moi. Chacun ses standards.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *