Como você já deve saber, o Chrome DevTools é um aplicativo da Web criado usando HTML, CSS e JavaScript. Ao longo dos anos, o DevTools ficou mais rico em recursos, mais inteligente e bem informado sobre a plataforma da Web mais ampla. Embora o DevTools tenha se expandido ao longo dos anos, sua arquitetura se assemelha muito à arquitetura original quando ainda fazia parte do WebKit.
Este post faz parte de uma série de posts do blog que descrevem as mudanças que estamos fazendo na arquitetura do DevTools e como ele é criado. Vamos explicar como as Ferramentas do desenvolvedor funcionaram historicamente, quais foram os benefícios e as limitações e o que fizemos para aliviar essas limitações. Vamos nos aprofundar nos sistemas de módulos, como carregar código e como acabamos usando módulos JavaScript.
No começo, não havia nada
Embora o cenário atual de front-end tenha uma variedade de sistemas de módulos com ferramentas criadas em torno deles, bem como o formato de módulos JavaScript agora padronizado, nenhum deles existia quando o DevTools foi criado. O DevTools é criado com base no código que foi enviado inicialmente 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 fontes (link em inglês).
Isso fazia parte da infraestrutura Python usada naquela época para compilar e criar o DevTools.
Uma mudança posterior extraiu todos os módulos para um arquivo frontend_modules.json
separado (commit) em 2013 e, em seguida, para arquivos module.json
separados (commit) em 2014.
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.
Enquanto isso, o ecossistema da Web evoluiu rapidamente e vários formatos de módulo foram criados, incluindo UMD, CommonJS e os módulos JavaScript padronizados.
No entanto, o DevTools permaneceu com o formato module.json
.
Embora o DevTools continuasse funcionando, havia algumas desvantagens em usar um sistema de módulos único e não padronizado:
- O formato
module.json
exigia ferramentas de build personalizadas, semelhantes aos agrupadores modernos. - Não havia integração com o ambiente de desenvolvimento integrado, o que exigia ferramentas personalizadas para gerar arquivos que os ambientes de desenvolvimento integrado modernos pudessem entender (o script original para gerar arquivos jsconfig.json para o VS Code).
- Funções, classes e objetos foram colocados no escopo global para permitir o compartilhamento entre módulos.
- Os arquivos dependiam da ordem, o que significa que a ordem em que
sources
era listada era importante. Não havia garantia de que o código em que você confiava seria carregado, a não ser que um humano o tivesse verificado.
Resumindo, 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. Por isso, era hora de planejar uma mudança.
Os benefícios dos padrões
Entre os sistemas de módulos existentes, escolhemos os módulos JavaScript como o destino da migração. No momento dessa decisão, os módulos JavaScript ainda estavam sendo enviados com uma flag no Node.js, e uma grande quantidade de pacotes disponíveis no NPM não tinha um pacote de módulos JavaScript que poderíamos 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 de módulo padronizado para JavaScript.
Ao listarmos as desvantagens do module.json
(veja acima), percebemos que quase todas elas estavam relacionadas ao uso de um formato de módulo não padronizado e único.
A escolha de um formato de módulo não padronizado significa que precisamos investir tempo para criar integrações com as ferramentas de build e as ferramentas usadas pelos nossos mantenedores.
Essas integrações geralmente eram frágeis e não tinham suporte a recursos, exigindo mais tempo de manutenção e, às vezes, levando a bugs sutis que eventualmente seriam enviados aos usuários.
Como os módulos JavaScript eram o padrão, isso significa que ambientes de desenvolvimento integrado como o VS Code, verificadores de tipo, como Closure Compiler/TypeScript e ferramentas de build, como Rollup/minifiers, poderiam entender o código-fonte que criamos.
Além disso, quando um novo mantenedor entrasse na equipe do DevTools, ele não precisaria perder tempo aprendendo um formato module.json
reservado, embora (provavelmente) já esteja 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 ambiente de execução e desenvolvedores usando módulos JavaScript, fornecendo feedback para chegar ao ponto em que estão agora. Mas, quando os módulos JavaScript ficaram disponíveis, tivemos que escolher entre manter nosso próprio formato ou migrar para o novo.
O custo do novo
Embora os módulos JavaScript tivessem muitos benefícios que gostaríamos de usar, permanecemos no mundo não padrão do module.json
.
Com as vantagens dos módulos JavaScript, tínhamos que investir significativamente em eliminar dívidas técnicas, realizando uma migração que poderia corromper os recursos e introduzir bugs de regressão.
Nesse ponto, não era uma questão de "Queremos usar módulos JavaScript?", mas "Quanto custa usar módulos JavaScript?". Aqui, tínhamos que equilibrar o risco de prejudicar nossos usuários com regressões, o custo dos engenheiros gastando (uma grande quantidade) de tempo na migração e a pior situação temporária em que trabalharíamos.
Esse último ponto acabou sendo muito importante. Embora fosse possível, em teoria, chegar aos módulos JavaScript, durante uma migração, acabaríamos com um código que precisaria levar em conta ambos os módulos module.json
e JavaScript.
Isso não era apenas difícil de alcançar tecnicamente, mas também significava que todos os engenheiros que trabalhavam com o DevTools precisavam saber como trabalhar nesse ambiente.
Eles teriam que se perguntar continuamente: "Para esta parte da base de código, são módulos module.json
ou JavaScript e como faço mudanças?".
Antevisão: o custo oculto de orientar nossos colegas de manutenção durante uma migração foi maior do que esperávamos.
Após a análise de custo, concluímos que ainda valia a pena migrar para módulos JavaScript. Portanto, nossas principais metas foram as seguintes:
- Verifique se o uso de módulos JavaScript aproveita os benefícios ao máximo.
- Verifique se a integração com o sistema baseado em
module.json
é segura e não causa um impacto negativo no usuário (bugs de regressão, frustração do usuário). - Orientar todos os mantenedores do DevTools durante a migração, principalmente com verificações e controles integrados para evitar erros acidentais.
Planilhas, transformações e débito técnico
Embora a meta fosse clara, 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 arquitetônicas até desenvolvermos uma solução com a qual nos sentimos confortáveis.
Escrevemos um documento de design com a estratégia de migração que usamos.
O documento de design também listou nossa estimativa inicial de tempo: de duas a quatro semanas.
Spoiler: a parte mais intensa da migração levou 4 meses e do início ao fim, 7 meses!
No entanto, o plano inicial resistiu ao teste do tempo: ensinamos o ambiente de execução do DevTools a carregar todos os arquivos listados na matriz scripts
no arquivo module.json
da maneira antiga, enquanto todos os arquivos listados na matriz modules
com importação dinâmica de módulos JavaScript.
Qualquer arquivo que residisse na matriz modules
poderia usar importações/exportações do ES.
Além disso, faríamos a migração em duas fases (dividimos a última fase em duas subfases, conforme abaixo): as fases export
e import
.
O status de qual módulo estaria em qual fase foi rastreado em uma planilha grande:
Um snippet da planilha 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 com a execução de um script por pasta.
O símbolo a seguir 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. No nosso sourcetree, seria front_end/module/file1.js
.
Isso seria transformado no seguinte:
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 era reescrever as importações do mesmo arquivo durante essa fase também.
Por exemplo, no exemplo acima, reescreveríamos Module.File1.localFunctionInFile
como localFunctionInFile
.
No entanto, percebemos que seria mais fácil automatizar e mais seguro aplicar se separássemos essas duas transformações.
Portanto, a "migração de todos os símbolos no mesmo arquivo" se tornaria a segunda subfase da fase 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 das Ferramentas do desenvolvedor precisou ser atualizada.
Isso incluiu o ambiente de execução (com importação dinâmica), mas também ferramentas como ESLint
para execução no modo de módulo.
Uma descoberta que fizemos ao trabalhar com esses problemas é que nossos testes estavam sendo executados no modo "frouxo".
Como os módulos JavaScript implicam que os arquivos são executados no modo "use strict"
, isso também afetaria nossos testes.
No fim das contas, uma quantidade não trivial de testes se baseava nessa desleixação, incluindo 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 reenvios.
import
-fase
Depois que todos os símbolos foram exportados usando instruções export
e permaneceram no escopo global (legado), foi necessário atualizar todas as referências para símbolos entre arquivos para usar importações do ES.
O objetivo final é 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 module.json
:
Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();
Elas seriam transformadas 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, havia algumas ressalvas com essa abordagem:
- Nem todos os símbolos foram nomeados como
Module.File.symbolName
. Alguns símbolos eram nomeados exclusivamente comoModule.File
ou até mesmoModule.CompletelyDifferentName
. Essa inconsistência significa que tínhamos que criar um mapeamento interno do objeto global antigo para o novo objeto importado. - Às vezes, há conflitos entre nomes de moduleScoped.
Com mais destaque, usamos um padrão de declaração de certos tipos de
Events
, em que cada símbolo era nomeado apenas comoEvents
. Isso significa que, se você estiver detectando vários tipos de eventos declarados em arquivos diferentes, ocorreria um conflito de nomes na instruçãoimport
para essesEvents
. - Havia dependências circulares entre os arquivos.
Isso estava correto em um contexto de escopo global, porque o uso do símbolo foi feito depois que todo o código foi carregado.
No entanto, se você precisar de uma
import
, a dependência circular será explicitada. Isso não é um problema imediato, a menos que você tenha chamadas de função com efeitos colaterais no código de escopo global, que o DevTools também tinha. No geral, foi necessário fazer algumas mudanças e refatorações 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 de um tempo, 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 do DevTools.
Elas serão removidas com o tempo, mas não são consideradas um bloqueio para o desenvolvimento futuro.
Temos também um guia de estilo para nosso uso de módulos JavaScript.
Estatísticas
As estimativas conservadoras para o número de listas de mudanças (CLs, na sigla em inglês) envolvidas nessa migração são de cerca de 250 CLs, em grande parte realizadas por dois engenheiros. Não temos estatísticas definitivas sobre o tamanho das mudanças 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 (~20% de todo o código do front-end do DevTools).
O primeiro arquivo que usa export
foi enviado no Chrome 79, lançado para o canal estável em dezembro de 2019.
A última mudança para migrar para import
foi enviada no Chrome 83, lançado para a versão estável em maio de 2020.
Sabemos de uma regressão que foi enviada para o Chrome estável e introduzida como parte dessa migração.
O preenchimento automático de snippets no menu de comando parou de funcionar devido a uma exportação default
externa.
Tivemos várias outras regressões, mas nossos pacotes de testes automatizados e os usuários do Chrome Canary as informaram, e elas foram corrigidas antes de chegarem aos usuários do Chrome Stable.
Você pode ver a jornada completa registrada em crbug.com/1006759. Nem todos os CLs estão anexados a esse bug, mas a maioria está registrada.
O que descobrimos
- As decisões tomadas no passado podem ter um impacto duradouro no seu projeto. Embora os módulos JavaScript (e outros formatos de módulo) estivessem disponíveis há algum tempo, o DevTools não estava em posição de justificar a migração. Decidir quando migrar e quando não migrar é difícil e se baseia em suposições.
- Nossas estimativas iniciais eram em semanas, não meses. Isso se deve principalmente ao fato de termos encontrado mais problemas inesperados do que esperávamos na nossa análise de custo inicial. Mesmo que o plano de migração fosse sólido, a dívida técnica era (mais frequentemente do que gostaríamos) o obstáculo.
- A migração de módulos JavaScript incluiu uma grande quantidade de limpezas de dívidas técnicas (aparentemente não relacionadas). A migração para um formato de módulo moderno e padronizado nos permitiu realinhar nossas práticas recomendadas de programação com o desenvolvimento moderno da Web. Por exemplo, foi possível substituir nosso bundler Python personalizado por uma configuração mínima de agrupamento.
- Apesar do grande impacto na nossa base de código (cerca de 20% do código alterado), poucas regressões foram relatadas. Embora tenhamos tido vários problemas ao migrar os primeiros arquivos, depois de um tempo, conseguimos um fluxo de trabalho sólido e parcialmente automatizado. Por isso, o impacto negativo para os usuários da versão estável foi mínimo durante essa migração.
- Ensinar as complexidades de uma migração específica para outros administradores é difícil e, às vezes, impossível. As 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 para o trabalho que elas estão fazendo. Saber o que compartilhar e quais detalhes não compartilhar é uma arte, mas é necessário. Portanto, é crucial reduzir a quantidade de migrações grandes ou, pelo menos, não realizá-las ao mesmo tempo.
Fazer o download dos canais de visualização
Use o Chrome Canary, Dev ou Beta como navegador de desenvolvimento padrão. Esses canais de pré-lançamento dão acesso aos recursos mais recentes do DevTools, permitem testar APIs modernas da plataforma da Web e ajudam a encontrar problemas no site antes que os usuários o façam!
Entre em contato com a equipe do Chrome DevTools
Use as opções a seguir para discutir novos recursos, atualizações ou qualquer outro item relacionado ao DevTools.
- Envie feedback e solicitações de recursos para crbug.com.
- Informe um problema do DevTools usando a opção Mais opções > Ajuda > Informar um problema do DevTools no DevTools.
- Envie um tweet em @ChromeDevTools.
- Deixe comentários nos vídeos Novidades do DevTools no YouTube ou Dicas do DevTools no YouTube.