Suporte a CSS-in-JS no DevTools

Alex Rudenko
Alex Rudenko

Este artigo fala sobre o suporte ao CSS-in-JS no DevTools, que foi lançado desde o Chrome 85, e, em geral, o que queremos dizer com CSS-in-JS e como ele é diferente do CSS normal que tem suporte do DevTools há muito tempo.

O que é CSS-in-JS?

A definição de CSS-in-JS é bastante vaga. Em um sentido amplo, é uma abordagem para gerenciar o código CSS usando JavaScript. Por exemplo, isso pode significar que o conteúdo CSS é definido usando JavaScript e a saída CSS final é gerada em tempo real pelo app.

No contexto das ferramentas do desenvolvedor, CSS-in-JS significa que o conteúdo CSS é injetado na página usando APIs CSSOM. O CSS regular é injetado usando elementos <style> ou <link> e tem uma origem estática (por exemplo, um nó DOM ou um recurso de rede). Por outro lado, o CSS-in-JS geralmente não tem uma origem estática. Um caso especial aqui é que o conteúdo de um elemento <style> pode ser atualizado usando a API CSSOM, fazendo com que a origem fique fora de sincronia com a folha de estilo CSS real.

Se você usar qualquer biblioteca CSS-in-JS (por exemplo, styled-component, Emotion, JSS), ela poderá injetar estilos usando APIs CSSOM, dependendo do modo de desenvolvimento e do navegador.

Vamos conferir alguns exemplos de como injetar uma folha de estilo usando a API CSSOM de forma semelhante ao que as bibliotecas CSS-in-JS fazem.

// Insert new rule to an existing CSS stylesheet
const element = document.querySelector('style');
const stylesheet = element.sheet;
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

Você também pode criar uma folha de estilo completamente nova:

// Create a completely new stylesheet
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

// Apply constructed stylesheet to the document
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

Suporte a CSS no DevTools

No DevTools, o recurso mais usado ao lidar com CSS é o painel Styles. No painel Styles, você pode conferir quais regras se aplicam a um elemento específico, editá-las e conferir as mudanças na página em tempo real.

Antes do ano passado, o suporte a regras de CSS modificadas usando APIs CSSOM era bastante limitado: você só podia ver as regras aplicadas, mas não editá-las. O principal objetivo do ano passado foi permitir a edição de regras CSS-in-JS usando o painel "Styles". Às vezes, também chamamos os estilos CSS-in-JS de "construídos" para indicar que eles foram criados usando APIs da Web.

Vamos nos aprofundar nos detalhes da edição de estilos no DevTools.

Mecanismo de edição de estilo no DevTools

Mecanismo de edição de estilo no DevTools

Quando você seleciona um elemento no DevTools, o painel Styles é mostrado. O painel Styles emite um comando CDP chamado CSS.getMatchedStylesForNode para receber as regras de CSS que se aplicam ao elemento. O CDP significa "Chrome DevTools Protocol" e é uma API que permite que o front-end do DevTools receba mais informações sobre a página inspecionada.

Quando invocado, CSS.getMatchedStylesForNode identifica todas as folhas de estilo no documento e as analisa usando o analisador de CSS do navegador. Em seguida, ele cria um índice que associa cada regra CSS a uma posição na origem da folha de estilo.

Você pode se perguntar por que é necessário analisar o CSS novamente. O problema aqui é que, por motivos de desempenho, o navegador não se preocupa com as posições de origem das regras CSS e, portanto, não as armazena. Mas o DevTools precisa das posições de origem para oferecer suporte à edição de CSS. Não queremos que os usuários regulares do Chrome paguem a penalidade de desempenho, mas queremos que os usuários do DevTools tenham acesso às posições de origem. Essa abordagem de reanalisador aborda ambos os casos de uso com desvantagens mínimas.

Em seguida, a implementação de CSS.getMatchedStylesForNode solicita que o mecanismo de estilo do navegador forneça regras de CSS que correspondam ao elemento fornecido. Por fim, o método associa as regras retornadas pelo mecanismo de estilo ao código-fonte e fornece uma resposta estruturada sobre as regras CSS para que as Ferramentas do desenvolvedor saibam qual parte da regra é o seletor ou as propriedades. Isso permite que o DevTools edite o seletor e as propriedades de forma independente.

Agora vamos falar sobre edição. Lembra que CSS.getMatchedStylesForNode retorna posições de origem para cada regra? Isso é crucial para a edição. Quando você muda uma regra, o DevTools emite outro comando CDP que atualiza a página. O comando inclui a posição original do fragmento da regra que está sendo atualizada e o novo texto que precisa ser usado no fragmento.

No back-end, ao processar a chamada de edição, o DevTools atualiza a folha de estilo de destino. Ele também atualiza a cópia da origem da folha de estilo que mantém e atualiza as posições da origem para a regra atualizada. Em resposta à chamada de edição, o front-end do DevTools recebe as posições atualizadas do fragmento de texto que acabou de ser atualizado.

Isso explica por que a edição de CSS-in-JS no DevTools não funcionava imediatamente: o CSS-in-JS não tem uma origem real armazenada em nenhum lugar e as regras do CSS ficam armazenadas na memória do navegador em estruturas de dados CSSOM.

Como adicionamos suporte ao CSS-in-JS

Portanto, para oferecer suporte à edição de regras CSS-in-JS, decidimos que a melhor solução seria criar uma origem para folhas de estilo construídas que podem ser editadas usando o mecanismo descrito acima.

A primeira etapa é criar o texto de origem. O mecanismo de estilo do navegador armazena as regras CSS na classe CSSStyleSheet. Essa é a classe que você pode criar instâncias em JavaScript, como discutido anteriormente. O código para criar o texto de origem é este:

String InspectorStyleSheet::CollectStyleSheetRules() {
  StringBuilder builder;
  for (unsigned i = 0; i < page_style_sheet_->length(); i++) {
    builder.Append(page_style_sheet_->item(i)->cssText());
    builder.Append('\n');
  }
  return builder.ToString();
}

Ele itera as regras encontradas em uma instância de CSSStyleSheet e cria uma única string. Esse método é invocado quando uma instância da classe InspectorStyleSheet é criada. A classe InspectorStyleSheet envolve uma instância CSSStyleSheet e extrai outros metadados necessários pelas Ferramentas do desenvolvedor:

void InspectorStyleSheet::UpdateText() {
  String text;
  bool success = InspectorStyleSheetText(&text);
  if (!success)
    success = InlineStyleSheetText(&text);
  if (!success)
    success = ResourceStyleSheetText(&text);
  if (!success)
    success = CSSOMStyleSheetText(&text);
  if (success)
    InnerSetText(text, false);
}

Neste snippet, CSSOMStyleSheetText chama CollectStyleSheetRules internamente. CSSOMStyleSheetText é invocado se a folha de estilo não estiver inline ou for uma folha de estilo de recurso. Basicamente, esses dois snippets já permitem a edição básica das folhas de estilo criadas usando o construtor new CSSStyleSheet().

Um caso especial são as folhas de estilo associadas a uma tag <style> que foram modificadas usando a API CSSOM. Nesse caso, a folha de estilo contém o texto de origem e regras adicionais que não estão presentes na origem. Para lidar com esse caso, apresentamos um método para mesclar essas regras adicionais no texto de origem. Aqui, a ordem é importante porque as regras do CSS podem ser inseridas no meio do texto original. Por exemplo, imagine que o elemento <style> original continha o seguinte texto:

/* comment */
.rule1 {}
.rule3 {}

Em seguida, a página inseriu algumas regras novas usando a API JS, produzindo a seguinte ordem de regras: .rule0, .rule1, .rule2, .rule3, .rule4. O texto de origem resultante após a operação de mesclagem deve ser o seguinte:

.rule0 {}
/* comment */
.rule1 {}
.rule2 {}
.rule3 {}
.rule4 {}

A preservação dos comentários e recuos originais é importante para o processo de edição, porque as posições do texto de origem das regras precisam ser precisas.

Outro aspecto especial das folhas de estilo CSS-in-JS é que elas podem ser alteradas pela página a qualquer momento. Se as regras do CSSOM atual ficarem fora de sincronia com a versão de texto, a edição não vai funcionar. Para isso, introduzimos uma sonda, que permite que o navegador notifique a parte de back-end do DevTools quando uma folha de estilo está sendo modificada. As folhas de estilo modificadas são sincronizadas na próxima chamada para CSS.getMatchedStylesForNode.

Com todas essas peças em vigor, a edição de CSS-in-JS já funciona, mas queríamos melhorar a interface para indicar se uma folha de estilo foi criada. Adicionamos um novo atributo chamado isConstructed ao CSS.CSSStyleSheetHeader do CDP que o front-end usa para mostrar corretamente a origem de uma regra de CSS:

Folha de estilo configurável

Conclusões

Para recapitular nossa história, abordamos os casos de uso relevantes relacionados ao CSS-in-JS que o DevTools não oferecia e a solução para esses casos. O interessante dessa implementação é que conseguimos aproveitar a funcionalidade atual fazendo com que as regras CSS do CSSOM tivessem um texto de origem regular, evitando a necessidade de reprojetar completamente a edição de estilo no DevTools.

Para mais informações, consulte nossa proposta de design ou o bug de rastreamento do Chromium, que faz referência a todos os patches relacionados.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como navegador de desenvolvimento padrão. Esses canais de visualização dão acesso aos recursos mais recentes do DevTools, permitem testar APIs de plataforma da Web de última geração e ajudam a encontrar problemas no seu site antes que os usuários.

Entre em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos, atualizações ou qualquer outra coisa relacionada ao DevTools.