CSS Modules: Módulos no seu CSS hoje

Pedro Tacla Yamada
Tableless
Published in
6 min readDec 3, 2015

Há algumas semanas tive a oportunidade de estar na conferência reactive2015, uma conferência sobre React.js e FRP (Functional Reactive Programming). Entre os vários tópicos muito interessantes e demos incríveis de tecnologias emergentes como Elm, cycle.js e React Native (com o surpreendente React Native Playground — um jsfiddle para apps mobile), algo me chamou muito a atenção como um game-changer para o stack front-end em 2015. Foi a palestra "The Case for CSS Modules" por Mark Dalgleish (você pode conferir a talk aqui).

Nesse (breve) post tentarei dar um overview do que é webpack e da implementação de CSS Modules no css-loader. Essa é uma implementação do spec CSS Modules, que funciona hoje.

Se você já é familiar com o webpack, pule para a segunda parte.

webpack & browserify

Esse tipo de avanço só é tão palpável por causa do build-system webpack. Portanto, em primeiro lugar, o que é webpack? Traduzida livremente para o português, a descrição do projeto no GitHub:

Um bundler para javascript & co. Empacota muitos módulos em alguns assets compilados. Divisão de código permite que partes diferentes da aplicação sejam carregadas sob demanda. Por meio de “loaders” módulos podem ser CommonJs, AMD, módulos ES6, CSS, Imagens, JSON, Coffeescript, LESS, … e suas coisas customizadas.

A princípio, o webpack é um bundler de módulos como o browserify, com o qual espero que a maioria de vocês devem ser familiares. Instalamos a ferramenta com:

npm install -g webpack

E então, dado um arquivo:

// index.js
var $ = require('jquery');
$(function() { alert('hello world'); });

E o jquery disponível por meio do NPM, podemos rodar:

webpack ./index.js bundle.js

E o webpack vai resolver nossos módulos e combinar tudo em um só arquivo.

Usado dessa forma, o webpack funciona exatamente como o browserify, recebe um módulo e emite um bundle. Mas a ferramenta é capaz de muito mais. No browserify temos "transforms" para aplicar transformações de código em um módulo quando damos require nele. Isso é necessário se estamos usando uma linguagem compilada para JavaScript, como CoffeeScript. No webpack há uma API um pouco mais complexa para a definição de "loaders" que fazem algo parecido, mas que têm — por convenção, suporte e esforço da comunidade — muito mais poder. Usando webpack podemos explicitar qual loader queremos usar para um módulo a nível do código e que opções queremos passar para ele com: require('nomeDoLoader?opcaoA=valorA?opcaoB=valorB!./modulo'). Isso extende a capacidade do que sua aplicação pode tratar como "módulos", já que eles não precisam mais ser só arquivos com código.

Há loaders suportados para usar arquivos ES6, CoffeeScript, Haskell (!!), arquivos de texto (lidos como uma String) e… CSS (entre muitas outras coisas). Em um setup mínimo, quando importamos um arquivo CSS, usamos o style-loader. Sem estar configurado, ele insere o arquivo CSS inteiro como uma String no seu "bundle" e assim que o require é executado no navegador, insere uma tag <style> no <head> da página com o código. Isso tem implicações de performance, mas elas são contornáveis usando outras funcionalidades da ferramenta que não serão discutidas aqui (veja https://github.com/webpack/extract-text-webpack-plugin).

require('style!./meuestilo.css');
alert('Agora a página tem CSS');

Mas algo fica em aberto. Nesse setup, quando chamamos require, o que a função retorna?

var wat = require('style!css!./meuestilo.css');

Grande parte do poder do webpack está na sua capacidade de concatenar vários "loaders" em sequência; algo como um pipe do UNIX, uma série de transformações/operações que queremos executadas sobre um arquivo. O style-loader toma conta de inserir o CSS ná página em runtime ou extrair todos os seus estilos em um só arquivo e o css-loader se preocupa em:

  • Repassar imports e urls no seu CSS para o webpack, para que eles também passem pela sua pipeline e sejam incluídos nos assets emitidos
  • Adicionar a capacidade de exportar símbolos do seu CSS para quem chama require('style!css!./estilo.css') (isso é o que precisamos!)

O loader adiciona a sintaxe :local .classe { /**/ } para o seu arquivo CSS. Quando escrevemos:

/* Componente.css */
:local .header {
font-size: 24px;
}

O sistema vai gerar um identificador único para essa classe .header e transformar o CSS em:

/* Componente-transformado.css */
.asdfasdfClasseGerada123123 {
font-size: 24px;
}

A classe é então retornada para quem chamar require:

// Componente.js
var classesGeradas = require('style!css!./Componente.css');
// => { header: '.asdfasdfClasseGerada123123' }
// podemos a usar com:
$(algumElemento).addClass(classesGeradas.header);

Incrível certo?!

Exceto quando você tem um bug e o devTools te mostra uma sopa de classes .asdfasdf e .123123. Podemos resolver isso usando o parâmetro localIdentName para o css-loader.

Com style!css?localIdentName=[name]__[local] temos uma boa emulação da notação BEM.

O require usando essa funcionalidade:

// Componente.js
var classesGeradas = require('style!css?localIdentName=[name]__[local]!./Componente.css');
// ... nada muda

E o CSS gerado fica:

/* Componente-transformado.css */
.Componente__header {
font-size: 24px;
}

Isso é muito interessante, traz uma forma flexível e legível de introduzir escopo local ao CSS e efetivamente "módulos CSS" no contexto do JavaScript. Mas temos alguns problemas:

  • Os "módulos" até aqui só existem na terra JavaScript
  • Temos que escrever :local em todo o lugar
  • Não é claro a nível do CSS como extender duas classes definidas em arquivos diferentes

Se quisermos definir .smallHeader que extende essa classe, temos que quebrar o encapsulamento do módulo e tratar as duas classes a nível do JavaScript:

/* ComponenteSmall.css */
:local .smallHeader {
font-size: 10px;
}

E o JavaScript:

// ComponenteSmall.js
var classesGeradas = require('style!css!./Componente.css');
var classesGeradasSmall = require('style!css!./ComponenteSmall.css');
$(algumElemento).addClass(classesGeradas.header + ' ' + classesGeradasSmall.smallHeader);

Isso é solucionável usando o parâmetro modules no css-loader, que ativa o spec css-modules. Ativá-lo muda duas coisas fundamentais.

Primeiro, o escopo padrão é o escopo local. Então não precisamos mais de :local, que é substituído por :global. Vale apontar que essas "keywords" podem ser aplicadas tanto como :global .classGlobal1 .classeGlobal2 {} quanto como .classeLocal1 :global(.classeGlobal) .classeLocal2.

O exemplo fica:

/* ComponenteUsandoCSSModules.css */
.header {
font-size: 24px;
}

Segundo, introduz a ideia de composição com a propriedade de CSS composes: classeLocal from './estilo.css';. Assim, podemos importar outros stylesheets em um módulo CSS e o "loader" vai tomar conta de inteligentemente exportar as duas classes para seu JavaScript. Então, .smallHeader seria definido como:

/* ComponenteSmallUsandoCSSModules.css */
.smallHeader {
font-size: 10px;
composes: header from './ComponenteUsandoCSSModules.css';
}

E o JavaScript agora tem acesso a composição dos dois estilos:

// ComponenteSmallUsandoCSSModules.js
var classesGeradasSmall = require('style!css?modules?localIdentName=[name]__[local]!./ComponenteSmallUsandoCSSModules.css');
// { smallHeader: 'ComponenteUsandoCSSModules__header ComponenteSmallUsandoCSSModules__smallHeader' }

O pipeline do require é:

  • ComponenteSmallUsandoCSSModules.css (O módulo importado)
  • css?modules?localIdentName=[name]__[local] (O css-loader configurado para gerar classes em estilo BEM e ativar o spec css-modules, retornando as classes para o "caller")
  • style (O style-loader responsável por inserir os tags <style> em runtime ou os extrair em tempo de compilação)

Uso em produção

O spec provavelmente irá mudar com o tempo e é arriscado usar esse tipo de ideia no seu código hoje. Isso posto, acabo de começar a usar essa ideia onde eu trabalho, na toggl.com. Acredito que entre escolher uma linguagem que compila para CSS ou algo que o CSS possa vir a ser, a escolha por usar o que será o futuro é muito mais segura — o mesmo vale quando estamos compilando para as outras linguagens da Web. Há um exemplo mais completo de uma configuração do webpack que retira a necessidade de especificar todos os loaders em cada require em: https://github.com/css-modules/webpack-demo.

Se você não usa o webpack hoje, acho que vale o esforço de portar partes novas da sua aplicação para o usar. É uma ferramenta muito capaz, que traz benefícios para sua aplicação e seu processo de desenvolvimento que seriam muito difíceis de atingir de outras formas. "CSS Modules" é uma delas, mas há outras. Uma das que faz parte hoje da Toggl e também é usada no front-end web do Instagram é a capacidade de carregar a aplicação de forma separada. Primeiro todos os scripts para uma parte principal são baixados e executados, e, uma vez que a página esteja funcionando, o resto é baixado e executado.* No nosso caso, o minificador também gerou bundles cerca de 20% menores do que o UglifyJS2 usado de forma separada (algo relacionado a tree-shaking, não sei ao certo; foram KBs de graça de todo jeito).

Separar o CSS em módulos é extremamente útil para quem:

  • está desenvolvendo uma SPA e quer ter o máximo de reuso e organização de estilos
  • está desenvolvendo uma biblioteca de componentes e quer exportar uma funcionalidade de "temas" (o componente recebe a classe, mas a classe é importada do pacote, não definida pelo desenvolvedor)

Acho que os recursos linkados tem explicações melhores e mais extensas, apesar de serem em inglês. Me siga no Medium para outros posts sobre desenvolvimento web.

no GitHub: https://github.com/yamadapc

no twitter: https://twitter.com/yamadapc

P.S. Se se interessar por linguagens de programação funcional, há um esforço em cultivar uma comunidade brasileira da linguagem de programação Haskell em: http://haskellbr.com/

*No caso da Toggl usamos preloading de todos os assets para que os scripts sejam baixados em paralelo assim que o browser tiver acesso ao HTML

--

--