Como você sabe, o Chrome DevTools é um aplicativo da Web criado usando HTML, CSS e JavaScript. Com o passar dos anos, as Ferramentas do desenvolvedor ficaram mais inteligentes, com mais recursos e mais conhecimento sobre a plataforma da Web. Embora as Ferramentas do desenvolvedor tenham se expandido ao longo dos anos, a arquitetura delas é muito semelhante à arquitetura original, quando ainda faziam 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 é de 2012: a introdução de uma lista de módulos com uma lista associada de origens.
Isso fazia parte da infraestrutura do Python usada na época para compilar e criar as Ferramentas para desenvolvedores.
Uma mudança posterior extraiu todos os módulos para um arquivo frontend_modules.json
separado (commit) em 2013 e depois 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 ficou 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 integrados 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, ou seja, a ordem em que as
sources
eram listadas 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.
No geral, ao avaliar o estado atual do sistema de módulos no DevTools e nos outros formatos de módulo (mais usados), concluímos que o padrão module.json
estava criando mais problemas do que resolvendo e era hora de planejar a 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. Na época dessa decisão, os módulos JavaScript ainda eram 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 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 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 significava que ambientes de desenvolvimento integrados como o VS Code, verificadores de tipo como o Closure Compiler/TypeScript e ferramentas de build como o Rollup/minifiers podiam entender o código-fonte que escrevemos.
Além disso, quando um novo mantenedor se junta à equipe do DevTools, ele não precisa perder tempo aprendendo um formato module.json
reservado, já que provavelmente já está 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 execução e desenvolvedores usando módulos JavaScript para fornecer feedback e 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
.
Para aproveitar os benefícios dos módulos JavaScript, tivemos que investir significativamente na limpeza da dívida técnica, realizando uma migração que poderia quebrar 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, tivemos que equilibrar o risco de quebrar nossos usuários com regressões, o custo de engenheiros gastando (uma grande quantidade de) tempo migrando e o estado temporariamente pior 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 as 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 balanços 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 de tempo inicial: de duas a quatro semanas.
Alerta de spoiler: a parte mais intensa da migração levou quatro meses, e a migração toda levou sete 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 neste link.
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.
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 de arquivos iguais durante essa fase.
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 para o 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.
Como resultado, uma quantidade não trivial de testes estavam dependendo dessa negligência, 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 relands.
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();
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, havia algumas ressalvas com essa abordagem:
- Nem todos os símbolos foram nomeados como
Module.File.symbolName
. Alguns símbolos foram nomeados apenas comoModule.File
ou até mesmoModule.CompletelyDifferentName
. Essa inconsistência significa que precisamos criar um mapeamento interno do antigo objeto global para o novo objeto importado. - Às vezes, há conflitos entre nomes de moduleScoped.
Usamos um padrão de declaração de determinados tipos de
Events
, em que cada símbolo era chamado apenas deEvents
. Isso significa que, se você estivesse detectando vários tipos de eventos declarados em arquivos diferentes, um conflito de nome ocorreria 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 novo mundo 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 bloqueador para o desenvolvimento futuro.
Também temos um guia de estilo para o 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 a versão 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.
Estamos cientes 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.
Confira a jornada completa (nem todos os CLs estão vinculados a esse bug, mas a maioria deles está) registrada em crbug.com/1006759.
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. Embora o plano de migração fosse sólido, a dívida técnica era o obstáculo com mais frequência do que gostaríamos.
- 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 foi 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. Isso significa que o impacto negativo para nossos usuários estáveis foi mínimo nessa migração.
- Ensinar as complexidades de uma migração específica para outros administradores é 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 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 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.
- 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 para @ChromeDevTools.
- Deixe comentários nos vídeos Novidades do DevTools no YouTube ou Dicas do DevTools no YouTube.