Divisão de código no Angular ou como compartilhar componentes entre módulos preguiçosos

Este artigo fornecerá uma melhor compreensão de como o Angular dividiu seu código em partes.

Se você está com medo da saída da CLI angular mostrada acima ou se está curioso para saber como essa divisão de código realmente acontece, esta postagem é para você.

A divisão de código permite que você divida seu código em vários pacotes que podem ser carregados sob demanda. Se usado corretamente, pode ter um grande impacto no tempo de carregamento.

Conteúdo

  1. Por que eu deveria me importar?
  2. Divisão de código de CLI angular sob o capô
  3. Aplicação angular simples com módulos preguiçosos
  4. Como compartilhar componentes entre módulos preguiçosos
  5. Conclusão

Por que eu deveria me importar?

Vamos imaginar que você iniciou um novo projeto Angular. Você lê muitos recursos sobre como arquitetar um aplicativo Angular, qual é a estrutura de pastas apropriada e, o que é mais importante, como manter um ótimo desempenho de inicialização.

Você escolheu o Angular CLI e criou um aplicativo modular com muitos módulos de recursos com carregamento lento. E, é claro, você criou um módulo compartilhado no qual coloca diretivas, tubos e componentes usados ​​com frequência.

Depois de um tempo, você se pegou pensando que, uma vez que seu novo módulo de recurso requer alguma funcionalidade de outros módulos de recurso, você tende a mover essa funcionalidade para esse único módulo compartilhado.

O aplicativo evolui e logo você percebeu que o tempo de inicialização não atende às suas expectativas (e, principalmente, ao seu cliente).

Agora, você está em dúvida ...

  • Se eu colocar todos os meus pipes, diretivas e componentes comuns em um grande módulo compartilhado e importá-lo em módulos de carregamento lento (onde eu uso apenas um ou dois dos recursos importados), provavelmente poderá causar duplicatas de código não utilizado nos arquivos de saída .
  • Por outro lado, se eu dividir os recursos compartilhados entre vários módulos compartilhados e importar apenas aqueles necessários em cada módulo específico, isso reduzirá o tamanho do meu aplicativo? Ou Angular faz todas essas otimizações por padrão?

Vamos desmistificar!

Divisão de código de CLI angular sob o capô

Como todos sabemos, a versão atual da CLI Angular usa o webpack para executar o agrupamento. Mas, apesar disso, o webpack também é responsável pela divisão de código.

Então, vamos dar uma olhada em como o webpack faz isso.

O Webpack 4 introduziu o SplitChunksPlugin que nos permite definir algumas heurísticas para dividir os módulos em partes. Muitas pessoas reclamam que essa configuração parece misteriosa. E, ao mesmo tempo, esta é a parte mais interessante da divisão de código.

Porém, antes da otimização do SplitChunksPlugin ser aplicada, o webpack cria um novo bloco:

  • para cada ponto de entrada

A CLI angular configura os seguintes pontos de entrada

principais estilos de polyfills

o que resultará em pedaços com os mesmos nomes.

  • para cada módulo carregado dinamicamente (usando a sintaxe import () que está em conformidade com a proposta ECMAScript para importações dinâmicas)

Você se lembra da sintaxe loadChildren? Este é o sinal para o webpack criar um pedaço.

Agora vamos para o SplitChunksPlugin. Ele pode ser ativado dentro do bloco de otimização do webpack.config.js

Vamos dar uma olhada no código-fonte da CLI Angular e encontrar essa seção de configuração:

Configuração do SplitChunksPlugin na CLI Angular 8

Vamos nos concentrar aqui nas opções de cacheGroups, já que esta é a "receita" do webpack sobre como criar chunks separados com base em algumas condições.

cacheGroups é um objeto simples em que a chave é um nome de grupo. Basicamente, podemos pensar em um grupo de cache como uma oportunidade potencial para a criação de um novo bloco.

Cada grupo tem muitas configurações e pode herdar a configuração do nível splitChunks.

Vamos ver rapidamente as opções que vimos na configuração da CLI Angular acima:

  • O valor de chunks pode ser usado para filtrar módulos entre os sync e async. Seu valor pode ser inicial, assíncrono ou tudo. inicial significa apenas adicionar arquivos ao bloco se forem importados dentro de blocos de sincronização. async significa apenas adicionar arquivos ao bloco se forem importados dentro de blocos assíncronos (async por padrão)
  • minChunks diz ao webpack para injetar apenas módulos no chunk se eles forem compartilhados entre pelo menos 2 chunks (1 por padrão)
  • name diz ao webpack para usar esse nome para um pedaço recém-criado. Especificar uma string ou uma função que sempre retorna a mesma string mesclará todos os módulos comuns em um único pedaço.
  • O valor de prioridade é usado para identificar os blocos com melhor correspondência quando um módulo se enquadra em muitos grupos de blocos.
  • enforce diz ao webpack para ignorar as opções minSize, minChunks, maxAsyncRequests e maxInitialRequests e sempre cria pedaços para este grupo de cache. Há uma pequena pegadinha aqui: se alguma dessas opções ignoradas for fornecida no nível cacheGroup, essa opção ainda será usada.
  • test controla quais módulos são selecionados por esse grupo de cache. Como pudemos observar, a CLI angular usa essa opção para mover todas as dependências node_modules para o pedaço do fornecedor.
  • minSize é usado para identificar o tamanho mínimo, em bytes, para que um pedaço seja gerado. Ele não apareceu na configuração da CLI Angular, mas é uma opção muito importante da qual devemos estar cientes. (Como o código-fonte indica, são 30kb por padrão na produção e 10kb no ambiente de desenvolvimento)
Dica: apesar de a documentação do webpack definir padrões, eu me referiria ao código-fonte do webpack para encontrar os valores exatos

Vamos recapitular aqui: a CLI angular moverá um módulo para:

  • parte do fornecedor se esse módulo vier do diretório node_modules.
  • pedaço padrão se esse módulo for importado dentro de um módulo assíncrono e compartilhado entre pelo menos dois módulos. Observe que muitos blocos padrão são possíveis aqui. Explicarei como o webpack gera nomes para esses blocos posteriormente.
  • pedaço comum se esse módulo for importado dentro de um módulo assíncrono e compartilhado entre pelo menos dois módulos e não se enquadra no pedaço padrão (prioridade olá) e também não importa qual o tamanho (graças à opção força)

Teoria suficiente, vamos praticar.

Aplicação angular simples com módulos preguiçosos

Para explicar o processo do SplitChunksPlugin, começaremos com uma versão simplificada do aplicativo Angular:

app ├── a (preguiçoso) │── a.component.ts │ └── a.module.ts │ ├── ab │ ─── ab.component.ts │ └── ab.module.ts │ ├── b (preguiçoso) │── b.component.ts │ └── b.module.ts │ └── c (preguiçoso) │ └── c.component.ts │ └── c.module. ts │ └── cd │ ─── cd.component.ts │ ── cd.module.ts │ └── d (preguiçoso) │ ─── d.component.ts │ └── d.module.ts │ ─── shared │ ── shared.module.ts │ ─── app.component.ts └── app.module.ts

Aqui a, b, c e d são módulos preguiçosos, o que significa que são importados usando a sintaxe import ().

Os componentes aeb usam um componente ab em seus modelos. os componentes c e d usam o componente cd.

Dependências entre módulos angulares

A diferença entre ab.module e cd.module é que ab.module é importado em a.module e b.module enquanto cd.module é importado em shared.module.

Essa estrutura descreve exatamente as dúvidas que queríamos desmistificar. Vamos descobrir onde os módulos ab e cd estarão na saída final.

Algoritmo

1) O algoritmo do SplitChunksPlugin começa fornecendo um índice a cada bloco criado anteriormente.

pedaços por índice

2) Em seguida, percorre todos os módulos em compilação para preencher o mapa chunkSetsInGraph. Este dicionário mostra quais blocos compartilham o mesmo código.

chunkSetsInGraph

Por exemplo, 1,2 linha principal de polyfill significa que existe pelo menos um módulo que aparece em dois blocos: main e polyfill.

Os módulos aeb compartilham o mesmo código fromab-module para que também possamos observar a combinação (4,5) acima.

3) Percorra todos os módulos e descubra se é possível criar um novo bloco para um cacheGroup específico.

3a) Antes de tudo, o webpack determina se um módulo pode ser adicionado ao cacheGroup específico, verificando a propriedade doacheGroup.test.

testes de ab.module

teste padrão indefinido => ok
teste comum indefinido => ok
função de teste do fornecedor => false

o grupo de cache padrão e comum não definiu a propriedade test; portanto, deve transmiti-la. O grupo de cache do fornecedor define uma função em que existe um filtro para incluir apenas módulos do caminho thenode_modules.

Os testes do cd.module são os mesmos.

3b) Agora é hora de percorrer todas as combinações de partes.

Cada módulo entende em quais partes ele aparece (graças à propriedade module.chunksIterable).

O ab.module é importado para dois blocos preguiçosos. Portanto, suas combinações são (4), (5) e (4,5).

Por outro lado, o cd.module é importado apenas no módulo compartilhado, o que significa que é importado apenas no bloco principal. Suas combinações são apenas (1).

Em seguida, o plug-in filtra combinações por tamanho minChunk:

se (chunkCombination.size 

Como o ab.module possui a combinação (4,5), deve passar nessa verificação. Isso não podemos dizer sobre o cd.module. Neste ponto, este módulo permanece dentro do bloco principal.

3c) Há mais uma verificação por cacheGroup.chunkds (inicial, assíncrona ou todas)

O ab.module é importado dentro de pedaços assíncronos (carregados com atraso). É exatamente isso que os grupos de cache padrão e comum exigem. Dessa forma, o ab.module é adicionado a dois novos blocos possíveis (padrão e comum).

Eu prometi mais cedo, então aqui vamos nós.

Como o webpack gera o nome de um pedaço criado pelo SplitChunksPlugin?

A versão simplificada disso pode ser representada como

Onde:

  • groupName é o nome do grupo (padrão no nosso caso)
  • ~ é um defaultAutomaticNameDelimiter
  • chunkNames refere-se à lista de todos os nomes de chunk incluídos nesse grupo. Esse nome é como um caminho fullPath, mas em vez de barra, ele usa -.

Por exemplo, dd-module significa que temos o arquivo d.module na pasta d.

Portanto, usando isso importamos ('./a / a.module') e importamos ('./b / b.module') obtemos

Estrutura do nome do bloco padrão

Mais uma coisa que vale a pena mencionar é que, quando o tamanho de um nome de bloco atinge 109 caracteres, o webpack o corta e adiciona um pouco de hash no final.

Estrutura do nome do grande bloco que compartilha o código entre vários módulos preguiçosos

Estamos prontos para preencher chunksInfoMap, que sabe tudo sobre todos os novos chunks possíveis e também sabe em quais módulos ele deve consistir e em chunks relacionados onde esses módulos residem atualmente.

chunksInfoMap

É hora de filtrar possíveis pedaços

O SplitChunksPlugin faz um loop nos itens de chunksInfoMap para encontrar a melhor entrada correspondente. O que isso significa?

o grupo de cache padrão tem uma prioridade 10 que supercomponha o peso comum (que possui apenas 5). Isso significa que o padrão é a melhor entrada correspondente e deve ser processado primeiro.

Quando todos os outros requisitos forem atendidos, o webpack remove todos os módulos do chunk de outros possíveis chunks no dicionário chunksInfoMap. Se não houver mais módulo, o módulo será excluído

Dessa forma, o ~ aa-module ~ bb-module padrão tem precedência sobre o bloco comum. O último é removido, pois contém a mesma lista de módulos.

Por último, mas não menos importante, a etapa é fazer algumas otimizações (como remover duplicações) e garantir que todos os requisitos, como o maxSize, sejam atendidos.

Todo o código fonte do SplitChunksPlugin pode ser encontrado aqui

Descobrimos que o webpack cria blocos de três maneiras diferentes:

  • para cada entrada
  • para módulos carregados dinamicamente
  • para código compartilhado com a ajuda de SplitChunksPlugin
Saída angular da CLI por tipo de chunks

Agora, voltemos às nossas dúvidas sobre qual é a melhor maneira de manter o código compartilhado.

Como compartilhar componentes entre módulos preguiçosos

Como vimos em nosso aplicativo Angular simples, o webpack criou um chunk separado para o ab.module, mas incluiu o cd.module no chunk principal.

Vamos resumir os principais tópicos deste post:

  • Se colocarmos todos os pipes compartilhados, diretivas e componentes comuns em um grande módulo compartilhado e importá-lo em qualquer lugar (dentro de partes de sincronização e assíncrona), esse código estará no nosso bloco principal inicial. Portanto, se você deseja obter um desempenho ruim de carregamento inicial, esse é o caminho.
  • Por outro lado, se dividirmos o código comumente usado em módulos carregados com preguiça, um novo bloco compartilhado será criado e será carregado apenas se algum desses módulos preguiçosos for carregado. Isso deve melhorar o carregamento inicial do aplicativo. Mas faça isso com sabedoria, porque às vezes é melhor colocar um código pequeno em um pedaço que tenha a solicitação extra necessária para uma carga de pedaço separada.

Conclusão

Espero que agora você entenda claramente a saída da CLI Angular e faça uma distinção entre entrada, dinâmica e dividida usando os pedaços SplitChunksPlugin.

Feliz codificação!