Na nossa transmissão ao vivo Supercharged mais recente, implementamos a divisão de código e o agrupamento baseado em rotas. Com o HTTP/2 e os módulos ES6 nativos, essas técnicas vão se tornar essenciais para permitir o carregamento e o armazenamento em cache eficientes de recursos de script.
Dicas e truques diversos neste episódio
asyncFunction().catch()
comerror.stack
: 9h55- Módulos e atributo
nomodule
em tags<script>
: 7:30 promisify()
no nó 8: 17:20
Texto longo, leia o resumo
Como fazer a divisão de código usando o agrupamento baseado em rota:
- Acesse uma lista dos seus pontos de entrada.
- Extraia as dependências de módulo de todos esses pontos de entrada.
- Encontre dependências compartilhadas entre todos os pontos de entrada.
- Agrupe as dependências compartilhadas.
- Reescreva os pontos de entrada.
Divisão de código x fragmentação baseada em rotas
A divisão de código e o agrupamento de rotas estão intimamente relacionados e são usados de forma intercambiável. Isso causou alguma confusão. Vamos esclarecer isso:
- Divisão do código: é o processo de dividir o código em vários pacotes. Se você não enviar um grande pacote com todo o JavaScript para o cliente, estará fazendo a divisão do código. Uma maneira específica de dividir seu código é usar o agrupamento baseado em rotas.
- Fragmentação baseada em rota: cria pacotes relacionados às rotas do app. Ao analisar suas rotas e as dependências delas, podemos mudar quais módulos vão para qual pacote.
Por que fazer a divisão do código?
Módulos soltos
Com os módulos ES6 nativos, cada módulo JavaScript pode importar as próprias dependências. Quando o navegador
recebe um módulo, todas as instruções import
acionam outras transferências para acessar os
módulos necessários para executar o código. No entanto, todos esses módulos podem ter dependências
próprios. O perigo é que o navegador acabe com uma cascata de buscas que dura várias
viagens de ida e volta antes que o código possa ser finalmente executado.
Agrupamento
O agrupamento, que é a inserção de todos os módulos em um único pacote, garante que o navegador tenha todo o código necessário após uma ida e volta e possa começar a executar o código mais rapidamente. Isso, no entanto, força o usuário a fazer o download de muito código que não é necessário, então a largura de banda e o tempo foram desperdiçados. Além disso, cada mudança em um dos nossos módulos originais vai resultar em uma mudança no pacote, invalidando qualquer versão em cache do pacote. Os usuários precisarão fazer o download de tudo de novo.
Divisão de código
O particionamento do código é o meio-termo. Estamos dispostos a investir em mais viagens de ida e volta para conseguir
eficiência de rede, fazendo o download apenas do que precisamos, e melhorando a eficiência do armazenamento em cache, reduzindo muito o
número de módulos por pacote. Se o agrupamento for feito corretamente, o número total de viagens de ida e volta
será muito menor do que com módulos soltos. Por fim, podemos usar mecanismos de pré-carregamento, como link[rel=preload]
, para economizar mais tempo no round trip, se necessário.
Etapa 1: acessar uma lista dos pontos de entrada
Essa é apenas uma das muitas abordagens, mas no episódio analisamos o
sitemap.xml
do site para encontrar os pontos de entrada. Normalmente, é usado um arquivo JSON dedicado que lista todos os pontos de entrada.
Como usar o Babel para processar JavaScript
O Babel é comumente usado para "transpilação": consumir código JavaScript de ponta e transformá-lo em uma versão mais antiga de JavaScript para que mais navegadores possam executar o código. A primeira etapa é analisar o novo JavaScript com um analisador (o Babel usa o babylon) que transforma o código em uma chamada "árvore de sintaxe abstrata" (AST, na sigla em inglês). Depois que o AST é gerado, uma série de plug-ins analisa e modifica o AST.
Vamos usar bastante o Babel para detectar (e manipular mais tarde) as importações de um módulo JavaScript. Você pode querer recorrer a expressões regulares, mas elas não são poderosas o suficiente para analisar corretamente uma linguagem e são difíceis de manter. Confiar em ferramentas testadas e comprovadas, como o Babel, vai evitar muitas dores de cabeça.
Confira um exemplo simples de execução do Babel com um plug-in personalizado:
const plugin = {
visitor: {
ImportDeclaration(decl) {
/* ... */
}
}
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});
Um plug-in pode fornecer um objeto visitor
. O visitante contém uma função para qualquer tipo de nó que o
plug-in queira processar. Quando um nó desse tipo é encontrado durante a travessia do AST, a
função correspondente no objeto visitor
é invocada com esse nó como parâmetro. No
exemplo acima, o método ImportDeclaration()
será chamado para cada declaração import
no
arquivo. Para ter uma ideia dos tipos de nó e da AST, acesse
astexplorer.net.
Etapa 2: extrair as dependências do módulo
Para criar a árvore de dependências de um módulo, vamos analisar esse módulo e criar uma lista de todos os módulos importados. Também precisamos analisar essas dependências, já que elas também podem ter dependências. Um caso clássico de recursão.
async function buildDependencyTree(file) {
let code = await readFile(file);
code = code.toString('utf-8');
// `dep` will collect all dependencies of `file`
let dep = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// Recursion: Push an array of the dependency’s dependencies onto the list
dep.push((async function() {
return await buildDependencyTree(`./app/${importedFile}`);
})());
// Push the dependency itself onto the list
dep.push(importedFile);
}
}
}
// Run the plugin
babel.transform(code, {plugins: [plugin]});
// Wait for all promises to resolve and then flatten the array
return flatten(await Promise.all(dep));
}
Etapa 3: encontrar dependências compartilhadas entre todos os pontos de entrada
Como temos um conjunto de árvores de dependência, uma floresta de dependências, podemos encontrar as dependências compartilhadas procurando nós que aparecem em todas as árvores. Vamos achatar e eliminar a duplicação da nossa floresta e filtrar para manter apenas os elementos que aparecem em todas as árvores.
function findCommonDeps(depTrees) {
const depSet = new Set();
// Flatten
depTrees.forEach(depTree => {
depTree.forEach(dep => depSet.add(dep));
});
// Filter
return Array.from(depSet)
.filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}
Etapa 4: agrupar dependências compartilhadas
Para agrupar nosso conjunto de dependências compartilhadas, podemos concatenar todos os arquivos de módulo. Dois
problemas surgem ao usar essa abordagem: o primeiro é que o pacote ainda vai conter
instruções import
, que vão fazer com que o navegador tente buscar recursos. O segundo problema é
que as dependências das dependências não foram agrupadas. Como já fizemos isso antes, vamos
escrever outro plug-in do Babel.
O código é bastante semelhante ao nosso primeiro plug-in, mas, em vez de extrair as importações, também vamos removê-las e inserir uma versão agrupada do arquivo importado:
async function bundle(oldCode) {
// `newCode` will be filled with code fragments that eventually form the bundle.
let newCode = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
newCode.push((async function() {
// Bundle the imported file and add it to the output.
return await bundle(await readFile(`./app/${importedFile}`));
})());
// Remove the import declaration from the AST.
decl.remove();
}
}
};
// Save the stringified, transformed AST. This code is the same as `oldCode`
// but without any import statements.
const {code} = babel.transform(oldCode, {plugins: [plugin]});
newCode.push(code);
// `newCode` contains all the bundled dependencies as well as the
// import-less version of the code itself. Concatenate to generate the code
// for the bundle.
return flatten(await Promise.all(newCode)).join('\n');
}
Etapa 5: reescrever pontos de entrada
Na última etapa, vamos criar outro plug-in do Babel. A tarefa dele é remover todas as importações de módulos que estão no pacote compartilhado.
async function rewrite(section, sharedBundle) {
let oldCode = await readFile(`./app/static/${section}.js`);
oldCode = oldCode.toString('utf-8');
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// If this import statement imports a file that is in the shared bundle, remove it.
if(sharedBundle.includes(importedFile))
decl.remove();
}
}
};
let {code} = babel.transform(oldCode, {plugins: [plugin]});
// Prepend an import statement for the shared bundle.
code = `import '/static/_shared.js';\n${code}`;
await writeFile(`./app/static/_${section}.js`, code);
}
Fim
Foi uma longa jornada, não é mesmo? Lembre-se de que nosso objetivo neste episódio era explicar e desmistificar a divisão de código. O resultado funciona, mas é específico para nosso site de demonstração e vai falhar horrivelmente no caso genérico. Para produção, recomendo usar ferramentas estabelecidas, como WebPack, RollUp etc.
Encontre nosso código no repositório do GitHub.
Espero vocês no próximo curso!