Atualização da arquitetura do DevTools: migração para módulos JavaScript

Tim van der Lippe
Tim van der Lippe

Como você deve saber, o Chrome DevTools é um aplicativo da Web escrito usando HTML, CSS e JavaScript. Ao longo dos anos, o DevTools ficou mais rico em recursos, mais inteligente e conhecendo a plataforma da Web como um todo. Embora o DevTools tenha se expandido com o passar dos anos, a arquitetura dele se assemelha em grande parte à arquitetura original de quando ainda fazia parte do WebKit.

Esta postagem faz parte de uma série de postagens do blog (em inglês) que descrevem as mudanças que estamos fazendo na arquitetura do DevTools e como ela é criada. Vamos explicar como ele funcionava historicamente, quais eram os benefícios e as limitações e o que fizemos para atenuar essas limitações. Portanto, vamos conferir os sistemas de módulos, como carregar código e como acabamos usando os módulos JavaScript.

No começo, não havia nada

Embora o cenário atual de front-end tenha vários sistemas de módulos com ferramentas criadas em torno deles, além do formato de módulos JavaScript agora padronizado, nenhum deles existia quando o DevTools foi criado. O DevTools foi desenvolvido com base no código que inicialmente era fornecido no WebKit há mais de 12 anos.

A primeira menção a um sistema de módulos no DevTools vem de 2012: a introdução de uma lista de módulos com uma lista associada de origens. Isso fazia parte da infraestrutura Python usada naquela época para compilar e criar DevTools. Uma mudança de acompanhamento extraiu todos os módulos para um arquivo frontend_modules.json separado (commit) em 2013 e, em 2014, em arquivos module.json separados (commit).

Um exemplo de arquivo module.json:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}

Desde 2014, o padrão module.json é usado no DevTools para especificar os módulos e arquivos de origem. Ao mesmo tempo, o ecossistema da Web evoluiu rapidamente e vários formatos de módulos foram criados, incluindo UMD, CommonJS e os módulos JavaScript mais padronizados. No entanto, o DevTools travou com o formato module.json.

Embora o DevTools continuasse funcionando, havia algumas desvantagens em usar um sistema de módulos exclusivo e não padronizado:

  1. O formato module.json exigia ferramentas de build personalizadas, semelhantes aos bundlers modernos.
  2. Não havia integração com o ambiente de desenvolvimento integrado, o que exigiu ferramentas personalizadas para gerar arquivos que os ambientes de desenvolvimento integrado modernos poderiam entender (o script original para gerar arquivos jsconfig.json para o VS Code).
  3. Funções, classes e objetos foram todos colocados no escopo global para possibilitar o compartilhamento entre os módulos.
  4. Os arquivos dependem da ordem, o que significa que a ordem em que os arquivos sources foram listados era importante. Não há garantia de que o código em que você confia seria carregado, a não ser que uma pessoa o tenha verificado.

No geral, ao avaliar o estado atual do sistema de módulos no DevTools e os outros formatos de módulo (mais usados), concluímos que o padrão module.json estava criando mais problemas do que resolveu e era hora de planejar nossa saída dele.

Os benefícios dos padrões

Entre os sistemas de módulos existentes, escolhemos os módulos JavaScript para serem migrados. No momento dessa decisão, os módulos JavaScript ainda estavam sendo enviados atrás de uma flag em Node.js e uma grande quantidade de pacotes disponíveis no NPM não tinha um pacote de módulos JavaScript que pudéssemos usar. Apesar disso, concluímos que os módulos JavaScript eram a melhor opção.

O principal benefício dos módulos JavaScript é que eles são o formato padronizado de JavaScript. Ao listar as desvantagens do module.json (confira acima), percebemos que quase todas estavam relacionadas ao uso de um formato de módulo exclusivo e não padronizado.

Escolher um formato de módulo não padronizado significa que temos que investir tempo nós mesmos na criação de integrações com as ferramentas de build e ferramentas que nossos mantenedores usaram.

Essas integrações muitas vezes eram frágeis e não tinham suporte para recursos, exigindo mais tempo de manutenção, às vezes levando a bugs sutis que eventualmente eram enviados aos usuários.

Como os módulos JavaScript eram o padrão, ambientes de desenvolvimento integrado como o VS Code, verificadores de tipos como closure Compiler/TypeScript e ferramentas de criação (como Rollup/minifiers) poderiam entender o código-fonte que criamos. Além disso, quando um novo mantenedor se juntava à equipe do DevTools, ele não precisaria gastar tempo aprendendo um formato module.json reservado, enquanto provavelmente já estaria familiarizado com os módulos JavaScript.

É claro que, quando o DevTools foi criado, nenhum dos benefícios acima existia. Foram necessários anos de trabalho em grupos de padrões, implementações de tempo de execução e desenvolvedores usando módulos JavaScript que fornecem feedback para chegar ao ponto em que eles estão agora. No entanto, quando os módulos JavaScript ficaram disponíveis, tivemos uma escolha: manter o próprio formato ou investir na migração para o novo.

O preço de um novinho em folha

Embora os módulos JavaScript tivessem muitos benefícios que gostaríamos de usar, continuamos no mundo module.json não padrão. Colhendo os benefícios dos módulos JavaScript significava que tínhamos que investir significativamente na limpeza da dívida técnica, realizando uma migração que poderia potencialmente quebrar recursos e introduzir bugs de regressão.

Naquela época, não era uma questão de "Do we want to use JavaScript modules?", mas uma pergunta de "Qual é o custo para usar módulos JavaScript?". Aqui, tivemos que equilibrar o risco de dividir nossos usuários com regressões, o custo dos engenheiros gastarem uma grande quantidade de tempo na migração e o estado pior temporário no qual trabalharíamos.

Esse último ponto acabou sendo muito importante. Em teoria, é possível chegar aos módulos JavaScript, mas, durante uma migração, acabaríamos com um código que precisaria considerar ambos os módulos module.json e JavaScript. Além de ser tecnicamente difícil de alcançar, todos os engenheiros que trabalham com o DevTools precisavam saber como trabalhar nesse ambiente. Eles teriam que se perguntar continuamente "Para esta parte da base do código, é module.json ou módulos JavaScript e como faço as mudanças?".

Prévia: o custo oculto de orientar nossos colegas mantenedores durante a migração foi maior do que o previsto.

Após a análise de custos, concluímos que ainda valeu a pena migrar para os módulos JavaScript. Portanto, nossas principais metas eram:

  1. Certifique-se de que o uso de módulos JavaScript colhe os benefícios o máximo possível.
  2. Verifique se a integração com o sistema atual baseado em module.json é segura e não causa impacto negativo no usuário, como bugs de regressão, frustração do usuário.
  3. Oriente todos os mantenedores do DevTools durante a migração, principalmente com freios e contrapesos integrados para evitar erros acidentais.

Planilhas, transformações e débito técnico

Embora o objetivo fosse claro, as limitações impostas pelo formato module.json se mostraram difíceis de contornar. Foram necessárias várias iterações, protótipos e mudanças de arquitetura até que desenvolvemos uma solução com que nos fôssemos confortáveis. Escrevemos um documento de design com a estratégia de migração que criamos. O documento de design também listou nossa estimativa de tempo inicial: duas a quatro semanas.

Spoiler: a parte mais intensa da migração levou 4 meses e do início ao fim levou 7 meses!

No entanto, o plano inicial durou o teste: ensinaríamos o ambiente de execução do DevTools a carregar todos os arquivos listados na matriz scripts no arquivo module.json usando a maneira antiga, enquanto todos os arquivos listados na matriz modules com importação dinâmica de módulos JavaScript. Qualquer arquivo que resida na matriz modules pode usar importações/exportações ES.

Além disso, realizaríamos a migração em duas fases (dividimos a última fase em duas subfases, conforme abaixo): export- e import. O status de qual módulo estaria em qual fase foi acompanhada em uma grande planilha:

Planilha de migração de módulos JavaScript

Um snippet da página de progresso está disponível publicamente aqui.

export fase

A primeira fase seria adicionar instruções export para todos os símbolos que deveriam ser compartilhados entre módulos/arquivos. A transformação seria automatizada, executando um script por pasta. Considerando que o seguinte símbolo existiria no mundo module.json:

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};

Aqui, Module é o nome do módulo e File1 é o nome do arquivo. Em nossa árvore de origem, seria front_end/module/file1.js.

Isso seria transformado da seguinte maneira:

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};

Inicialmente, nosso plano também era reescrever as importações do mesmo arquivo durante essa fase. Por exemplo, no exemplo acima, reescreveríamos Module.File1.localFunctionInFile para localFunctionInFile. No entanto, percebemos que seria mais fácil automatizar e mais seguro de aplicar se separamos essas duas transformações. Portanto, a opção "migrar todos os símbolos no mesmo arquivo" se tornaria a segunda subfase da import.

Como a adição da palavra-chave export em um arquivo transforma o arquivo de um "script" em um "módulo", grande parte da infraestrutura do DevTools precisou ser atualizada dessa forma. Isso incluiu o ambiente de execução (com a importação dinâmica), mas também ferramentas como ESLint para executar no modo de módulo.

Ao resolver esses problemas, descobrimos que os testes estavam sendo executados no modo "sloppy". Como os módulos JavaScript implicam que os arquivos são executados no modo "use strict", isso também afeta nossos testes. No final das contas, uma quantidade incomum de testes estava contando com esse erro, inclusive um teste que usava uma instrução with 😂.

No final, a atualização da primeira pasta para incluir instruções export levou cerca de uma semana e várias tentativas com relands.

import fase

Depois que todos os símbolos são exportados usando instruções export e permaneceram no escopo global (legado), tivemos que atualizar todas as referências a símbolos entre arquivos para usar importações ES. O objetivo final seria remover todos os "objetos de exportação legados", limpando o escopo global. A transformação seria automatizada, executando um script por pasta.

Por exemplo, para os seguintes símbolos que existem no mundo da module.json:

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();

Eles seriam transformados em:

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();

No entanto, houve algumas ressalvas com essa abordagem:

  1. Nem todos os símbolos foram nomeados como Module.File.symbolName. Alguns símbolos foram nomeados exclusivamente como Module.File ou até mesmo Module.CompletelyDifferentName. Essa inconsistência significou que tínhamos que criar um mapeamento interno do antigo objeto global para o novo objeto importado.
  2. Às vezes, há conflitos entre os nomes de moduleScoped. Mais notavelmente, usamos um padrão para declarar certos tipos de Events, em que cada símbolo foi nomeado apenas como Events. Isso significava que, se você estivesse ouvindo vários tipos de eventos declarados em arquivos diferentes, ocorreria um conflito de nomes na instrução import para essas Events.
  3. No resultado, havia dependências circulares entre os arquivos. Isso não era um problema em um contexto de escopo global, já que o uso do símbolo era feito após o carregamento de todo o código. No entanto, se você precisar de um import, a dependência circular será explícita. Isso não é um problema de imediato, a menos que você tenha chamadas de função de efeito colateral no código de escopo global, que o DevTools também tinha. No geral, foi necessária alguma cirurgia e refatoração para tornar a transformação segura.

Um mundo totalmente novo com módulos JavaScript

Em fevereiro de 2020, seis meses após o início em setembro de 2019, as últimas limpezas foram realizadas na pasta ui/. Isso marcou o fim não oficial da migração. Depois que a poeira baixou, marcamos oficialmente a migração como concluída em 5 de março de 2020. 🎉

Agora, todos os módulos no DevTools usam módulos JavaScript para compartilhar código. Ainda colocamos alguns símbolos no escopo global (nos arquivos module-legacy.js) para nossos testes legados ou para integração com outras partes da arquitetura DevTools. Eles serão removidos ao longo do tempo, mas não consideramos que eles sejam um obstáculo para desenvolvimento futuro. Também temos um guia de estilo para o uso de módulos JavaScript.

Estatísticas

As estimativas conservadoras do número de CLs (abreviação de changelist, termo usado no Gerrit que representa uma mudança, semelhante a uma solicitação de envio do GitHub) envolvidas nessa migração têm cerca de 250 CLs, geralmente realizadas por dois engenheiros. Não temos estatísticas definitivas sobre o tamanho das alterações feitas, mas uma estimativa conservadora de linhas alteradas (calculada como a soma da diferença absoluta entre inserções e exclusões de cada CL) é de aproximadamente 30.000 (aproximadamente 20% de todo o código de front-end do DevTools).

O primeiro arquivo usando export enviado no Chrome 79, lançado na versão estável em dezembro de 2019. A última mudança para migrar para o import lançada no Chrome 83, lançada na versão estável em maio de 2020.

Estamos cientes de uma regressão enviada ao Chrome Stable e que foi introduzida como parte dessa migração. O preenchimento automático de snippets no menu de comando foi corrompido devido a uma exportação de default externa. Tivemos várias outras regressões, mas nossos conjuntos de testes automatizados e usuários do Chrome Canary relataram essas falhas e as corrigimos antes que pudessem alcançar os usuários estáveis do Chrome.

Você pode conferir a jornada completa (nem todos os CLs estão anexados a esse bug, mas a maioria deles está) conectada em crbug.com/1006759.

O que descobrimos

  1. Decisões no passado podem ter um impacto duradouro no projeto. Embora os módulos JavaScript (e outros formatos de módulos) estivessem disponíveis por algum tempo, o DevTools não estava em uma posição para justificar a migração. Decidir quando migrar é difícil e com base em suposições.
  2. Nossas estimativas de tempo iniciais foram em semanas, e não em meses. Em grande parte, isso se deve ao fato de que encontramos mais problemas inesperados do que prevíamos em nossa análise de custos inicial. Embora o plano de migração fosse sólido, a dívida técnica era (com mais frequência do que gostaríamos) o obstáculo.
  3. A migração dos módulos JavaScript incluiu uma grande quantidade de limpeza de dívidas técnicas (aparentemente não relacionadas). A migração para um formato moderno de módulo padronizado nos permitiu realinhar nossas práticas recomendadas de codificação com o desenvolvimento atual da Web. Por exemplo, pudemos substituir nosso bundler Python personalizado por uma configuração de visualização completa mínima.
  4. Apesar do grande impacto em nossa base de código (cerca de 20% do código alterado), poucas regressões foram relatadas. Tivemos vários problemas para migrar os primeiros arquivos, mas depois de um tempo tínhamos um fluxo de trabalho sólido e parcialmente automatizado. Por isso, o impacto negativo sobre os usuários na versão estável foi mínimo nessa migração.
  5. Ensinar as complexidades de uma migração específica a outros mantenedores é difícil e às vezes impossível. Migrações dessa escala são difíceis de acompanhar e exigem muito conhecimento do domínio. Transferir esse conhecimento de domínio para outras pessoas que trabalham na mesma base de código não é desejável por si só para o trabalho que eles estão fazendo. Saber o que compartilhar e quais detalhes não compartilhar é uma arte, mas necessária. Portanto, é crucial reduzir a quantidade de grandes migrações ou, pelo menos, não executá-las ao mesmo tempo.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como seu navegador de desenvolvimento padrão. Esses canais de pré-lançamento oferecem acesso aos recursos mais recentes do DevTools, testam as APIs modernas de plataformas da Web e encontram problemas no site antes dos usuários.

Entrar em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos e mudanças na publicação ou qualquer outra coisa relacionada ao DevTools.

  • Envie uma sugestão ou feedback em crbug.com.
  • Informe um problema do DevTools em Mais opções   Mais   > Ajuda > Informar problemas no DevTools.
  • Envie um tweet em @ChromeDevTools.
  • Deixe comentários nos nossos vídeos do YouTube sobre a ferramenta DevTools ou nos vídeos do YouTube com dicas sobre o DevTools.