"
Acho que os compiladores são muito interessantes ", diz Uri Shaked, autor do material, que estamos publicando hoje. No ano passado, ele escreveu
um artigo que falava sobre engenharia reversa do compilador Angular e simulação de algumas etapas do processo de compilação para ajudar a entender os recursos da estrutura interna desse mecanismo. Deve-se notar que geralmente o que o autor deste material fala como um "compilador" é chamado de "mecanismo de renderização".
Quando Uri soube que uma nova versão do compilador Angular, chamada Ivy, foi lançada, ele imediatamente quis dar uma olhada e descobrir o que havia mudado nela em comparação à versão antiga. Aqui, como antes, o compilador recebe os modelos e componentes criados pelo Angular, que são convertidos em código HTML e JavaScript comum, compreensível para o Chrome e outros navegadores.

Se você comparar a nova versão do compilador com a anterior, o Ivy usa o algoritmo de agitação de árvore. Isso significa que o compilador exclui automaticamente fragmentos de código não utilizados (isso também se aplica ao código angular), reduzindo o tamanho dos pacotes de projetos. Outra melhoria diz respeito ao fato de que agora cada arquivo é compilado independentemente, o que reduz o tempo de recompilação. Em poucas palavras, graças ao novo
compilador, obtemos montagens menores, recompilação acelerada de projetos, código pronto mais simples.
Entender como o compilador funciona é interessante por si só (pelo menos o autor do material espera isso), mas também ajuda a entender melhor os mecanismos internos do Angular. Isso leva ao aprimoramento das habilidades do "pensamento angular", que, por sua vez, permite que você use mais efetivamente essa estrutura para o desenvolvimento da web.
A propósito, você sabe por que o novo compilador foi chamado Ivy? O fato é que essa palavra soa como uma combinação de letras “IV”, lidas em voz alta, que representa o número 4, escrito em algarismos romanos. "4" é a quarta geração de compiladores Angular.
Aplicação Ivy
Ivy ainda está em processo de desenvolvimento intensivo, esse processo pode ser observado
aqui . Embora o próprio compilador ainda não seja adequado para uso em combate, a abstração do RendererV3, que ele usará, já é bastante funcional e vem com o Angular 6.x.
Embora Ivy ainda não esteja totalmente pronta, ainda podemos dar uma olhada nos resultados de seu trabalho. Como fazer isso? Criando um novo projeto Angular:
ng new ivy-internals
Depois disso, você precisa habilitar o Ivy adicionando as seguintes linhas ao arquivo
tsconfig.json
localizado na nova pasta do projeto:
"angularCompilerOptions": { "enableIvy": true }
E, finalmente, iniciamos o compilador executando o comando
ngc
na pasta do projeto recém-criada:
node_modules/.bin/ngc
Isso é tudo. Agora você pode examinar o código gerado localizado na
dist/out-tsc
. Por exemplo, dê uma olhada no seguinte fragmento do modelo
AppComponent
:
<div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div>
Aqui estão alguns links para ajudar você a começar:
O código gerado para este modelo pode ser encontrado no
dist/out-tsc/src/app/app.component.js
:
i0.ɵE(0, , _c0); i0.ɵE(1, ); i0.ɵT(2); i0.ɵe(); i0.ɵE(3, , _c1); i0.ɵe(); i0.ɵe(); i0.ɵE(4, ); i0.ɵT(5, ); i0.ɵe();
É nesse tipo de código JavaScript que Ivy transforma o modelo de componente. Aqui está como a mesma coisa foi feita na versão anterior do compilador:
Código produzido por uma versão anterior do compilador AngularHá um sentimento de que o código que Ivy gera é muito mais simples. Você pode experimentar o modelo de componente (localizado em
src/app/app.component.html
), compilá-lo novamente e ver como as alterações feitas nele afetarão o código gerado.
Analisar código gerado
Vamos tentar analisar o código gerado e ver exatamente quais ações ele executa. Por exemplo, vamos procurar uma resposta para uma pergunta sobre o significado de chamadas como
i0.ɵE
e
i0.ɵT
Se você observar o início do arquivo gerado, encontraremos a seguinte expressão:
var i0 = require("@angular/core");
Portanto,
i0
é apenas o módulo principal do Angular, e todas essas são as funções exportadas pelo Angular. A letra
ɵ
usada pela equipe de desenvolvimento Angular para indicar que alguns métodos destinam-se apenas a fornecer
mecanismos internos de estrutura, ou seja, os usuários não devem chamá-los diretamente, pois a invariância da API desses métodos não é garantida quando novas versões do Angular são lançadas (na verdade, Eu diria que é quase garantido que suas APIs mudam).
Portanto, todos esses métodos são APIs privadas exportadas pelo Angular. É fácil descobrir sua funcionalidade abrindo o projeto no VS Code e analisando as dicas de ferramentas:
Análise de código no código VSMesmo que um arquivo JavaScript seja analisado aqui, o VS Code usa informações de tipo do TypeScript para identificar a assinatura da chamada e encontrar a documentação para um método específico. Se, após selecionar o nome do método, use a combinação Ctrl + clique (Cmd + clique no Mac), descobrimos que o nome real desse método é
elementStart
.
Essa técnica tornou possível descobrir que o nome do método
ɵT
é
text
, o nome do método
ɵe
é
elementEnd
. Armado com esse conhecimento, podemos "traduzir" o código gerado, transformando-o em algo que será mais conveniente para ler. Aqui está um pequeno fragmento dessa "tradução":
var core = require("angular/core"); //... core.elementStart(0, "div", _c0); core.elementStart(1, "h1"); core.text(2); core. (); core.elementStart(3, "img", _c1); core.elementEnd(); core.elementEnd(); core.elementStart(4, "h2"); core.text(5, "Here are some links to help you start: "); core.elementEnd();
E, como já mencionado, esse código corresponde ao seguinte texto do modelo HTML:
<div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div>
Aqui estão alguns links para ajudar você a começar:
Depois de analisar tudo isso, é fácil perceber o seguinte:
- Cada tag HTML de abertura possui uma chamada para
core.elementStart()
. - As tags de fechamento correspondem às chamadas para
core.elementEnd()
. - Nós de texto correspondem a chamadas para
core.text()
.
O primeiro argumento para os métodos
elementStart
e
text
é um número cujo valor aumenta a cada chamada. Provavelmente, representa um índice em alguma matriz na qual o Angular armazena links para elementos criados.
O terceiro argumento também é passado para o método
elementStart
. Tendo estudado os materiais acima, podemos concluir que o argumento é opcional e contém uma lista de atributos para o nó DOM. Você pode verificar isso observando o valor de
_c0
e descobrindo que ele contém uma lista de atributos e seus valores para o elemento
div
:
var _c0 = ["style", "text-align:center"];
NgComponentDef Nota
Até o momento, analisamos a parte do código gerado responsável pela renderização do modelo para o componente. Na verdade, esse código está localizado em um pedaço maior de código atribuído a
AppComponent.ngComponentDef
- uma propriedade estática que contém todos os metadados sobre o componente, como seletores CSS, sua estratégia de detecção de alterações (se um for especificado) e o modelo. Se você sente desejo de aventura - agora pode descobrir de maneira independente como isso funciona, embora falaremos sobre isso a seguir.
Ivy caseiro
Agora que, em termos gerais, entendemos como é o código gerado, podemos tentar criar, a partir do zero, nosso próprio componente usando a mesma API RendererV3 usada pela Ivy.
O código que vamos criar será semelhante ao código que o compilador produz, mas faremos com que seja mais fácil de ler.
Vamos começar escrevendo um componente simples e depois convertê-lo manualmente em código semelhante ao obtido pelo Ivy:
import { Component } from '@angular/core'; @Component({ selector: 'manual-component', template: '<h2><font color="#3AC1EF">Hello, Component</font></h2>', }) export class ManualComponent { }
O compilador usa a entrada do decorador
@component
como uma
@component
, cria instruções e organiza tudo como uma propriedade estática da classe de componente. Portanto, para simular a atividade do Ivy, removemos o decorador
@component
e o substituímos
ngComponent
propriedade estática
ngComponent
:
import * as core from '@angular/core'; export class ManualComponent { static ngComponentDef = core.ɵdefineComponent({ type: ManualComponent, selectors: [['manual-component']], factory: () => new ManualComponent(), template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
Definimos metadados para o componente compilado chamando
ɵdefineComponent
. Os metadados incluem o tipo de componente (usado anteriormente para implementar a dependência), o seletor (ou seletores) de CSS que chamará esse componente (no nosso caso,
manual-component
é o nome do componente no modelo HTML), a fábrica que retorna a nova instância componente e, em seguida, a função que define o modelo para o componente. Este modelo exibe uma representação visual do componente e a atualiza quando as propriedades do componente são alteradas. Para criar esse modelo, usaremos os métodos que encontramos acima:
ɵE
,
ɵe
e
ɵT
.
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { core.ɵE(0, 'h2'); // h2 core.ɵT(1, 'Hello, Component'); // core.ɵe(); // h2 },
Nesta fase, não usamos os parâmetros
rf
ou
ctf
fornecidos pela nossa função de modelo. Voltaremos a eles. Mas primeiro, vamos ver como exibir nosso primeiro componente caseiro na tela.
Primeira aplicação
Para exibir componentes na tela, o Angular exporta um método chamado
ɵrenderComponent
. Tudo o que você precisa fazer é verificar se o arquivo
index.html
contém uma tag HTML correspondente ao seletor de elemento,
<manual-component>
, e depois adicionar o seguinte ao final do arquivo:
core.ɵrenderComponent(ManualComponent);
Isso é tudo. Agora, temos um aplicativo Angular mínimo feito por você, composto apenas por 16 linhas de código. Você pode experimentar o aplicativo finalizado no
StackBlitz .
Mecanismo de detecção de alterações
Então, nós temos um exemplo de trabalho. Você pode adicionar interatividade a ele? Digamos, que tal algo interessante, como usar o sistema de detecção de alterações da Angular aqui?
Altere o componente para que o usuário possa personalizar o texto de boas-vindas. Ou seja, em vez de o componente sempre exibir o texto
Hello, Component
, permitiremos que o usuário altere a parte do texto que vem depois do
Hello
.
Começamos adicionando a propriedade
name
e um método para atualizar o valor dessa propriedade na classe de componente:
export class ManualComponent { name = 'Component'; updateName(newName: string) { this.name = newName; }
Embora tudo isso não pareça particularmente impressionante, mas o mais interessante está à frente.
Em seguida, editaremos a função de modelo para que, em vez de texto imutável, ela exiba o conteúdo da propriedade
name
:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { // : core.ɵE(0, 'h2'); core.ɵT(1, 'Hello, '); core.ɵT(2); // <-- name core.ɵe(); } if (rf & 2) { // : core.ɵt(2, ctx.name); // ctx - } },
Você deve ter notado que colocamos as instruções do modelo em instruções
if
que verificam os valores de
rf
. Este parâmetro é usado pelo Angular para indicar se o componente está sendo criado pela primeira vez (o bit menos significativo será
definido ) ou apenas precisamos atualizar o conteúdo dinâmico no processo de detecção de alterações (é a segunda
if
essa é a finalidade).
Portanto, quando o componente é exibido pela primeira vez, criamos todos os elementos e, quando são detectadas alterações, atualizamos apenas o que pode mudar. O método interno é responsável por isso (observe a letra minúscula
t
), que corresponde à função
textBinding
exportada pelo Angular:
Função textBindingPortanto, o primeiro parâmetro é o índice do elemento a ser atualizado, o segundo é o valor. Nesse caso, criamos um elemento de texto vazio com o índice 2 com o comando
core.ɵT(2);
. Ele atua como um espaço reservado para o
name
. Nós o atualizamos com o comando
core.ɵt(2, ctx.name);
após a detecção de uma alteração na variável correspondente.
No momento, a saída desse componente ainda exibirá o texto
Hello, Component
, embora possamos alterar o valor da propriedade
name
, o que levará a uma alteração no texto na tela.
Para que o aplicativo se torne verdadeiramente interativo, adicionaremos aqui um campo de entrada de dados com um ouvinte de eventos que chama o método de componente
updateName()
:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { core.ɵE(0, 'h2'); core.ɵT(1, 'Hello, '); core.ɵT(2); core.ɵe(); core.ɵT(3, 'Your name: '); core.ɵE(4, 'input'); core.ɵL('input', $event => ctx.updateName($event.target.value)); core.ɵe(); }
A ligação de evento é realizada na linha
core.ɵL('input', $event => ctx.updateName($event.target.value));
. Ou seja, o método
ɵL
responsável por definir o ouvinte de eventos para os elementos mais recentes declarados. O primeiro argumento é o nome do evento (nesse caso,
input
é o evento gerado quando o conteúdo do elemento
<input>
muda), o segundo argumento é um retorno de chamada. Esse retorno de chamada aceita dados do evento como argumento. Em seguida, extraímos o valor atual do elemento de destino do evento, ou seja, do elemento
<input>
e passamos para a função no componente.
O código acima é equivalente a escrever o seguinte HTML em um modelo:
Your name: <input (input)="updateName($event.target.value)" />
Agora você pode editar o conteúdo do elemento
<input>
e observar como o texto no componente é alterado. No entanto, o campo de entrada não é preenchido quando o componente é carregado. Para que tudo funcione dessa maneira, você precisa adicionar mais uma instrução ao código de função do modelo, executado quando uma alteração é detectada:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { ... } if (rf & 2) { core.ɵt(2, ctx.name); core.ɵp(4, 'value', ctx.name); } }
Aqui, usamos outro método interno do sistema de renderização,
ɵp
, que atualiza a propriedade de um elemento com um determinado índice. Nesse caso, o índice 4 é passado para o método, que é o índice atribuído ao elemento de
input
, e
ctx.name
método para colocar o valor
ctx.name
na propriedade
value
desse elemento.
Agora nosso exemplo está finalmente pronto. Implementamos, do zero, ligação de dados bidirecional usando a API do sistema de renderização Ivy. Isso é ótimo.
Aqui você pode experimentar o código finalizado.
Agora estamos familiarizados com a maioria dos componentes básicos do novo compilador Ivy. Sabemos como criar elementos e nós de texto, como vincular propriedades e configurar ouvintes de eventos e como usar o sistema de detecção de alterações.
Sobre os blocos * ngIf e * ngFor
Antes de terminarmos o estudo da Ivy, vejamos outro tópico interessante. Nomeadamente, vamos falar sobre como o compilador funciona com subpadrões. Esses são os padrões usados para os
*ngIf
ou
*ngFor
. Eles são processados de uma maneira especial. Vamos ver como usar
*ngIf
em nosso código de modelo caseiro.
Primeiro você precisa instalar o pacote npm
@angular/common
- é aqui que
*ngIf
. Em seguida, você precisa importar a diretiva deste pacote:
import { NgIf } from '@angular/common';
Agora, para poder usar o
NgIf
no modelo, é necessário fornecer alguns metadados, pois o módulo
@angular/common
não foi compilado usando o Ivy (pelo menos enquanto escrevia o material, e no futuro isso provavelmente mudará de introdução do
ngcc ).
Vamos usar o método
ɵdefineDirective
, que está relacionado ao método familiar
ɵdefineComponent
. Ele define metadados para diretivas:
(NgIf as any).ngDirectiveDef = core.ɵdefineDirective({ type: NgIf, selectors: [['', 'ngIf', '']], factory: () => new NgIf(core.ɵinjectViewContainerRef(), core.ɵinjectTemplateRef()), inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'} });
Encontrei essa definição no
código fonte Angular , junto com a
ngFor
. Agora que preparamos o
NgIf
para uso no Ivy, podemos adicionar o seguinte à lista de diretivas do componente:
static ngComponentDef = core.ɵdefineComponent({ directives: [NgIf], // ... });
Em seguida, definimos o subpadrão apenas para a partição delimitada por
*ngIf
.
Suponha que você precise exibir uma imagem. Vamos definir uma nova função para este modelo dentro da função de modelo:
function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) { if (rf & 1) { core.ɵE(0, 'div'); core.ɵE(1, 'img', ['src', 'https://pbs.twimg.com/tweet_video_thumb/C80o289UQAAKIqp.jpg']); core.ɵe(); } }
Essa função de modelo não é diferente da que já escrevemos. Ele usa as mesmas construções para criar um elemento
img
dentro de um elemento
div
.
E, finalmente, podemos juntar tudo adicionando a diretiva
ngIf
ao modelo de componente:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { // ... core.ɵC(5, ifTemplate, null, ['ngIf']); } if (rf & 2) { // ... core.ɵp(5, 'ngIf', (ctx.name === 'Igor')); } function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) { // ... } },
Observe a chamada para o novo método no início do código (
core.ɵC(5, ifTemplate, null, ['ngIf']);
). Ele declara um novo elemento de contêiner, ou seja, um elemento que possui um modelo. O primeiro argumento é o índice do elemento, já vimos esses índices. O segundo argumento é a função subpadrão que acabamos de definir. Será usado como modelo para o elemento container. O terceiro parâmetro é o nome da tag do elemento, o que não faz sentido aqui e, finalmente, há uma lista de diretivas e atributos associados a esse elemento. É
ngIf
entra o
ngIf
.
Na linha
core.ɵp(5, 'ngIf', (ctx.name === 'Igor'));
o estado do elemento é atualizado ligando o atributo
ngIf
ao valor da expressão lógica
ctx.name === 'Igor'
. Isso verifica se a propriedade
name
do componente é igual a
Igor
.
O código acima é equivalente ao seguinte código HTML:
<div *ngIf="name === 'Igor'"> <img align="center" src="..."> </div>
Aqui pode-se notar que o novo compilador não produz o código mais compacto, mas não é tão ruim em comparação com o que é agora.
Você pode experimentar um novo exemplo
aqui . Para ver a seção
NgIf
em ação, digite o nome
Igor
no campo
Your name
.
Sumário
Nós viajamos pelos recursos do compilador Ivy. Esperamos que esta viagem tenha despertado seu interesse em explorar mais a Angular. Nesse caso, agora você tem tudo o que precisa para experimentar o Ivy. Agora você sabe como "traduzir" modelos para JavaScript, como acessar os mesmos mecanismos angulares que Ivy usa sem usar este compilador. Suponho que tudo isso lhe dará a oportunidade de explorar os novos mecanismos angulares tão profundamente quanto você desejar.
Aqui ,
aqui e
aqui - três materiais nos quais você pode encontrar informações úteis sobre Ivy. E
aqui está o código fonte do Render3.
Caros leitores! Como você se sente sobre os novos recursos do Ivy?
