Évaluons votre projet

Webpacker : the highway to hell

Publié le 7 mai 2021 par Martin Catty | ops

Cet article est publié sous licence CC BY-NC-SA

Un outil pour les gouverner tous

Note I : dans cet article quand je parle de production il s’agit du mode production plutôt que d’environnement de production.

Note II : Webpacker est une gem permettant d’utiliser Webpack dans une application Ruby.

Note III : Juste parce que j’aime bien les chiffres romains.

Nos applications Rails, dès lors qu’elles quittent nos machines, tournent toujours dans un mode production, aussi bien sur un environnement de pré-production que de revue.

Lorsque nous démarrons de nouveaux projets, nous utilisons notre générateur de projets, répondant au doux nom d’Opsible (car utilisant Ansible) et qui nous permet de mettre en place une application prête à l’emploi, aussi bien localement qu’en production.

Tous nos projets utilisant Docker, l’outil est en charge de mettre en place une configuration fonctionnelle.

Il s’appuie également sur des templates Rails pour nous permettre de créer de pures API ou des monolithes.

Lors d’une nouvelle mise à jour d’Opsible s’est reposée la sempiternelle question : Sprockets et/ou Webpacker ?

Sprockets et/ou Webpacker ?

Si vous lisez le guide Ruby on Rails dédié à Webpacker la réponse est très claire (</troll>) : vous pouvez utiliser les deux pour faire … les mêmes choses.

New Rails apps are configured to use webpack for JavaScript and Sprockets for CSS, although you can do CSS in webpack.

Dans l’équipe notre consensus était clair : Webpack a plutôt vocation à être utilisé lorsqu’on intègre des app riches dans notre monolithe.

Comprenez que dès que l’on souhaite intégrer des frameworks front type Vue.js nous mettons en place Webpacker.

Sauf que celui-ci venait en addition de Sprockets plutôt qu’en remplacement.

Qui peut le plus peut le moins

Dès lors qu’on bascule dans ce setup qui mix les deux approches, ceux qui travaillent sur des parties sans Webpack ont un peu l’impression de se faire avoir.

Ils font tourner les processus nécessaires à Webpacker, notamment un serveur dédié à la recompilation des assets, sans en profiter.

En effet, Webpack offre des avantages non négligeables comme le hot reloading et le hot module replacement, offrant une expérience de développement bien plus fluide.

Dès lors, nous avons décidé pour nos nouveaux projets de ne garder que Webpacker.

De la bonne configuration de Webpacker

Avec Webpacker la règle est simple : tant que vous n’êtes pas en production vous n’êtes pas sûr que ça fonctionne.

Là où Rails nous a habitué à son convention over configuration, dans le contexte de Webpack il semble n’y avoir étonnamment pas de consensus fort sur la bonne façon d’organiser ses fichiers, quand l’utiliser, etc.

Pour matérialiser les soucis qu’on peut rencontrer lors de sa mise en place je vais prendre un cas d’usage dans l’air du temps : l’intégration de Tailwind dans notre application.

La configuration de Webpacker (config/webpacker.yml) est découpée en environnements, vous aurez donc probablement une configuration development et une production à minima (nous y reviendrons).

Notre objectif est de ne garder dans notre layout que les deux balises suivantes :

= stylesheet_pack_tag  "application", media: "all", "data-turbolinks-track": "reload"
= javascript_pack_tag  "application", "data-turbolinks-track": "reload"

Notre configuration par défaut de Webpacker est la suivante :

default: &default
  source_path: app/javascript
  source_entry_path: packs
  public_root_path: public
  public_output_path: packs
  cache_path: tmp/cache/webpacker
  webpack_compile_output: true
  check_yarn_integrity: false
  

Rien de complexe ici, par défaut nos fichiers seront recherchés dans app/javascript. Les « manifestes » devront se trouver dans le dossier packs. Les dossiers de sortie des fichiers compilés seront dans public/packs.

Sans perdre de temps nous voilà en route pour intégrer Tailwind. L’un des avantages de Webpacker est de pouvoir facilement intégrer des bibliothèques publiées sous forme de paquets NPM.

Ni une ni deux :

yarn add tailwindcss
yarn tailwind init

Puis on configure notre fichier postcss.config.js :

module.exports = {
  plugins: [
    require("postcss-import"),
    require("tailwindcss"), // on ajoute cette ligne uniquement
    require("postcss-flexbugs-fixes"),
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009"
      },
      stage: 3
    })
  ]
};

On part sur une arborescence de la sorte pour organiser notre JavaScript :

app/javascript
├── channels
│   ├── consumer.js
│   └── index.js
└── packs
    └── application.js

Et celle ci pour nos CSS :

app/stylesheet
└── app.scss

Dans notre manifeste application.js on va référencer nos fichiers CSS, c’est une pratique normale de Webpack qui fonctionne sur un mécanisme d’import.

Dès lors que les fichiers CSS ont été référencés, ils sont utilisables dans notre application et cela fonctionne même sans directive d’import de notre manifeste CSS dans notre layout.

import Rails from "@rails/ujs";
import Turbolinks from "turbolinks";
import * as ActiveStorage from "@rails/activestorage";
import "channels";

import "../../stylesheet/app.scss"; // on ajoute cette ligne

Rails.start();
Turbolinks.start();
ActiveStorage.start();

Le torse bombé on relance notre serveur webpack et là, c’est le drame.

Erreur postcss
8

Visiblement Tailwind veut la version 8 de PostCSS. Mais cette version n’est pas compatible avec la version stable actuelle de Webpacker (5.3).

Combatif, on ne lâche pas l’affaire, il se trouve qu’il y a une version de Tailwind compatible avec PostCSS 7.

yarn remove tailwindcss
yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

Magnifique, on peut maintenant écrire des styles dans notre fichier app/stylesheet/app.scss, ceux-ci seront appliqués à la volée.

Profitons-en pour importer également Tailwind (qui n’est pas encore chargé à ce stade), on aura au final dans notre fichier app.scss :

@import "tailwindcss/base";
@import "tailwindcss/utilities";
@import "tailwindcss/components";

Cela fonctionne bien, avec un bémol :

webpack_1  | ℹ 「wdm」: Compiled successfully.
webpack_1  | ℹ 「wdm」: Compiling...
webpack_1  | ℹ 「wdm」: Hash: e0f5684b0fd5ecd26488
webpack_1  | Version: webpack 4.46.0
webpack_1  | Time: 4470ms

Quasiment 5 secondes de compilation, et ce à chaque fois qu’on va introduire une directive dans le fichier. Cela tient au fait que Tailwind contient une tétrachiée de classes utilitaires, prêtes à l’emploi et mises à disposition même si on ne s’en sert pas.

Pour y palier on va découper notre fichier en deux :

app/stylesheet
├── app.scss
└── tailwind.scss

Le nouveau fichier tailwind.scss ne contiendra que les directives relatives à ce dernier. Lorsqu’on édite notre fichier app.scss, on revient à des temps acceptables :

webpack_1  | ℹ 「wdm」: Compiled successfully.
webpack_1  | ℹ 「wdm」: Compiling...
webpack_1  | ℹ 「wdm」: Hash: 9f7a514f5e27f38ab57e
webpack_1  | Version: webpack 4.46.0
webpack_1  | Time: 156ms

Maintenant que tout est en ordre il faut qu’on prépare notre configuration pour la production. Si la balise suivante est inutile en local, elle le sera en production :

= stylesheet_pack_tag  "application", media: "all", "data-turbolinks-track": "reload"

On va donc créer notre manifeste :

app/javascript/packs
├── application.js
└── application.scss

Qui contient les même instructions :

@import "../../stylesheet/tailwind.scss";
@import "../../stylesheet/app.scss";

On rajoute quelques directives dans notre fichier app.scss et là surprise, notre build prend de nouveau 5 secondes !

webpack_1  | ℹ 「wdm」: Compiling...
webpack_1  | ℹ 「wdm」: Hash: 36fdd1b07e74458fd268
webpack_1  | Version: webpack 4.46.0
webpack_1  | Time: 4754ms
webpack_1  | Built at: 04/27/2021 2:35:27 PM

Et oui dès lors que notre manifeste est déclaré, l’ensemble des fichiers qui y sont référencés vont être surveillés et compilés.

Pour éviter cela nous avons décidé de déclarer le manifeste dans un dossier qui ne sera utilisé que pour la production.

production:
  <<: *default

  additional_paths: ["app/stylesheet"]
app/stylesheet
├── app.scss
├── packs
│   └── application.scss
└── tailwind.scss

S’en est tout de mes pérégrinations dans le pays merveilleux de Webpack(er). Je vous ai toutefois laissé avec un problème à résoudre, celui de retirer les CSS inutilisées dans votre build de production (autrement le fichier Tailwind pèse quelque 3Mo non compressé).

Si vous avez des avis / astuces / bonnes pratiques sur la façon de dompter ce monstre qu’est Webpack, n’hésitez pas à engager la conversation sur Twitter.


L’équipe Synbioz.
Libres d’être ensemble.