Un estudio de Ivy, el nuevo compilador angular

" Creo que los compiladores son muy interesantes ", dice Uri Shaked, el autor del material, que publicamos hoy. El año pasado, escribió un artículo que hablaba sobre ingeniería inversa del compilador Angular y simulaba algunas etapas del proceso de compilación, lo que ayuda a comprender las características de la estructura interna de este mecanismo. Cabe señalar que generalmente lo que el autor de este material habla como un "compilador" se llama un "motor de renderizado".

Cuando Uri escuchó que se lanzó una nueva versión del compilador Angular, llamada Ivy, inmediatamente quiso echar un vistazo más de cerca y descubrir qué había cambiado en comparación con la versión anterior. Aquí, como antes, el compilador recibe las plantillas y componentes creados por Angular, que se convierten en código HTML y JavaScript normal que es comprensible para Chrome y otros navegadores.



Si compara la nueva versión del compilador con la anterior, resulta que Ivy usa el algoritmo de sacudida de árboles. Esto significa que el compilador elimina automáticamente los fragmentos de código no utilizados (esto también se aplica al código angular), reduciendo el tamaño de los paquetes de proyectos. Otra mejora se refiere al hecho de que ahora cada archivo se compila de forma independiente, lo que reduce el tiempo de recompilación. En pocas palabras, entonces, gracias al nuevo compilador, obtenemos ensamblajes más pequeños, recompilación acelerada de proyectos, código listo más simple.

Comprender cómo funciona el compilador es interesante en sí mismo (al menos el autor del material lo espera), pero también ayuda a comprender mejor los mecanismos internos de Angular. Esto conduce a la mejora de las habilidades del "pensamiento angular", que, a su vez, le permite utilizar de manera más eficaz este marco para el desarrollo web.

Por cierto, ¿sabes por qué el nuevo compilador se llamaba Ivy? El hecho es que esta palabra suena como una combinación de letras "IV", leídas en voz alta, que representa el número 4, escrito en números romanos. "4" es la cuarta generación de compiladores angulares.

Aplicación de hiedra


Ivy todavía está en proceso de desarrollo intensivo, este proceso se puede observar aquí . Aunque el compilador en sí aún no es adecuado para el uso en combate, la abstracción de RendererV3, que utilizará, ya es bastante funcional y viene con Angular 6.x.

Aunque Ivy aún no está listo, aún podemos echar un vistazo a los resultados de su trabajo. Como hacerlo Al crear un nuevo proyecto angular:

ng new ivy-internals 

Después de eso, debe habilitar Ivy agregando las siguientes líneas al archivo tsconfig.json ubicado en la nueva carpeta del proyecto:

 "angularCompilerOptions": { "enableIvy": true } 

Y finalmente, comenzamos el compilador ejecutando el comando ngc en la carpeta del proyecto recién creado:

 node_modules/.bin/ngc 

Eso es todo Ahora puede examinar el código generado ubicado en la dist/out-tsc . Por ejemplo, eche un vistazo al siguiente fragmento de la plantilla AppComponent :

 <div style="text-align:center"> <h1>   Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div> 

Aquí hay algunos enlaces para ayudarlo a comenzar:


El código generado para esta plantilla se puede encontrar mirando el dist/out-tsc/src/app/app.component.js :

 i0.ɵE(0, "div", _c0); i0.ɵE(1, "h1"); i0.ɵT(2); i0.ɵe(); i0.ɵE(3, "img", _c1); i0.ɵe(); i0.ɵe(); i0.ɵE(4, "h2"); i0.ɵT(5, "Here are some links to help you start: "); i0.ɵe(); 

Es en este tipo de JavaScript que Ivy transforma la plantilla del componente. Así es como se hizo lo mismo en la versión anterior del compilador:


Código producido por una versión anterior del compilador angular

Existe la sensación de que el código que genera Ivy es mucho más simple. Puede experimentar con la plantilla del componente (se encuentra en src/app/app.component.html ), compilarla nuevamente y ver cómo los cambios realizados afectarán el código generado.

Analizar código generado


Intentemos analizar el código generado y ver exactamente qué acciones realiza. Por ejemplo, busquemos una respuesta a una pregunta sobre el significado de llamadas como i0.ɵE e i0.ɵT

Si observa el comienzo del archivo generado, allí encontraremos la siguiente expresión:

 var i0 = require("@angular/core"); 

Entonces i0 es solo el módulo central Angular, y todas estas son las funciones exportadas por Angular. La letra ɵ utilizada por el equipo de desarrollo de Angular para indicar que algunos métodos están destinados únicamente a proporcionar mecanismos de marco interno , es decir, los usuarios no deben llamarlos directamente, ya que la invariabilidad de la API de estos métodos no está garantizada cuando se lanzan nuevas versiones de Angular (de hecho, Diría que sus API están casi garantizadas a cambiar).

Por lo tanto, todos estos métodos son API privadas exportadas por Angular. Es fácil descubrir su funcionalidad abriendo el proyecto en VS Code y analizando la información sobre herramientas:


Análisis de código en código VS

Aunque aquí se analiza un archivo JavaScript, VS Code utiliza información de tipo de TypeScript para identificar la firma de la llamada y buscar documentación para un método en particular. Si, después de haber seleccionado el nombre del método, use la combinación Ctrl + clic (Cmd + clic en Mac), descubriremos que el nombre real de este método es elementStart .

Esta técnica permitió descubrir que el nombre del método ɵT es text , el nombre del método ɵe es ɵe . Armados con este conocimiento, podemos "traducir" el código generado, convirtiéndolo en algo que sea más conveniente para leer. Aquí hay un pequeño fragmento de tal "traducción":

 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(); 

Y, como ya se mencionó, este código corresponde al siguiente texto de la plantilla HTML:

 <div style="text-align:center"> <h1>   Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div> 

Aquí hay algunos enlaces para ayudarlo a comenzar:


Después de analizar todo esto, es fácil notar lo siguiente:

  • Cada etiqueta HTML de apertura tiene una llamada a core.elementStart() .
  • Las etiquetas de cierre corresponden a llamadas a core.elementEnd() .
  • Los nodos de texto corresponden a llamadas a core.text() .

El primer argumento para los métodos elementStart y text es un número cuyo valor aumenta con cada llamada. Probablemente representa un índice en alguna matriz en la que Angular almacena enlaces a elementos creados.

El tercer argumento también se pasa al método elementStart . Habiendo estudiado los materiales anteriores, podemos concluir que el argumento es opcional y contiene una lista de atributos para el nodo DOM. Puede verificar esto mirando el valor de _c0 y descubriendo que contiene una lista de atributos y sus valores para el elemento div :

 var _c0 = ["style", "text-align:center"]; 

NgComponentDef Note


Hasta ahora, hemos analizado la parte del código generado que es responsable de representar la plantilla para el componente. Este código se encuentra realmente en un fragmento de código más grande que se asigna a AppComponent.ngComponentDef , una propiedad estática que contiene todos los metadatos sobre el componente, como los selectores CSS, su estrategia de detección de cambios (si se especifica uno) y la plantilla. Si sientes ansias de aventura, ahora puedes descubrir de forma independiente cómo funciona, aunque hablaremos de ello a continuación.

Hiedra casera


Ahora que, en términos generales, entendemos cómo se ve el código generado, podemos intentar crear, desde cero, nuestro propio componente utilizando la misma API RendererV3 que utiliza Ivy.

El código que vamos a crear será similar al código que produce el compilador, pero lo haremos para que sea más fácil de leer.

Comencemos escribiendo un componente simple, y luego traduzca manualmente en un código similar al obtenido por Ivy:

 import { Component } from '@angular/core'; @Component({ selector: 'manual-component', template: '<h2><font color="#3AC1EF">Hello, Component</font></h2>', }) export class ManualComponent { } 

El compilador toma la entrada del decorador @component como @component , crea instrucciones y luego lo organiza todo como una propiedad estática de la clase componente. Por lo tanto, para simular la actividad de Ivy, eliminamos el decorador @component y lo reemplazamos con la propiedad 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 metadatos para el componente compilado llamando a ɵdefineComponent . Los metadatos incluyen el tipo de componente (utilizado anteriormente para implementar la dependencia), el selector (o selectores) CSS que llamará a este componente (en nuestro caso, manual-component es el nombre del componente en la plantilla HTML), la fábrica que devuelve la nueva instancia componente, y luego la función que define la plantilla para el componente. Esta plantilla muestra una representación visual del componente y la actualiza cuando cambian las propiedades del componente. Para crear esta plantilla, utilizaremos los métodos que encontramos arriba: ɵE , ɵe y ɵT .

     template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {     core.ɵE(0, 'h2');                 //    h2     core.ɵT(1, 'Hello, Component');   //       core.ɵe();                        //    h2   }, 

En esta etapa, no usamos los parámetros rf o ctf proporcionados por nuestra función de plantilla. Volveremos a ellos. Pero primero, veamos cómo mostrar nuestro primer componente casero en la pantalla.

Primera aplicación


Para mostrar los componentes en la pantalla, Angular exporta un método llamado ɵrenderComponent . Todo lo que necesita hacer es verificar que el archivo index.html contenga una etiqueta HTML correspondiente al selector de elementos, <manual-component> , y luego agregue lo siguiente al final del archivo:

 core.ɵrenderComponent(ManualComponent); 

Eso es todo Ahora tenemos una mínima aplicación Angular hecha a sí misma que consta de solo 16 líneas de código. Puede experimentar con la aplicación terminada en StackBlitz .

Cambiar mecanismo de detección


Entonces, tenemos un ejemplo de trabajo. ¿Puedes agregarle interactividad? Digamos, ¿qué tal algo interesante, como usar el sistema de detección de cambios de Angular aquí?

Cambie el componente para que el usuario pueda personalizar el texto de bienvenida. Es decir, en lugar de que el componente siempre muestre el texto Hello, Component , vamos a permitir que el usuario cambie la parte del texto que viene después de Hello .

Comenzamos agregando la propiedad de name y un método para actualizar el valor de esta propiedad a la clase de componente:

 export class ManualComponent { name = 'Component'; updateName(newName: string) {   this.name = newName; } // ... } 

Si bien todo esto no parece particularmente impresionante, pero lo más interesante está por delante.

A continuación, editaremos la función de plantilla para que, en lugar de texto inmutable, muestre el contenido de la propiedad de 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 -     } }, 

Es posible que haya notado que incluimos las instrucciones de la plantilla en if que verifican los valores de rf . Angular utiliza este parámetro para indicar si el componente se está creando por primera vez (se establecerá el bit menos significativo), o simplemente necesitamos actualizar el contenido dinámico en el proceso de detección de cambios (esto es a lo if se dirige la segunda if ).

Entonces, cuando el componente se muestra por primera vez, creamos todos los elementos, y luego, cuando se detectan cambios, solo actualizamos lo que podría cambiar. El método interno ɵt es responsable de esto (observe la letra minúscula t ), que corresponde a la función textBinding exportada por Angular:


Función textBinding

Entonces, el primer parámetro es el índice del elemento a actualizar, el segundo es el valor. En este caso, creamos un elemento de texto vacío con el índice 2 con el comando core.ɵT(2); . Actúa como un marcador de posición para el name . Lo actualizamos con el comando core.ɵt(2, ctx.name); al detectar un cambio en la variable correspondiente.

Por el momento, la salida de este componente seguirá mostrando el texto Hello, Component , aunque podemos cambiar el valor de la propiedad de name , lo que provocará un cambio en el texto en la pantalla.

Para que la aplicación se vuelva verdaderamente interactiva, agregaremos aquí un campo de entrada de datos con un detector de eventos que llama al 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(); } // ... }, 

El enlace de eventos se realiza en la línea core.ɵL('input', $event => ctx.updateName($event.target.value)); . A saber, el método ɵL responsable de configurar el detector de eventos para los elementos declarados más recientes. El primer argumento es el nombre del evento (en este caso, la input es el evento que se genera cuando el contenido del elemento <input> cambia), el segundo argumento es una devolución de llamada. Esta devolución de llamada acepta datos de eventos como argumento. Luego extraemos el valor actual del elemento objetivo del evento, es decir, del elemento <input> , y lo pasamos a la función en el componente.

El código anterior es equivalente a escribir el siguiente HTML en una plantilla:

 Your name: <input (input)="updateName($event.target.value)" /> 

Ahora puede editar el contenido del elemento <input> y observar cómo cambia el texto en el componente. Sin embargo, el campo de entrada no se completa cuando se carga el componente. Para que todo funcione de esta manera, debe agregar una instrucción más al código de función de la plantilla, que se ejecuta cuando se detecta un cambio:

 template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { ... } if (rf & 2) {   core.ɵt(2, ctx.name);   core.ɵp(4, 'value', ctx.name); } } 

Aquí usamos otro método incorporado del sistema de representación, ɵp , que actualiza la propiedad de un elemento con un índice dado. En este caso, el índice 4 se pasa al método, que es el índice que se asigna al elemento de input , y le ctx.name método que coloque el valor ctx.name en la propiedad value de este elemento.

Ahora nuestro ejemplo está finalmente listo. Implementamos, desde cero, el enlace de datos bidireccional utilizando la API del sistema de renderizado Ivy. Esto es simplemente genial.
Aquí puedes experimentar con el código terminado.

Ahora estamos familiarizados con la mayoría de los componentes básicos del nuevo compilador Ivy. Sabemos cómo crear elementos y nodos de texto, cómo vincular propiedades y configurar escuchas de eventos, y cómo usar el sistema de detección de cambios.

Acerca de los bloques * ngIf y * ngFor


Antes de terminar el estudio de Ivy, veamos otro tema interesante. A saber, hablemos sobre cómo funciona el compilador con subpatrones. Estos son los patrones que se usan para los *ngIf o *ngFor . Se procesan de manera especial. Veamos cómo usar *ngIf en nuestro código de plantilla casero.

Primero debe instalar el paquete npm @angular/common : aquí es donde *ngIf . A continuación, debe importar la directiva desde este paquete:

 import { NgIf } from '@angular/common'; 

Ahora, para poder usar NgIf en la plantilla, debe proporcionarle algunos metadatos, ya que el módulo @angular/common no se compiló con Ivy (al menos al escribir el material, y en el futuro esto probablemente cambiará de introducción de ngcc ).

Vamos a utilizar el método ɵdefineDirective , que está relacionado con el método familiar ɵdefineComponent . Define metadatos para directivas:

 (NgIf as any).ngDirectiveDef = core.ɵdefineDirective({ type: NgIf, selectors: [['', 'ngIf', '']], factory: () => new NgIf(core.ɵinjectViewContainerRef(), core.ɵinjectTemplateRef()), inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'} }); 

Encontré esta definición en el código fuente angular , junto con la ngFor . Ahora que hemos preparado NgIf para usar en Ivy, podemos agregar lo siguiente a la lista de directivas para el componente:

 static ngComponentDef = core.ɵdefineComponent({ directives: [NgIf], // ... }); 

A continuación, definimos el subpatrón solo para la partición delimitada por *ngIf .

Suponga que necesita mostrar una imagen. Vamos a establecer una nueva función para esta plantilla dentro de la función de plantilla:

 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(); } } 

Esta función de plantilla no es diferente de la que ya escribimos. Utiliza las mismas construcciones para crear un elemento img dentro de un elemento div .

Y finalmente, podemos poner todo junto agregando la directiva ngIf a la plantilla del 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) {   // ... } }, 

Tenga en cuenta la llamada al nuevo método al comienzo del código ( core.ɵC(5, ifTemplate, null, ['ngIf']); ). Declara un nuevo elemento contenedor, es decir, un elemento que tiene una plantilla. El primer argumento es el índice del elemento, ya hemos visto dichos índices. El segundo argumento es la función del subpatrón que acabamos de definir. Se utilizará como plantilla para el elemento contenedor. El tercer parámetro es el nombre de la etiqueta para el elemento, que no tiene sentido aquí, y finalmente, hay una lista de directivas y atributos asociados con este elemento. Aquí es donde entra ngIf .

En la línea core.ɵp(5, 'ngIf', (ctx.name === 'Igor')); el estado del elemento se actualiza vinculando el atributo ngIf al valor de la expresión lógica ctx.name === 'Igor' . Esto verifica si la propiedad de name del componente es igual a Igor .

El código anterior es equivalente al siguiente código HTML:

 <div *ngIf="name === 'Igor'"> <img align="center" src="..."> </div> 

Aquí se puede observar que el nuevo compilador no produce el código más compacto, pero no es tan malo en comparación con lo que es ahora.

Puedes experimentar con un nuevo ejemplo aquí . Para ver la sección NgIf en acción, ingrese el nombre Igor en el campo Your name .

Resumen


Prácticamente viajamos por las capacidades del compilador Ivy. Esperemos que este viaje haya despertado su interés en una mayor exploración de Angular. Si es así, ahora tienes todo lo que necesitas para experimentar con Ivy. Ahora sabe cómo "traducir" plantillas a JavaScript, cómo acceder a los mismos mecanismos angulares que utiliza Ivy sin utilizar este compilador. Supongo que todo esto te dará la oportunidad de explorar los nuevos mecanismos angulares tan profundo como quieras.

Aquí , aquí y aquí : tres materiales en los que puede encontrar información útil sobre Ivy. Y aquí está el código fuente de Render3.

Estimados lectores! ¿Cómo te sientes acerca de las nuevas características de Ivy?

Source: https://habr.com/ru/post/es419995/


All Articles