A l’heure où l’on parle de micro-services, où on cherche à tout modulariser, et à créer des systèmes à base de dépendances explicites, à ne plus rien mettre dans le scope global
et à utiliser le scope local
uniquement, histoire de maîtriser ce à quoi on a accès et d’éviter des effets de bord : on utilise encore un moteur CSS où on balance tous les sélecteurs à sa racine, qui utilise donc un scope global
. Heureusement, pour ceux qui utilise le shadow DOM, ce problème est résolu. Mais quid de ceux qui ne l’utilise pas ?
Une partie de ce problème peut être évité en adoptant des normes d’écriture et de nommage telle que la norme BEM qui peuvent malgré tout ne pas suffir ou être délicate à utiliser.
Enfin, surtout avec BEM, les noms de classes peuvent être à rallonge, on aimerait bien généraliser le process de minification qu’on utilise sur le contenu des .js
et des .css
, sur le nom des classes elles-même ! Mais cela oblige à modifier d’une part le nom de la classe dans le fichier .css et également le code js qui l’utilise.
Il est même maintenant possible de complétement se passer de fichier .css et d’utiliser uniquement du style inline (quid des performances ?). Par exemple, avec React, le style peut être défini directement dans les composants javascript, et avec des outils tel que Radium on peut même utiliser les sélecteurs spéciaux css tel que :hover
, ou les média queries.
Facebook et le CSS
Facebook a déjà résolu ce problème. Sans doute avez-vous vu la présentation de où il évoque comment Facebook s’assure qu’il n’y a aucun conflit de nom de classe css, que le scope global n’est pas pollué, et que les développeurs peuvent facilement rajouter du style sans avoir peur d’avoir des effets de bord et de modifier le layout quelque part, sans le savoir.
Ils ont étendu le langage css en rajoutant une syntaxe spéciale pour les sélecteurs : button/container
qui ne peut être utilisé que dans le fichier button.css
qui a son tour est référence dans un composant React button.js
, qui enfin, fait référence à className={cx('button/container')}
pour définir la classe d’un élément.
Le process de build vérifie ces références et génére un nom de classe unique à partir de button/container
(qui n’est pas valide en CSS) par quelque-chose comme ._f8z
qui fera parti du scope css global mais qui n’entrera jamais en conflit avec quoi que ce soit vu que le nom est généré aléatoirement (et est unique), personne ne pouvant le deviner à l’avance. Tout le monde doit donc utiliser ce système pour travailler et styler son contenu.
Moi aussi j’aimerai faire ça. Ca tombe bien, webpack est là.
webpack et css-loader
webpack, et en particulier le plugin css-loader combiné à extract-text-webpack-plugin, permet de former un (ou des) bundle css à partir de fichiers .css
.less
ou .sass
(avec les loaders qui vont bien) qui sont eux-même importés dans des fichiers .js :
import React from 'react'; import './App.less'; export default class extends React.Component { render() { return } }
Avec une configuration webpack de ce genre :
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: './src/app.js', output: { path: __dirname + '/dist', filename: 'bundle.js' }, loaders: [ { test: /\.js$/, exclude: /node_modules/, loaders: [ 'babel-loader' ], }, { // we can do: import './Component.css'; test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') }, { // we can do: import './Component.less'; test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader') } ] }, plugins: [ new ExtractTextPlugin('[name].css') ] };
Ce build permet de créer un bundle css qui contiendra tout le contenu référencé par les imports de fichiers .css
ou .less
dans les fichiers javascript.
import './App.less';
css-loader local scope
La nouveauté (d’avril 2015) est une nouvelle syntaxe au niveau des fichiers css pas encore transformés, mais prise en compte par css-loader
:
:local(.container) { font-size: 30px; }
Se transforme en :
._3ImWIJ65ktg-PxiyA_aFIC { font-size: 30px; }
Cela signifie que vous ne pouvez plus utiliser simplement className="container"
, cette classe css n’existe plus. Et vous ne pouvez pas mettre className="_3ImWIJ65ktg-PxiyA_aFIC"
quand même ! Il faut importer différemment le fichier .css
ou .less
:
import AppStyles from './App.less'; ...Les classes définies dans le fichier css seront accessibles via leur nom (si
:local(.my-class)
alors viamy-class
) dans l’objet importé, qu’on peut alors utiliser pour indiquer laclassName
. Au niveau HTML, on aura ce rendu :Autre exemple avec un autre sélecteur css à l’intérieur :
:local(.container) { font-size: 31px; span { letter-spacing: 5px; } }Cela génère bien :
._3ImWIJ65ktg-PxiyA_aFIC { font-size: 31px; } ._3ImWIJ65ktg-PxiyA_aFIC span { letter-spacing: 5px; }Au niveau du bundle
AppStyles
est en fait simplement un dictionnaire généré et injecté à la volée de la forme :module.exports = { "container":"_3ImWIJ65ktg-PxiyA_aFIC" };className et pas style
Attention à bien utiliser
className={ AppStyle.container }
et passtyle={ AppStyle.container }
sans quoi l’erreur suivante se produirait :Uncaught Error: Invariant Violation: The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.Le nom des classes générées
On peut modifier la manière dont le nom des classes sont générés. Par défaut, il s’agit d’un hash comme on peut voir, mais on peut le modifier de la sorte (via webpack.config.js) :
css-loader?localIdentName=[hash:base64] _3ImWIJ65ktg-PxiyA_aFICcss-loader?localIdentName=[path]-[name]-[local]-[hash:base64:5] .src--App-container-3ImWI
path:
le path du fichier javascriptname:
le nom du fichier javascriptlocal:
le nom utilisé dans le fichier css:local(.container)
Il n’y a malheureusement pas de méthodes (pour l’instant) pour générer des noms du genre
._a
,._b
, …._az
histoire de rester ultra court et unique (étant un simple compteur).La gestion de cet identifiant provient du module webpack loader-utils.
Plusieurs imports
Si un fichier Javascript dépend de plusieurs fichiers de styles et qu’un, il faut simplement concaténer le nom des classes provenant des 2 imports :
import AppStyle from './App.less'; import GlobalStyle from './Global.less'; ... className={GlobalStyle.container + ' ' + AppStyle.container}Plus loin
D’autres fonctionnalités sont disponibles, comme l’héritage de règles ou la définition de règle globale via
:global
, je vous conseille donc d’aller lire la page du projet : https://github.com/webpack/css-loaderTout cela est encore récent et en développement (allant même à provoquer des breaking changes, des changements de syntaxe), ce qui la rend instable à utiliser pour l’instant, attention!