CSS

28 mai, 2015

Efeitos colaterais no CSS

Publicidade

Parece que quase todo dia eu leio uma nova e brilhante forma como as pessoas estão escrevendo CSS. Muitas dessas “novas” formas não são tão novas assim, mas variações de uma ou mais metodologias conhecidas, com algumas mudanças.

Bem, eu certamente não sou contra a mudança, muito menos contrário a fazer melhorias nas boas práticas atuais. Mas se você vai fazer mudanças, elas devem ser para melhor. E, do que eu tenho visto, muitas dessas novas metodologias não são melhores; no mínimo, elas são um passo para trás.

Há muito tempo que falo que a melhor metodologia para escrever CSS é BEM, e já que muitas dessas novas metodologias são baseadas em BEM, fiz questão de dar uma olhada nelas. O que eu descobri é que a maioria comete os mesmos erros que eu quando comecei a escrever CSS modular. Elas pegam as partes favoritas das metodologias populares e fazem um mix de tudo para formar as suas abordagens próprias, personalizadas.

O problema de misturar características BEM com outras não-BEM é que geralmente isso destrói a rede de segurança que BEM cria. BEM é efetiva não porque te oferece um monte de opções, mas porque limita o que você pode fazer. Isso previne que você dê um tiro no pé. Todas as regras de BEM existem por uma boa razão, e se você vai sugerir alguma alteração, é melhor saber quais são essas razões.

Muitos frameworks oferecem estrutura e organização para o seu código, e consistência para os nomes de classe. Se você acha que é isso que BEM faz, está deixando passar a sua característica mais importante.

BEM é diferente porque faz uma promessa: se você segue todas as regras, vai evitar o pior problema no CSS: fazer com que as regras correspondam aos elementos que você quer, sem que haja uma correspondência acidental com elementos que você não quer.

O pior problema no CSS

Existem dois tipos de problemas no CSS: cosméticos e arquiteturais.

Problemas cosméticos – questões como centralização vertical ou colunas de alturas iguais – geralmente geram as reclamações mais “barulhentas”, mas quase nunca são críticos. São irritantes, claro, mas não destroem nada.

Por outro lado, problemas arquiteturais podem causar grandes problemas no desenvolvimento. Eu me lembro de casos distintos, em cada uma das empresas nas quais trabalhei, quando atrasamos o desenvolvimento de uma nova feature devido ao medo de fazer quaisquer mudanças no CSS.

CSS é algo global e cada regra escrita tem o potencial de afetar totalmente partes não relacionadas do site [1]. É essa imprevisibilidade que torna o ato de escrever um bom CSS tão difícil.

Se eu tivesse que escolher entre contratar um designer fantástico que conseguisse replicar até os mais complexos desafios visuais facilmente no código, e um outro profissional que compreendesse as nuances de um CSS previsível e sustentável, eu escolheria o segundo sem a menor dúvida.

Os problemas cosméticos ficam quase invisíveis quando comparados aos arquiteturais, e o problema arquitetural mais difícil de todos é como fazer a prevenção de estilos de correspondência inesperados e indesejados.

Para falar em termos com os quais os programadores estão mais familiarizados: o problema mais difícil no CSS é eliminar os efeitos colaterais.

Os efeitos colaterais no CSS

Em Ciência da Computação, você diz que uma função tem efeitos colaterais se, além de retornar um valor, ela modifica algum estado do mundo exterior.

Para colocar isso de forma mais geral, os efeitos colaterais descrevem o fenômeno em que algo que parece afetar só as coisas de um alcance muito limitado na verdade afeta uma gama muito mais ampla, e faz isso de uma forma que pode não ser óbvia para a pessoa que executa a ação.

Como todas as regras CSS vivem no âmbito global [1], os efeitos colaterais são extremamente comuns. E uma vez que sua folha de estilos média consiste, geralmente, em uma coleção extremamente frágil de regras altamente acopladas, todos são dependentes intimamente da presença, da ordem e da especificidade de outras regras, mesmo as mudanças mais despretensiosas podem ter consequências imprevisíveis.

No CSS, os efeitos colaterais vêm em três formas principais:

  • Mudanças de regras de base
  • Nomeação de colisões
  • Correspondências de sub-árvore

Mudanças nas regras de base

Os desenvolvedores têm que usar tags HTML para escrever HTML, e há um número limitado de tags para escolher [2]. Embora possa ser tentador definir uma série de estilos de base usando seletores de tag (tecnicamente eles são chamados de tipo seletores), para evitar ter que adicionar classes para todos os seus elementos de conteúdo, isso cria, necessariamente, uma dependência não declarada entre essas regras e todos os seus componentes.

Isso não costuma parecer uma grande coisa ao construir primeiramente um site; na verdade, parece natural e DRY. Você cria uma base, estilos fundamentais (margens, tamanhos de fonte, cores etc.) e, em seguida, seus componentes constroem em cima deles, para que eles não tenham que reescrever as regras comuns.

O problema é que nessa abordagem você só economiza tempo se nunca alterar as suas regras básicas. Mas, na prática, o design do site pode e deve mudar. Você pode optar por fazer o tamanho da fonte de seus cabeçalhos um pouco maior, ou usar diferentes margens padrão em seus parágrafos, ou talvez perceba que você prefere bordas em vez de sublinhados para links. Se os seus componentes .article-title, .alert-content e .footer-link dependem dessas regras básicas, você vai logo perceber o quão frágil e acoplado seu código é.

Se os seus componentes dependem de estilos de base, então mudanças para eles vão exigir a verificação de todo o seu site para garantir que tudo ainda pareça estar certo.

Colisões de nomenclatura

O CSS não vai avisá-lo se você usar um seletor de classe que já existe em sua folha de estilos. Na verdade, a capacidade de substituir as regras é uma das características da linguagem. Como resultado, sem uma convenção para evitar isso, ou uma verificação em tempo de compilação para se proteger, não existe nenhuma boa maneira de ter certeza de que a classe que você escolheu já não foi escolhida por outra pessoa.

Quando vários desenvolvedores estão se comprometendo com a mesma base de código, as chances de duas pessoas escolherem o mesmo nome e não saberem é extremamente alta. Isso é especialmente verdadeiro nas opções de nomes comuns como “botão”, “conteúdo” ou “título”.

E isso não é apenas um problema com nomes de classe de nível superior. Como eu vou mostrar na próxima seção, escolher o mesmo nome em uma sub-árvore pode ser tão perigoso, se não mais.

Correspondências de sub-árvore

Muitos desenvolvedores já conhecem as duas formas anteriores de efeitos colaterais do CSS, por isso muitas vezes você vai ver as pessoas usando um combinator descendente para limitar o âmbito das regras que estão escrevendo (por exemplo #homepage .header ou .some-widget .title).

Mesmo esta abordagem sendo um pouco mais segura, ela ainda pode produzir os efeitos colaterais. Como eu sugeri acima, a aparência de segurança pode realmente tornar esta prática mais arriscada.

Limitar o escopo de um seletor para uma sub-árvore DOM em particular garante que isso não afetará elementos fora dessa sub-árvore. O problema é que não há garantias de que isso não vai afetar involuntariamente elementos dentro da mesma sub-árvore.

Considere o seguinte exemplo:

/* in article.css */
.article .title {
  border-bottom: 1px solid;
  font-size: 1.5em;
}

/* in widget.css */
.widget .title {
  color: gray;
  text-transform: uppercase;
}

Embora seja verdade que os elementos .title que não estão dentro de sub-árvores .article ou .widget não terão qualquer um desses estilos, ainda há a possibilidade de que um elemento .title estará dentro de ambas sub-árvores .article e .widget ao mesmo tempo.

Dado o CSS acima, o título do widget neste exemplo vai renderizar com um limite inferior inesperado:

<!-- The .article module -->
<article class="article">
  <h2 class="title">Article Title</h2>
  <div class="content">
    <p>…</p>

    <!-- The .widget module -->
    <form class="widget">
      <h2 class="title">Widget Title</h2>
      <fieldset>…</fieldset>
    </form>

  </div>
</article>

No mundo real do desenvolvimento, as estruturas de HTML são complexas e, se todos em sua equipe estão escrevendo CSS dessa forma, é só uma questão de tempo antes de dois deles escolherem o mesmo nome e colocá-lo na mesma sub-árvore.

Gostaria de salientar também que o uso de seletores de tipo de escopo torna esse problema muito pior. Regras de escrita como .article h3 estão apenas pedindo para ter problemas.

Como BEM elimina os efeitos colaterais

Eu disse acima que todas as regras CSS são globais, e cada regra tem o potencial de entrar em conflito com qualquer outra na página. Isso significa que os efeitos colaterais não podem ser prevenidos pela linguagem; no entanto, podem ser evitados por meio de uma convenção de nomenclatura disciplinada e aplicável. E isso é exatamente o que BEM fornece.

  • Mudanças de regras de base: Convenções rigorosas de BEM exigem o uso exclusivo dos seletores de classe. Você começa com uma reposição global e aí usa blocos para denominar tudo na página. Em outras palavras, adicionar uma classe a um elemento é a única maneira de denominá-lo, o que significa que todo estilo é opt-in. Os blocos encapsulam todo o seu estilo e não contam com nenhuma dependência externa. [3]
  • Colisões de nomenclatura: Em BEM, cada seletor de classe ou é o próprio nome do bloco ou usa o nome do bloco como um prefixo, e as regras para cada bloco moram em seu próprio arquivo dedicado. Uma vez que os sistemas de arquivos não permitem que dois arquivos tenham o mesmo nome, o sistema operacional está realmente ajudando a evitar a duplicação acidental. Se você seguir todas as convenções de nomenclatura BEM, e garantir que todo código de bloco resida em seu próprio arquivo, você nunca terá colisões de nomenclatura.
  • Correspondências de sub-árvore: A sub-árvore correspondente ao exemplo na seção anterior usou os seletores .article .title e .widget .title. Uma vez que o nome da classe “título” foi usado em ambos os casos, há um risco de correspondência da sub-árvore. BEM evita esse risco, exigindo que todas as classes de elemento sejam prefixadas com o nome do bloco. Os equivalentes BEM para esses dois seletores de título seriam .Article-title e .Widget-title (ou .article__title e .widget__title, dependendo de sua preferência) [4]. Como os nomes de classe são diferentes, seus estilos nunca vão entrar em conflito e, portanto, é impossível ter correspondências de sub-árvore não intencionais.

Reforçando convenções

Uma adesão rigorosa às convenções BEM vai evitar efeitos colaterais no CSS, mas como se certificar de que as convenções sempre são seguidas? Se o ressurgimento de efeitos colaterais pode ocorrer devido a algo tão simples como um novo desenvolvedor não conhecer (ou compreender plenamente) as regras, como é que isso é melhor do que o que se tinha antes?

Felizmente, ao contrário da maioria das metodologias de CSS, o uso adequado da convenção de nomenclatura de BEM é muito fácil de testar e aplicar, tanto do lado do CSS quanto no do HTML. A seguir, estão algumas regras que você pode testar em uma linter de sua escolha.

No CSS:

  • Com exceção do reset da folha de estilos, todos os outros arquivos devem conter apenas seletores de classe.
  • Todos os seletores de classe devem começar com o nome do arquivo.
  • Seletores aninhados podem ter só dois níveis de profundidade e devem consistir de uma classe modificador seguida de uma classe de elemento.

No HTML:

  • Qualquer tag HTML com um elemento classe deve ser um descendente de uma tag com um bloco de classe com o mesmo nome.
  • Qualquer tag HTML com uma classe modificador também deve ter um bloco de classe com o mesmo nome.

Você pode achar as seguintes ferramentas úteis para fazer cumprir as convenções BEM:

Abrindo exceções

No mundo real, há casos em que o cumprimento rigoroso das convenções BEM é impraticável ou impossível. Isso é comum quando utilizamos plugins ou ferramentas de terceiros que geram parte de seu HTML para você, ou ao criar um aplicativo em que o conteúdo vai ser gerado por um usuário final.

Existem casos também em que, por conveniência, os desenvolvedores optam por ignorar convenções BEM. Um exemplo comum disso está na área de conteúdo de um site. Um desenvolvedor pode optar por favorecer seletores de tag em vez de ter que colocar uma classe em cada tag <p> ou <a>.

Até agora, eu espero que esteja óbvio que abrir exceções ou ignorar convenções BEM acarretará risco. E depois de ler este artigo deverá estar evidente exatamente quais são esses riscos. Você pode decidir por si só o nível de risco que está disposto a tomar, tendo em conta a sua situação.

Se as suas exceções estão limitadas a apenas uma área do seu site (por exemplo, a área de conteúdo), e se você não tem que suportar os navegadores mais antigos, pode adotar uma estratégia como esta:

.Content h1:not([class]) { }
.Content p:not([class]) { }
.Content a:not([class]) { }

Embora eu não tenha testado essa abordagem em um cenário do mundo real, eu a mencionei porque é um exemplo de uma variação de convenções BEM que não compromete sua garantia de nenhum efeito colateral. Uma vez que todos os blocos de BEM são denominados via classes, os elementos de estilo que não têm uma classe são “seguros”, pelo menos do conflito com o resto do seu CSS (obviamente, se você estiver usando as classes para outras coisas, isso ainda pode ser arriscado como a adição de uma classe para um elemento que a impediria de combinar com o seletor).

Outro exemplo com o qual me deparo com frequência é a utilização de classes de estado ou de suporte. Modernizr é um bom exemplo. Embora essa técnica aumente realmente a especificidade dos seletores nos quais ela é usada, o aumento da especificidade só deverá afetar outras regras definidas dentro do mesmo bloco (supondo que você tenha seguido todas as outras convenções BEM).

.GridRow {
  display: flex;
}

/* Fallback for older browsers. */
.no-flexbox .GridRow {
  display: table;
}

Claro, se você é capaz de escrever os componentes que podem gerenciar seu próprio estado via modificadores de BEM, será sempre preferível contar com as normas de estado de todo o site.

Aprendendo com JavaScript

Nos velhos – e ruins – tempos do JavaScript, era comum que os autores de biblioteca adicionassem métodos aos protótipos nativos de construtores globais, como Object, Array, String e Function. No começo, isso parecia ser uma conveniência, mas os desenvolvedores rapidamente perceberam que era um pesadelo. Se duas bibliotecas diferentes tivessem acrescentado o mesmo método para Array.prototype, cada uma com uma assinatura ou um comportamento um pouco diferente, isso levaria a erros quase impossíveis de rastrear.

Atualmente, quase nenhuma biblioteca modifica os protótipos nativos. Na verdade, eu vi algumas bibliotecas envergonhadas publicamente por tentar. Se nós aprendemos a nossa lição no JavaScript, por que não aprendemos no CSS?

Os sistemas de classe de nomenclatura utilizados por praticamente todos os frameworks CSS populares são tão ruins, se não piores, do que modificar os protótipos nativos no JavaScript. Eles enchem o namespace global com estilos de base, escolhem nomes tão comuns de classes que é quase garantido o conflito com o código existente, e eles nem fazem esforço nenhum para encapsular seus componentes.

Considere o Bootstrap. Cada um de seus plugins JavaScript usa um namespace e vem com um método .noConflict() para evitar colisões de nomenclatura. Seu CSS, por outro lado, não faz tal esforço apesar dos inúmeros pedidos, e soluções fáceis que as pessoas têm sugerido por muito tempo.

Não me refiro a chamar Bootstrap especificamente porque praticamente todos frameworks CSS convencionais fazem isso. Minha esperança é que a comunidade CSS vai começar a exigir mais de suas ferramentas, da mesma maneira que a comunidade JavaScript.

Encerrando

Se você está tentando avaliar uma nova metodologia ou framework CSS, ou está querendo desenvolver o seu próprio, peço que faça do código previsível uma das suas maiores prioridades.

Muitas metodologias tentam vender para você sutilezas e falsos confortos como marcação mínima ou sistemas de classe de nomenclatura legíveis. Enquanto padrões como class=”full height” ou class=”four wide column” parecem bons quando você os lê em voz alta, as concessões arquitetônicas necessárias para atingir esse “recurso” simplesmente não valem a pena.

Enquanto 100% dos códigos previsíveis podem nunca ser possíveis, é importante entender as trocas que você faz com as convenções que escolher. Se seguir as convenções estritas de BEM, você será capaz de atualizar e adicionar ao seu CSS no futuro, com a plena confiança de que suas alterações não terão efeitos colaterais. Se optar por seguir uma versão diluída de BEM, vai estar assumindo um pouco mais de risco. Às vezes, esses riscos são controláveis; às vezes, não. A quantidade de risco que você pode se dar ao luxo de tomar é inversamente proporcional ao tamanho da sua equipe.

Em minha opinião, se sua equipe é maior do que só você, o risco não vale a recompensa.

Notas de rodapé:

[1] Shadow DOM traz uma sub-árvore de duas vias no âmbito do CSS, apesar de não ser suportado, atualmente, por todos os navegadores.

[2] Com elementos personalizados, você pode criar tags adicionais, o que resolve parcialmente esse problema.

[3] A única exceção para isso são as propriedades herdadas, como font-size e line-height. Os blocos podem depender desses estilos que estão sendo definidos fora do bloco, pois isso lhes permite ser mais adaptável ao seu ambiente de hospedagem. Se os blocos escolherem não redefinirem as propriedades herdadas, eles devem ser flexíveis o suficiente para se adaptarem a quaisquer propriedades que possam receber.

[4] Há diversas variações diferentes sobre as convenções de nomenclatura tradicionais em BEM. Eu pessoalmente prefiro o defendido por SUIT CSS.

***

Philip Walton faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://philipwalton.com/articles/side-effects-in-css/