Article écrit par Martin Catty
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.
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.