🍪 🚀 Deviens expert(e) de Sprockets et de l'asset pipeline
🚨 Cette édition est relativement longue, votre boîte mail risque de la tronquer. Je vous conseille de cliquer sur le titre ci-dessus pour l’ouvrir dans votre navigateur web. ☝️ Bonne lecture !
La plupart d'entre nous n'ont jamais pris le temps d'étudier les subtilités de la compilation des assets et la manière dont ils sont interprétés par le navigateur. Cette démarche peut s'avérer complexe et prendre beaucoup de temps. Heureusement, si notre projet est correctement configuré, il est rare que nous ayons besoin d'approfondir ces aspects techniques.
Cependant, avec l'avènement de Rails 7, de nouvelles options de compilation pour JavaScript et CSS ont été introduites, et l'arrivée de Rails 8 prévoit la transition de Sprockets vers Propshaft.
Ne serait-ce donc pas le moment parfait pour comprendre enfin le fonctionnement de l'asset pipeline, quitte à se faire quelques cheveux blancs ?
Au programme :
Comprendre Sprockets et l'asset pipeline par Tim
Le job de tes rêves !
Temps de lecture : 12 min
Hello les petits Biscuits !
Bienvenue sur la 14ème édition de Ruby Biscuit.
Vous êtes maintenant 295 abonnés 🥳 Si vous n’êtes pas déjà inscrit :
Comprendre Sprockets et l'asset pipeline
Aujourd'hui nous allons principalement nous concentrer sur la gestion des fichiers JavaScript, bien que nous aborderons parfois d'autres types d'assets. Tout au long de mes explications, j'illustrerai ces concepts avec des exemples concrets. Voici un aperçu de ce que vous allez apprendre :
Qu'est-ce que Sprockets et comment fonctionne-t-il ?
Comment Sprockets s'intègre-t-il harmonieusement dans l'écosystème de Rails ?
Pourquoi est-il bénéfique de déléguer la compilation des fichiers JavaScript et CSS, et à qui pouvons-nous confier cette tâche ?
Enfin, nous clôturerons avec un exercice pratique pour mettre en application ces concepts.
Sprockets et ses composants
Sprockets est un outil conçu pour simplifier le processus de gestion, de compilation et de livraison des assets tels que les fichiers JavaScript, CSS, images, etc., aux navigateurs web.
Voici ces principaux composants :
Les Processors
Ils sont les éléments les plus importants. Leurs rôles est d'effectuer une transformation ou une manipulation sur un asset (fichier JavaScript, CSS, image, ...) au moment de sa compilation. Les processors sont utilisés pour apporter des modifications au contenu des fichiers sources avant qu'ils ne soient intégrés dans le fichier de sortie final.
Voici quelques exemples de ce que les processors peuvent accomplir :
Donner des instructions (les "directives") : ce sont des instructions spéciales incorporées dans les fichiers d'assets, pour indiquer au système de gestion des assets comment gérer ces fichiers lors de la compilation. Elles peuvent être utilisées pour spécifier l'ordre de chargement des fichiers et activer ou désactiver certaines fonctionnalités de Sprockets. Parmi les plus courantes (et que vous voyez souvent) on retrouve
//= require
//= require_tree
//= require_directory
.Compilation de langages (aka Transformers 🤖) : compiler des langages sources tels que CoffeeScript en JavaScript ou Sass en CSS.
Transpilation : transformer le code source écrit dans des versions plus récentes de JavaScript.
Minification : réduire la taille des fichiers en supprimant les espaces et les commentaires, et en raccourcissant les noms de variables.
Compression : compresser les fichiers pour réduire leur taille.
Le Manifest
Il est le fichier spécifique qui répertorie tous les assets utilisés dans notre application. Il sert de point d'entrée central pour le processus de compilation des assets, aidant le système à comprendre quelles dépendances doivent être chargées et dans quel ordre. Le plus courant étant application.js
.
Pour s'assurer que le navigateur prend bien en compte nos dernières modifications, un "digest" (nous reviendrons sur cette notion un peu plus tard) est ajouté aux fichiers compilés. Sprockets maintient une correspondance entre les noms de fichiers originaux et ceux contenant des digests dans un hash interne.
Le contenu de ce manifest est visible dans public/manifest.json
une fois qu'on a fait un rails assets:precompile
.
Comment intégrer Sprockets à Rails ?
Bien que Sprockets soit la technologie sous-jacente utilisée par l'Asset Pipeline de Rails pour la gestion des assets, c'est la gem sprockets-rails qui simplifie le processus d'intégration de Sprocket et offre des fonctionnalités spécifiques à Rails.
Cette gem ajoute les fameux helpers javascript_include_tag
et stylesheet_link_tag
, configure Sprockets dans le fichier config/initializers/assets.rb
et se charge de raise une erreur quand un asset utilisé n'est pas déclaré (entre autres).
Venons-en au fait, comment sont compilés nos assets avec Sprockets ?
Partons d'un helper javascript_include_tag "potato"
, qui pointe vers un fichier potato.js.erb
qui se trouve dans app/assets/javascript
et qui nous produira une balise <script/>
qui ressemblerait à cela :
#<script src="/assets/potato.debug-ae0e5a78gfb231d11e07e00ec30g39f0a.js" />
Lors du rendu de la page, Sprockets choisit alors le pipeline interne
debug
(qui est une configuration pour permettre notamment d'ajouter des éléments de debug dans les fichiers compilés). En environnement de développement, le fichier est compilé à la volée.La résolution du MIME-type du fichier se fera de droite à gauche, et dictera quel processor utiliser :
on commence par
.erb
. LeERBProcessor
remplace tous les tags ERB du fichier. (l'extension.erb
est en général déconseillée).on continue par
.js
. LeBundleProcessor
entre en action et va lui-même utiliser leDirectiveProcessor
pour concaténer les éventuels autres fichiers qui auraient été requis via des directivesrequire_xxx
.ensuite le fichier
.js
étant totalement "processé", on en génère une source map via leSourceMapProcessor
.on finit avec le
UglifierCompressor
qui va minifier ce JS.
Le manifest.json interne à Sprockets est mis à jour avec une entrée qui ressemble à ça :
{ "potato.js" => "potato.ae0e5a78gfb231d11e07e00ec30g39f0a.js" }
C'est grâce à cela que Rails est en mesure de résoudre le fichier potato.js
vers sa version compilée /assets/potato-ae0e5a78gfb231d11e07e00ec30g39f0a.js
.
Le digest ae0e5a78gfb231d11e07e00ec30g39f0a
fourni une signature numérique unique pour le fichier. Si le moindre caractère change alors le digest change.
En production cette compilation n'est faite qu'une fois, en général préalablement à un déploiement, elle est déclenchée grâce à la tâche rails assets:precompile
.
Seul un asset statique est retourné par le javascript_include_tag
.
Ce comportement est configuré dans le fichier config/environments/production.rb
:
config.assets.compile = false
Voici le résultat d'une compilation simple, sur des fichiers .js
uniquement, avant la minification avec un fichier application.js
basique :
Mon manifest va prendre tout ce qui est à la racine de app/assets/javascripts
. Donc ici application.js
et aucun des sous-dossiers de app/assets/javascripts
Les contenus des fichiers JS :
Une fois compilés (avant le travail du compressor), on obtient ce résultat :
On rencontre très rapidement des problèmes de collision. Par exemple, aucun autre module que ceux déjà appelés via require
ne peuvent utiliser de variable ingredient
, car elle est déjà déclarée. À l'inverse, certaines variables sont accessibles alors qu'elles ne sont pas déclarées dans le fichier source.
Il est alors nécessaire de n'inclure que le strict minimum dans application.js
.
La plupart des packages que vous importez avec require
sont au format UMD (Universal Module Definition) ; autrement dit, en IIFE (Expression de Fonction Immédiatement Invoquée, on détaillera un peu plus ce qu'est l'UMD plus bas).
Par exemple, si on transforme notre knife.js
en IIFE, on aurait :
La fonction écrite est invoquée dès sa lecture, et son résultat est enregistré dans la variable knife
, sans que le scope global n'ait conscience de ce qu'il se passe à l'intérieur de cette fonction. Cela nous permet d'avoir toutes les fonctions de notre librairie knife.js
préférée dans cette variable globale knife
. Il y en a certains qui appellent leur librairie $
😁
La même fonction #cut
qu'on avait précédemment dans le scope global, se fera maintenant invoquer via knife.cut()
.
Il faut aussi savoir qu'un même module déclaré plusieurs fois n'est contacté qu'une fois.
Le module tools/knife.js
est require
à la fois par application.js
et par /ingredients/processed/sliced_onion.js
. Sprockets détecte que c'est le même fichier, et ne l'importera qu'une seule fois ; ce qui nous évite une collision de nom.
⚠️ Impossible d'utiliser les import/export de JS (ESM)
Si vous essayez de faire un import onion from './ingredients/onion.js'
, Vous rencontrerez ce genre d'erreur : Uncaught SyntaxError: import declarations may only appear at top level of a module
. Effectivement, si on concatène tout, difficile de faire en sorte que les import
soient toujours tout en haut du fichier. Idem pour les export
.
Cette syntaxe ESM est arrivée tard dans la vie du langage Javascript (ES6 en 2015), et bien après les débuts de Sprockets.
Donc dès que vous voulez modulariser votre JS (autrement dit, aller plus loin qu'ajouter des event listeners simples), avec Sprockets vous devrez le faire en IIFE ou sous le format UMD.
Comment ça se passe quand on délègue la compilation JS ou CSS à un autre outil ?
Sprockets ne permet donc pas d'utiliser la syntaxe ESM. Je vais détailler un peu sur ce sujet, car ça nous aidera à voir plus clair sur la modularisation en JS.
Comme on se retrouve souvent à faire cohabiter plusieurs types de modules, c'est d'autant plus utile.
Il existe plusieurs standards de modules pour JS, qui ont été créés en dehors du contexte de ce langage : AMD pour le front-end, CJS pour node, etc. Et depuis 2015, l'ESM, qui est le standard intégré au langage.
Vous trouverez très facilement une flopée d'articles sur comment marche quel type de module, je trouve celui-ci très clair.
En voici un petit résumé :
CJS (pour "Common JavaScript") : popularisé avec Node, il est utilisé pour un environnement de back-end et qui fonctionne en synchrone (il arrive souvent de voir les configurations webpack avec cette syntaxe par exemple).
UMD : marche en front et en back. Consiste en une IIFE pour vérifier quel système de module est disponible dans l'environnement actuel, et suivie d'une fonction anonyme qui va créer le dit module.
ESM : le standard du langage. Utilisable en back et en front, permet de charger ses modules de façon asynchrone, qu'ils soient des fichiers ou une URL.
C'est vers ce standard ESM que se tourne l'industrie. Pour importer un module ainsi, cela doit donc être un module ESM, ce qui n'est pas encore le cas de tout les packages. Cependant beaucoup de packages sont en train de subir cette transition (comme Stimulus récemment par exemple). En attendant, il faudra faire cohabiter les packages UMD et ESM ensemble pendant un moment.
À qui peut-on déléguer notre compilation ?
Les import-maps et la gem importmap-rails
La gem importmap-rails a été présentée comme un remplaçant de webpack pour Rails 7 en ce qui concerne le JS, mais ce n'est pas un remplaçant de Sprockets. C'est en fait le moyen le plus simple pour écrire du JS moderne dans Rails, en particulier en nous donnant la possibilité d'utiliser l'ESM, tout en profitant des fonctionnalités de Sprockets. Elle intègre la spécification des import-map dans Rails, nous offrant des helpers et des options de configuration.
On doit toujours déclarer ses assets JS dans le
config/manifest.js
Le JS est minifié par Sprockets
On peut toujours faire appel aux processors de Sprockets
Qu'est-ce qu'un import map alors ?
Si vous faites ceci dans un navigateur
import moment from "moment";
import { partition } from "lodash";
vous aurez une erreur, car la spécification ESM indique que le « module spécifier » (par exemple lodash
au-dessus) doit être soit :
un chemin absolu
un chemin relatif
une URL
lodash
« tout court » est appelé un « bare module spécifier ». Si vous fournissez au navigateur cet import map
alors le code JS agira finalement comme si vous aviez écrit
import moment from "/node_modules/moment/src/moment.js";
import { partition } from "/node_modules/lodash-es/lodash.js";
Conceptuellement, c'est juste un mapping de chemin logique. Sprockets et importmap-rails sont complémentaires.
Petit (gros) détail concernant notre environnement Rails : il ne faut pas utiliser de chemin relatif dans vos imports. Si vous ajoutez cela dans votre script
import { cut } from "./tools/knife"
le navigateur va littéralement chercher le module à cet endroit, sans passer par l'asset pipeline.
Ça fonctionnera en environnement local mais pas en production. La gem importmap-rails
nous permet alors de faire le lien entre les « modules spécifier » et l'asset pipeline via les pin
dans config/importmap
.
On reprend le même exemple que tout à l'heure, avec une différence : les fichiers JS seront dans app/javascript
au lieu de app/assets/javascript
.
En pratique, cela ne fait aucune différence, car Sprockets va résoudre les noms de fichiers via son asset path, qui par défaut va vérifier ces deux dossiers. C'est simplement une convention Rails ; le JS moderne va dans app/javascript
. On va aussi mettre de l'ERB et voir le résultat.
Quelques remarques sur la configuration des pins :
pin_all_from
ne va capter que les fichiers.js
donc notrejs.erb
ne sera pas « pinned ». Il faut donc « pin » cejs.erb
séparément.bien qu'on a un
js.erb
, on déclare un fichier.js
à la fois dansconfig/importmap.rb
etassets/config/manifest.js
. C'est « le résultat » qu'on déclare.
À quoi ressemblent nos fichiers JS compilés alors ? Eh bien si on met de côté la minification, ils sont identiques à la source, car nous ne faisons plus de concaténation dans un seul gros fichier. Il y a bien une seule différence, attendue, dans notre fichier potato.js.erb
qui devient
car on a interagi avec les processors de Sprockets via l'extension de fichier .erb
.
jsbundling-rails
La gem webpacker
avec Rails 6 était complètement indépendante de Sprockets ; on pouvait y gérer tous ses assets si on le voulait (pas uniquement le JS). Webpack est puissant, mais devient rapidement difficile à comprendre, à configurer et à debugger, même pour des cas d'utilisation simples. J'y ai moi-même rencontré pas mal de difficultés « fatigantes » ; et au vu des échos que j'ai pu voir sur les internets, je ne suis pas le seul. La gem jsbundling-rails quant à elle travaille toujours avec Sprockets, et sépare les rôles :
elle lui laisse la gestion du manifest des fichiers compilés (soit ceux qui ont un digest). On utilise donc son helper
javascript_include_tag
pour inclure un script dans une vue. On déclare aussi dans leconfig/manifest.js
de Sprockets les fichiers à compiler (par exemple, un simple//= link_tree ../builds
, qui sera la cible d'output du bundler)lors du lancement d'un
rails assets:precompile
, le scriptbuild
dans lepackage.json
sera lancé lui aussi.le traitement des assets JS sera entièrement géré par le bundler JS de son choix (compilation, transpilation, minification, ...). La configuration de ce bundler se fera dans un fichier à part (si besoin). Tout part de ce script
build
.
Autrement dit, jsbundling-rails
n'est qu'un adaptateur entre Sprockets et le bundler choisi. Le but est simplement de déléguer tous les besoins de compilation ou transpilation à des outils du monde JS qui sont plus adaptée, performants, et mieux maintenus que Sprockets. C'est un bon entre-deux entre l'asset pipeline qu'on connaît bien, et les possibilités de compilations JS modernes que nous offrent les bundlers actuels.
Pourquoi utiliser un bundler au lieu d'un(e) import-map(s) ?
Un bundler n'est utile que si vous avez un besoin auquel le JS natif ne peut pas répondre. Par exemple :
transpiler du JSX React ou du TypeScript
optimiser les performances avec du code splitting ou du tree shaking
modifier le code source au moment de la compilation, par exemple retirer des commentaires ou modifier la valeur de variables d'environnement
... (les options possibles dépendront du bundler choisi)
Sinon, vous pouvez simplement rester sur les import-maps.
Exemple :
Pour changer de l'application.js
, prenons une page HTML sur laquelle on importe un script (les fichiers JS restent les mêmes). On va aussi utiliser le bundler esbuild, qui a une belle offre d'options tout en étant simple et rapide.
et dans le package.json
Lors de son lancement, le script
build
dupackage.json
va lancer le bundleresbuild
, qui va prendre tout ce qui est dansapp/javascript/
, le compiler, générer les source-map (pour faciliter le débuggage), et mettre le résultat dansapp/assets/builds
. On peut le lancer manuellement (yarn build
) ou automatiquement avec l'option--watch
qui est incluse avec le scriptbin/dev
fourni par la gem.Lorsqu'on arrive sur la page, Sprockets va aller chercher
app/assets/builds/kitchen/worktop.js
, puisapp/assets/builds/tools/knife.js
etapp/assets/builds/ingredients/processed/slicedOnion.js
(car ils sontimport
és), et mettre à jour leur digest si le contenu a changéSi on lance la task
rails assets:precompile
, les fichiers avec leurs digest sont copiés danspublic/assets
Le fichier compilé sera au final sans dépendances, c'est-à-dire qu'il inclura tout ce dont il a besoin pour fonctionner. Voici ce que contiendra le kitchen/worktop.js
compilé :
cssbundling-rails
cssbundling-rails a exactement le même rôle et fonctionnement que jsbundling-rails : déléguer la compilation CSS à un outil dédié (sass, tailwind, ...), et récupérer les fichiers compilés dans app/assets/builds
. La seule différence à part sera le script de build en fonction du bundler CSS que vous aurez choisi.
Par exemple :
et la compilation se lancera via yarn build:css
ou bin/dev
.
Il est important de noter que Sprockets utilise initialement pour la compilation scss la gem sass-rails, qui dépend sur la gem dépréciée sassc. C'est donc une excellente raison de commencer à utiliser dès maintenant cssbundling-rails
(avec le bundler sass par exemple).
Exercice pratique avec toutes les options en même temps
La théorie c'est bien, mais rien ne vaut la pratique, donc voilà un petit exo rapide. Vous devriez avoir tout ce qu'il faut dans cet article pour le faire en environ une heure. On va reprendre les exemples JS présentés plus haut pour faire simple. Cela semblera être simpliste, mais personnellement ça m’a permis de mieux comprendre, en pratique, les relations entre ces outils.
Petite contrainte : interdiction d'utiliser l'application.js
pour y écrire le code. Je vous laisse faire le setup.
Option Sprockets JS
ajoutez un
javascript_include_tag "sprockets_exo"
dans votre vue, qui va injecter dans le DOM (ou un simpleconsole.log
) le contenu de la variable présente dans le fichieringredients/processed/slicedOnion.js
. Ce fichierslicedOnion.js
sera un module en IIFE, et vous devrez l'importer depuis votre fichiersprockets_exo.js
Option import-maps
ajoutez un
javascript_import_module_tag "importmap_exo"
dans votre vue, qui va injecter dans le DOM le contenu de la variable présente dans le fichieringredients/onion.js
. Ce contenu devra être auparavant traité avec la fonction#cut
d'un module ESMtools/knife.js
Option esbuild
ajoutez un
javascript_include_tag "esbuild_exo"
dans votre vue, qui va injecter dans le DOM le contenu de la variable présente dans le fichieringredients/potato.js
. Ce contenu devra être auparavant traité avec la fonctino#cut
d'un module ESMtools/knife.js
Résultat
Vous devriez avoir comme résultat d'affiché ['o', 'n', 'i', 'o', 'n']
, ['o', 'n', 'i', 'o', 'n']
et ['p', 'o', 't', 'a', 't', 'o']
.
Petit tips, si vous vous retrouvez coincé au moment de l'import maps, jetez un oeil au code source de #javascript_importmaps_tags
Pourquoi Rails 8 utilisera Propshaft ?
Propshaft, toujours en version 0 pour l'instant, va arriver avec Rails 8, et a pour vocation de remplacer Sprockets.
Sprockets a été créé en 2009 pour résoudre des problèmes d'un autre temps, est complexe et difficile à maintenir. Propshaft sera une évolution dans le sens de la simplicité : ses responsabilités sont réduites (délégation totale de la compilation CSS/JS), ce qui facilitera sa maintenance tout en disposant d'un outillage plus moderne.
On a donc un intérêt à utiliser dès maintenant ces nouvelles options de compilation, afin de réduire les efforts de migration lorsque Sprockets sera déprécié. Sprockets devrait être maintenu encore longtemps, donc il n'y a pas encore d'urgence à migrer.
Le job de tes rêves !
Comme vous le savez, derrière Ruby Biscuit, il y a Capsens 👋 , nous sommes une agence web spécialisée dans le Ruby on Rails depuis 10 ans.
Avec le temps on s'est rendu compte que beaucoup de dévs choisissent leur entreprise un peu par hasard alors qu'ils pourraient mieux s'épanouir et se valoriser dans des structures qui leur correspondent bien.
Ce qui tombe super bien c'est que chez Capsens nous avons une excellente connaissance de l'écosystème Ruby on Rails en France, avec un réseau d'entreprises considérable.
C'est pourquoi on t’annonce à travers cette newsletter que nous mettons à profit notre connaissance du métier pour t’aider à trouver le poste de vos rêves !
Concrètement :
Tu souhaites trouver le job de tes rêves ? Alors réponds à cet e-mail ! Tu peux dire "Coucou", ça suffit !
On te proposera aussitôt des créneaux pour une visio afin de faire connaissance
Puis nous te proposerons 3 entreprises qui correspondent à qui tu es. Et pour chacun de ces postes :
Tu pourras discuter avec un développeur de l'équipe (pas le recruteur lui-même) afin de savoir comment ça se passe de l'intérieur. No bullshit
Un développeur de Capsens t'aidera à :
Analyser le poste : ce qui a l'air bien, les dangers, quelles conditions poser pour que tout se passe bien
Te préparer pour que tu décroches le bon poste
Tu veux aller vers une vie professionnelle plus épanouie ? 😀 Eh bien, on attend ton e-mail ! Et si tu aimes déjà ton travail, ne nous contacte surtout pas ! Ou alors fais-le pour nous recommander ta boîte 😉
Si cette édition vous à plus, je compte sur vous pour la partager !
Tim et Mélanie