Olá Habr!
Há pouco tempo, percebi que trabalhar com CSS em todos os meus aplicativos é uma dor para o desenvolvedor e o usuário.
Sob o corte estão meus problemas, um monte de códigos estranhos e armadilhas no caminho para trabalhar com estilos corretamente.
Problema de CSS
Nos projetos React e Vue que eu fiz, a abordagem dos estilos era aproximadamente a mesma. O projeto é montado pelo webpack, um arquivo CSS é importado do ponto de entrada principal. Esse arquivo importa dentro de si o restante dos arquivos CSS que usam o BEM para nomear classes.
styles/ indes.css blocks/ apps-banner.css smart-list.css ...
Isso é familiar? Eu usei essa implementação em quase todos os lugares. E tudo correu bem até que um dos sites cresceu a tal ponto que os problemas com os estilos começaram a irritar muito meus olhos.
1. O problema do hot-reloadA importação de estilos um do outro ocorreu através do plug-in postcss ou do carregador de pontas.
O problema é este:
Quando resolvemos as importações por meio do plug-in postcss ou do stylus-loader, a saída é um arquivo CSS grande. Agora, mesmo com uma pequena alteração em uma das folhas de estilo, todos os arquivos CSS serão processados novamente.
Realmente reduz a velocidade de recarga a quente: leva cerca de 4 segundos para processar ~ 950 Kbytes de arquivos de caneta.
Nota sobre o css-loaderSe a importação de arquivos CSS fosse resolvida através do css-loader, esse problema não teria surgido:
css-loader transforma CSS em JavaScript. Ele substituirá todas as importações de estilo por exigir. A alteração de um arquivo CSS não afetará outros arquivos e o hot-reload ocorrerá rapidamente.
Para css-loader
@import './test.css'; html, body { margin: 0; padding: 0; width: 100%; height: 100%; } body { background-color: red; }
Depois
2. O problema da divisão de códigoQuando os estilos são carregados de uma pasta separada, não conhecemos o contexto de uso de cada um deles. Com essa abordagem, não é possível dividir o CSS em várias partes e carregá-lo conforme necessário.
3. Grandes nomes de classes CSSCada nome de classe BEM se parece com: block-name__element-name. Um nome tão longo afeta fortemente o tamanho final do arquivo CSS: no site da Habr, por exemplo, os nomes de classes CSS ocupam 36% do tamanho do arquivo de estilo.
O Google está ciente desse problema e há muito tempo usa a minificação de nomes em todos os seus projetos:
Um pedaço de google.comTodos esses problemas me colocaram em ordem, finalmente decidi encerrá-los e alcançar o resultado perfeito.
Seleção de decisão
Para me livrar de todos os problemas acima, encontrei duas soluções: CSS nos módulos JS (styled-components) e CSS.
Não vi falhas críticas nessas soluções, mas no final minha escolha caiu nos Módulos CSS devido a várias razões:
- Você pode colocar CSS em um arquivo separado para armazenar em cache separado JS e CSS.
- Mais opções para estilos de aglomeração.
- É mais comum trabalhar com arquivos CSS.
A escolha é feita, é hora de começar a cozinhar!
Configuração básica
Configure um pouco o webpack. Adicione o css-loader e ative os Módulos CSS nele:
module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, } }, ], }, ], }, };
Agora vamos espalhar arquivos CSS em pastas com componentes. Dentro de cada componente, importamos os estilos necessários.
project/ components/ CoolComponent/ index.js index.css
.contentWrapper { padding: 8px 16px; background-color: rgba(45, 45, 45, .3); } .title { font-size: 14px; font-weight: bold; } .text { font-size: 12px; }
import React from 'react'; import styles from './index.css'; export default ({ text }) => ( <div className={styles.contentWrapper}> <div className={styles.title}> Weird title </div> <div className={styles.text}> {text} </div> </div> );
Agora que quebramos os arquivos CSS, o hot-reload processará apenas as alterações em um arquivo. Problema número 1 resolvido, felicidades!
Divida CSS em pedaços
Quando um projeto tem muitas páginas e o cliente precisa de apenas uma delas, não faz sentido distribuir todos os dados. O React possui uma excelente biblioteca carregável por reagentes para isso. Permite criar um componente que baixa dinamicamente o arquivo que precisamos, se necessário.
import Loadable from 'react-loadable'; import Loading from 'path/to/Loading'; export default Loadable({ loader: () => import('path/to/CoolComponent'), loading: Loading, });
O Webpack transformará o componente CoolComponent em um arquivo JS separado (bloco), que será baixado quando o AsyncCoolComponent for renderizado.
Ao mesmo tempo, CoolComponent contém seus próprios estilos. Até agora, o CSS está como uma string JS e é inserido como um estilo usando o carregador de estilos. Mas por que não cortamos os estilos em um arquivo separado?
Criaremos nosso próprio arquivo CSS para o arquivo principal e para cada um dos blocos.
Instale mini-css-extract-plugin e conjure com a configuração do webpack:
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ (isDev ? 'style-loader' : MiniCssExtractPlugin.loader), { loader: 'css-loader', options: { modules: true, }, }, ], }, ], }, plugins: [ ...(isDev ? [] : [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', chunkFilename: '[name].[contenthash].css', }), ]), ], };
Isso é tudo! Vamos montar o projeto no modo de produção, abrir o navegador e ver a guia de rede:
// GET /main.aff4f72df3711744eabe.css GET /main.43ed5fc03ceb844eab53.js // CoolComponent , JS CSS GET /CoolComponent.3eaa4773dca4fffe0956.css GET /CoolComponent.2462bbdbafd820781fae.js
O problema número 2 acabou.
Minificamos classes CSS
O Css-loader altera os nomes das classes dentro e retorna uma variável com o mapeamento dos nomes das classes locais para global.
Após nossa configuração básica, o css-loader gera um hash longo com base no nome e no local do arquivo.
No navegador, nosso CoolComponent agora fica assim:
<div class="rs2inRqijrGnbl0txTQ8v"> <div class="_2AU-QBWt5K2v7J1vRT0hgn"> Weird title </div> <div class="_1DaTAH8Hgn0BQ4H13yRwQ0"> Lorem ipsum dolor sit amet consectetur. </div> </div>
Claro, isso não é suficiente para nós.
É necessário que, durante o desenvolvimento, haja nomes para encontrar o estilo original. E no modo de produção, os nomes das classes devem ser minificados.
O Css-loader permite personalizar a alteração dos nomes das classes através das opções localIdentName e getLocalIdent. No modo de desenvolvimento, definiremos o localIdentName descritivo - '[caminho] _ [nome] _ [local]' e, para o modo de produção, criaremos uma função que reduzirá os nomes das classes:
const getScopedName = require('path/to/getScopedName'); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ (isDev ? 'style-loader' : MiniCssExtractPlugin.loader), { loader: 'css-loader', options: { modules: true, ...(isDev ? { localIdentName: '[path]_[name]_[local]', } : { getLocalIdent: (context, localIdentName, localName) => ( getScopedName(localName, context.resourcePath) ), }), }, }, ], }, ], }, };
E aqui temos o desenvolvimento de belos nomes visuais:
<div class="src-components-ErrorNotification-_index_content-wrapper"> <div class="src-components-ErrorNotification-_index_title"> Weird title </div> <div class="src-components-ErrorNotification-_index_text"> Lorem ipsum dolor sit amet consectetur. </div> </div>
E nas classes minificadas de produção:
<div class="e_f"> <div class="e_g"> Weird title </div> <div class="e_h"> Lorem ipsum dolor sit amet consectetur. </div> </div>
O terceiro problema foi superado.
Remova a invalidação desnecessária do cache
Usando a técnica de redução de classe descrita acima, tente criar o projeto várias vezes. Preste atenção aos caches de arquivos:
/* */ app.bf70bcf8d769b1a17df1.js app.db3d0bd894d38d036117.css /* */ app.1f296b75295ada5a7223.js app.eb2519491a5121158bd2.css
Parece que após cada nova compilação, invalidamos os caches. Como assim?
O problema é que o webpack não garante a ordem em que os arquivos são processados. Ou seja, os arquivos CSS serão processados em uma ordem imprevisível, diferentes nomes minificados serão gerados para o mesmo nome de classe com diferentes assemblies.
Para superar esse problema, vamos salvar os dados sobre os nomes de classe gerados entre assemblies. Atualize um pouco o arquivo getScopedName.js:
const incstr = require('incstr');
A implementação do arquivo generatorHelpers.js realmente não importa, mas se você estiver interessado, aqui está o meu:
generatorHelpers.js const fs = require('fs'); const path = require('path'); const getGeneratorDataPath = generatorIdentifier => ( path.resolve(__dirname, `meta/${generatorIdentifier}.json`) ); const getGeneratorData = (generatorIdentifier) => { const path = getGeneratorDataPath(generatorIdentifier); if (fs.existsSync(path)) { return require(path); } return {}; }; const saveGeneratorData = (generatorIdentifier, uniqIds) => { const path = getGeneratorDataPath(generatorIdentifier); const data = JSON.stringify(uniqIds, null, 2); fs.writeFileSync(path, data, 'utf-8'); }; module.exports = { getGeneratorData, saveGeneratorData, };
Os caches se tornaram os mesmos entre as compilações, está tudo bem. Outro ponto a nosso favor!
Remova a variável de tempo de execução
Como decidi tomar uma decisão melhor, seria bom remover essa variável com o mapeamento de classes, porque temos todos os dados necessários no estágio de compilação.
Babel-plugin-react-css-modules nos ajudará com isso. Em tempo de compilação, ele:
- Encontrará a importação de CSS no arquivo.
- Ele abrirá esse arquivo CSS e alterará os nomes das classes CSS, exatamente como o css-loader faz.
- Ele encontrará nós JSX com o atributo styleName.
- Substitui nomes de classes locais de styleName por nomes globais.
Configure este plugin. Vamos brincar com a configuração babel:
Atualize nossos arquivos JSX:
import React from 'react'; import './index.css'; export default ({ text }) => ( <div styleName="content-wrapper"> <div styleName="title"> Weird title </div> <div styleName="text"> {text} </div> </div> );
Então, paramos de usar a variável com a exibição de nomes de estilos, agora não temos!
... Ou existe?
Vamos coletar o projeto e estudar as fontes:
function(e, t, n) { e.exports = { "content-wrapper": "e_f", title: "e_g", text: "e_h" } }
Parece que a variável ainda está lá, embora não seja usada em nenhum lugar. Por que isso aconteceu?
O webpack suporta vários tipos de estrutura modular, os mais populares são o ES2015 (importação) e o commonJS (requerido).
Os módulos do ES2015, diferentemente do commonJS, suportam o
tremor de árvores devido à sua estrutura estática.
Mas o carregador de css e o carregador de mini-css-extract-plugin usam a sintaxe commonJS para exportar nomes de classes, para que os dados exportados não sejam excluídos da construção.
Escreveremos nosso pequeno carregador e excluiremos os dados extras no modo de produção:
const path = require('path'); const resolve = relativePath => path.resolve(__dirname, relativePath); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ ...(isDev ? ['style-loader'] : [ resolve('path/to/webpack-loaders/nullLoader'), MiniCssExtractPlugin.loader, ]), { loader: 'css-loader', }, ], }, ], }, };
Verifique o arquivo montado novamente:
function(e, t, n) {}
Você pode expirar com alívio, tudo funcionou.
Falha ao excluir a variável de mapeamento de classeNo começo, parecia-me mais óbvio usar o pacote de
carregador nulo já existente.
Mas tudo acabou não sendo tão simples:
export default function() { return '// empty (null-loader)'; } export function pitch() { return '// empty (null-loader)'; }
Como você pode ver, além da função principal, o null-loader também exporta a função de afinação. Aprendi
com a documentação que os métodos de pitch são chamados mais cedo que outros e podem cancelar todos os carregadores subsequentes se eles retornarem alguns dados desse método.
Com um carregador nulo, a sequência de produção CSS começa a ter a seguinte aparência:
- O método de pitch do null-loader é chamado, que retorna uma string vazia.
- Devido ao retorno do método de pitch, todos os carregadores subsequentes não são chamados.
Eu não via mais as soluções e decidi fazer meu próprio carregador.
Use com Vue.jsSe você tem apenas um Vue.js na ponta dos dedos, mas realmente deseja compactar os nomes das classes e remover a variável de tempo de execução, então eu tenho um ótimo hack!
Tudo o que precisamos é de dois plugins: babel-plugin-transform-vue-jsx e babel-plugin-react-css-modules. Vamos precisar do primeiro para escrever JSX nas funções de renderização e do segundo, como você já sabe, para gerar nomes no estágio de compilação.
module.exports = { plugins: [ 'transform-vue-jsx', ['react-css-modules', {
import './index.css'; const TextComponent = { render(h) { return( <div styleName="text"> Lorem ipsum dolor. </div> ); }, mounted() { console.log('I\'m mounted!'); }, }; export default TextComponent;
Compactar CSS ao máximo
Imagine o seguinte CSS apareceu no projeto:
.component1__title { color: red; } .component2__title { color: green; } .component2__title_red { color: red; }
Você é um minificador de CSS. Como você apertaria?
Eu acho que sua resposta é algo como isto:
.component2__title{color:green} .component2__title_red, .component1__title{color:red}
Agora vamos verificar o que os minificadores comuns farão. Coloque nosso pedaço de código em algum
minifier online :
.component1__title{color:red} .component2__title{color:green} .component2__title_red{color:red}
Por que ele não podia?
O minificador tem medo de que, devido a uma alteração na ordem da declaração de estilos, algo ocorra. Por exemplo, se o projeto tiver este código:
<div class="component1__title component2__title">Some weird title</div>
Por sua causa, o título ficará vermelho e o minificador online deixará a ordem correta da declaração de estilo e ficará verde. Obviamente, você sabe que nunca haverá a interseção de component1__title e component2__title, porque eles estão em componentes diferentes. Mas como dizer isso ao minifier?
Depois de pesquisar a documentação, descobri a capacidade de especificar o contexto para o uso de classes apenas com o
csso . E ele não tem uma solução conveniente para o webpack pronto para uso. Para ir mais longe, precisamos de uma bicicleta pequena.
Você precisa combinar os nomes das classes de cada componente em matrizes separadas e fornecer dentro do csso. Um pouco antes, geramos nomes de classes minificados de acordo com este padrão: '[componentId] _ [classNameId]'. Assim, os nomes das classes podem ser combinados simplesmente pela primeira parte do nome!
Aperte o cinto e escreva o seu plugin:
const cssoLoader = require('path/to/cssoLoader'); module.exports = { plugins: [ new cssoLoader(), ], };
const csso = require('csso'); const RawSource = require('webpack-sources/lib/RawSource'); const getScopes = require('./helpers/getScopes'); const isCssFilename = filename => /\.css$/.test(filename); module.exports = class cssoPlugin { apply(compiler) { compiler.hooks.compilation.tap('csso-plugin', (compilation) => { compilation.hooks.optimizeChunkAssets.tapAsync('csso-plugin', (chunks, callback) => { chunks.forEach((chunk) => {
const csso = require('csso'); const getComponentId = (className) => { const tokens = className.split('_');
E não foi tão difícil, certo? Geralmente, essa minificação também comprime CSS em 3-6%.
Valeu a pena?
Claro.
Nos meus aplicativos, finalmente apareceu um hot-recarregamento rápido, e o CSS começou a se fragmentar e pesar em média 40% menos.
Isso agilizará o carregamento do site e reduzirá o tempo de análise de estilos, o que afetará não apenas os usuários, mas também os CEOs.
O artigo cresceu bastante, mas fico feliz que alguém tenha conseguido rolar o texto até o final. Obrigado pelo seu tempo!
Materiais utilizados