Générer des classes css avec un nom unique

May 23rd, 2015 | es6, javascript, react, webpack |

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 via my-class) dans l’objet importé, qu’on peut alors utiliser pour indiquer la className. 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 pas style={ 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_aFIC
css-loader?localIdentName=[path]-[name]-[local]-[hash:base64:5]
.src--App-container-3ImWI
  • path: le path du fichier javascript
  • name: le nom du fichier javascript
  • local: 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-loader

Tout 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!

Published by

ctheu

Hey. I love computer sciences almost since I'm born. I work with frontend and backend technologies; I can't make my mind ! Let's share some tips about how some of them work, shall we ?