DEV Community

Cover image for Como funciona el compilador de Angular

Como funciona el compilador de Angular

Artículo original de Angular Blog por Alex Rickabaugh en inglés aquí:

El Compilador de Angular (al cual llámanos ngc) es la herramienta utilizada para compilar aplicaciones y librerías de Angular. ngc se basa en el compilador TypeScript (llamado tsc) y amplía el proceso de compilación de código TypeScript para agregar generación de código adicional relacionada con capacidades de Angular.

El compilador de angular sirve como un puente entre la experiencia del desarrollador y el rendimiento del tiempo de ejecución, los usuarios de Angular crean aplicaciones con una API amigable al usuario y basada en decoradores, y ngc traduce este código en instrucciones de tiempo de ejecución más eficientes.

Por ejemplo, un componente básico de Angular podría verse de esta manera:

Después de la compilación a través de ngc, este componente en su lugar se ve de esta manera:

El decorador del @Component se remplazó con varias propiedades estáticas (ɵfac y ɵcmp), que describen este componente en el tiempo de ejecución de Angular e implementan la representación y la detección de cambios para su plantilla.

De esta forma, ngc puede considerarse un compilador de TypeScript extendido que también sabe cómo "ejecutar" decoradores Angular, aplicando sus efectos a las clases decoradas en tiempo de compilación (a diferencia del tiempo de ejecución).

Dentro de ngc

ngc tiene varios objetivos importantes:

  • Compilar los decoradores Angular, incluidos los componentes y sus plantillas.

  • Aplicar las reglas de verificación de tipos de TypeScript a las plantillas de componentes.

  • Volver a compilar rápidamente cuando el desarrollador realice cambios.

Examinemos cómo ngc gestiona cada uno de estos objetivos.

Flujo de compilación

El objetivo principal de ngc es compilar código TypeScript mientras transforma las clases decoradas de Angular reconocidas en representaciones más eficientes para el tiempo de ejecución. El flujo principal de la compilación Angular procede de la siguiente manera:

  1. Crear una instancia del compilador TypeScript, con alguna funcionalidad Angular adicional.

  2. Escanear cada archivo en el proyecto en busca de clases decoradas y generar un modelo de qué componentes, directivas, pipes, NgModules, etc. deben compilarse.

  3. Hacer conexiones entre clases decoradas (por ejemplo, qué directivas se usan en que plantillas de componentes).

  4. Aprovechar TypeScript para verificar expresiones en plantillas de componentes.

  5. Compilar todo el programa, incluyendo la generación de código Angular adicional para cada clase decorada.

Paso 1: Creación del programa TypeScript

En el compilador de TypeScript, un programa que se va a compilar está representado por una instancia de ts.Program. Esta instancia combina el conjunto de archivos que se compilarán, escribe la información de las dependencias y el conjunto particular de opciones del compilador que se utilizará.

Identificar el conjunto de archivos y dependencias no es sencillo. A menudo, el usuario especifica un archivo de "punto de entrada" (por ejemplo, main.ts), y TypeScript debe mirar las importaciones en ese archivo para descubrir otros archivos que necesitan compilarse. Esos archivos tienen importaciones adicionales, que se expanden a más archivos, y así sucesivamente. Algunas de estas importaciones apuntan a dependencias: referencias a código que no se está compilando, pero que se usa de alguna manera y debe ser conocido por el sistema de tipos de TypeScript. Estas importaciones de dependencia son para archivos .d.ts, generalmente en node_modules 

En este punto, el compilador de Angular hace algo especial: agrega archivos de entrada adicionales al ts.Program. Para cada archivo escrito por el usuario (por ejemplo, my.component.ts), ngc agrega un archivo "sombra" con un sufijo .ngtypecheck (por ejemplo, my.component.ngtypecheck.ts). Estos archivos se utilizan internamente para verificar el tipo de plantilla (más sobre esto más adelante).

Según las opciones del compilador, ngcpuede agregar otros archivos al ts.Program, como archivos .ngfactory para compatibilidad con la arquitectura anterior de View Engine.

Paso 2: Análisis Individual

En la fase de análisis de la compilación, ngc busca clases con decoradores Angular e intenta comprender estáticamente cada decorador. Por ejemplo, si encuentra una clase decorada @Component , mira al decorador e intenta determinar la plantilla del componente, su selector, ver la configuración de encapsulación y cualquier otra información sobre el componente que pueda ser necesaria para generar código para él. Esto requiere que el compilador sea capaz de realizar una operación conocida como evaluación parcial: leer expresiones dentro de los metadatos del decorador e intentar interpretar esas expresiones sin ejecutarlas realmente.

Evaluación parcial

A veces, la información en un decorador angular se oculta detrás de una expresión. Por ejemplo, un selector para un componente se proporciona como una cadena literal, pero también podría ser una constante:

ngc usa las API de TypeScript para navegar por el código para evaluar la expresión MY_SELECTOR, rastrearla hasta su declaración y finalmente resolverla en la cadena 'my-cmp'. El evaluador parcial puede entender constantes simples; objetos y arreglos literales; accesos a las propiedades; importaciones/exportaciones; aritmética y otras operaciones binarias; e incluso evaluar llamadas a funciones simples. Esta función brinda a los desarrolladores de Angular más flexibilidad en la forma en que describen los componentes y otros tipos de Angular al compilador.

Salida del análisis

Al final de la fase de análisis, el compilador ya tiene una buena idea de qué componentes, directivas, pipes, inyectables y NgModules se encuentran en el programa de entrada. Para cada uno de estos, el compilador construye un objeto de "metadatos" que describe todo lo que aprendió de los decoradores de la clase. En este punto, los componentes tienen sus plantillas y hojas de estilo cargadas desde el disco (si es necesario), y es posible que el compilador ya haya producido errores (conocidos en TypeScript como "diagnósticos") si se detectan errores semánticos en cualquier parte de la entrada hasta el momento.

Paso 3: Análisis Global

Antes de que pueda verificar el tipo o generar código, el compilador necesita comprender cómo se relacionan entre sí los diversos tipos decorados en el programa. El objetivo principal de este paso es comprender la estructura NgModule del programa.

NgModules

Para comprobar el tipo y generar código, el compilador necesita saber qué directivas, componentes y conductos se usan en la plantilla de cada componente. Esto no es sencillo porque los componentes de Angular no importan directamente sus dependencias. En cambio, los componentes de Angular describen plantillas utilizando HTML, y las dependencias potenciales se comparan con elementos en esas plantillas utilizando selectores de estilo CSS. Esto habilita una poderosa capa de abstracción: los componentes Angular no necesitan saber exactamente cómo están estructuradas sus dependencias. En cambio, cada componente tiene un conjunto de dependencias potenciales (su "ámbito de compilación de plantilla"), solo un subconjunto del cual terminará haciendo coincidir los elementos de su plantilla.

Esta indirección se resuelve a través de la abstracción Angular @NgModule. NgModules se puede considerar como unidades componibles del ámbito de la plantilla. Un NgModule básico puede verse así:

Se puede entender que NgModules declara cada uno dos ámbitos diferentes:

  • Un "ámbito de compilación", que representa el conjunto de dependencias potenciales que están disponibles para cualquier componente declarado en el propio NgModule.

  • Un "ámbito de exportación", que representa un conjunto de dependencias potenciales que están disponibles en el ámbito de compilación de cualquier NgModules que importe el NgModule determinado.

En el ejemplo anterior, ImageViewerComponent es un componente declarado en este NgModule, por lo que sus dependencias potenciales vienen dadas por el ámbito de compilación de NgModule. Este alcance de compilación es la unión de todas las declaraciones y los alcances de exportación de cualquier NgModules que se importe. Debido a esto, es un error en Angular declarar un componente en múltiples NgModules. Además, un componente y su NgModule deben compilarse al mismo tiempo.

En este caso, se importa CommonModule, por lo que el ámbito de compilación de ImageViewerModule (y, por lo tanto, ImageViewerComponent) incluye todas las directivas y canalizaciones exportadas por CommonModule- NgIf, NgForOf, AsyncPipe, y media docena más. El alcance de la compilación también incluye ambas directivas declaradas - ImageViewerComponent e ImageResizeDirective.

Tenga en cuenta que para los componentes, su relación con el NgModule que los declara es bidireccional: el NgModule define el ámbito de la plantilla del componente y hace que ese componente esté disponible en los ámbitos de la plantilla de otros componentes.

El NgModule anterior también declara un "alcance de exportación" que consiste solo en ImageViewerComponent. Otros NgModules que importan este tendrán ImageViewerComponent agregado a sus ámbitos de compilación. De esta manera, NgModule permite la encapsulación de los detalles de implementación de ImageViewerComponent -  internamente, podría usar ImageResizeDirective, pero esta directiva no está disponible para los consumidores de ImageViewerComponent.

Para determinar estos ámbitos, el compilador crea un gráfico de NgModules, sus declaraciones y sus importaciones y exportaciones, utilizando la información que aprendió sobre cada clase individualmente en el paso anterior. También requiere conocimientos sobre dependencias: componentes y NgModules importados de bibliotecas y no declarados en el programa actual. Angular codifica esta información en los archivos .d.ts de esas dependencias.

metadata .d.ts

Por ejemplo, el ImageViewerModule anterior importa CommonModule del paquete @angular/common. La evaluación parcial de la lista de importaciones resolverá las clases nombradas en las declaraciones de importaciones dentro de los archivos .d.ts de esas dependencias.

El simple hecho de conocer el símbolo de NgModules importados no es suficiente. Para construir su gráfico, el compilador pasa información sobre las declaraciones, importaciones y exportaciones de NgModules a través de los archivos .d.ts en un tipo de metadatos especial. Por ejemplo, en el archivo de declaración generado para CommonModule de Angular, estos metadatos (simplificados) se ven así:

Esta declaración de tipo no está destinada a la verificación de tipos por parte de TypeScript, sino que incorpora información (referencias y otros metadatos) sobre la comprensión de Angular de la clase en cuestión en el sistema de tipos. A partir de estos tipos especiales, ngc puede determinar el ámbito de exportación de CommonModule. Usando las API de TypeScript para resolver las referencias dentro de estos metadatos a esas definiciones de clase, puede extraer metadatos útiles con respecto a las directivas.

Esto le da a ngc suficiente información sobre la estructura del programa para continuar con la compilación.

Paso 4: Comprobación de tipo de plantilla

ngc es capaz de informar errores de tipo dentro de las plantillas de Angular. Por ejemplo, si una plantilla intenta vincular un valor {{name.first}} pero el objeto de nombre no tiene una propiedad first, ngc puede mostrar este problema como un error de tipo. Realizar esta verificación de manera eficiente es un desafío importante para ngc.

TypeScript por sí mismo no comprende la sintaxis de la plantilla Angular y no puede verificar el tipo directamente. Para realizar esta verificación, el compilador de Angular convierte las plantillas de Angular en código TypeScript (conocido como "Bloque de verificación de tipo" o TCB) que expresa operaciones equivalentes en el nivel de tipo y alimenta este código a TypeScript para la verificación semántica. Cualquier diagnóstico generado se mapea y se informa al usuario en el contexto de la plantilla original.

Por ejemplo, considere un componente con una plantilla que usa ngFor:

Para esta plantilla, el compilador desea verificar que el acceso a la propiedad user.name sea legal. Para hacer esto, primero debe comprender cómo se deriva el tipo de user de la variable de bucle a través de NgFor a partir de la matriz de entrada de users.

El bloque de verificación de tipos que genera el compilador para la plantilla de este componente tiene el siguiente aspecto:

La complejidad aquí parece ser alta, pero fundamentalmente este TCB está realizando una secuencia específica de operaciones:

  • Primero deduce el tipo real de la directiva NgForOf (que es genérica) a partir de sus enlaces de entrada. Esto se llama _t1.

  • Valida que la propiedad de usuarios del componente se pueda asignar a la entrada NgForOf, a través de la instrucción de asignación _t1.ngForOf = ctx.users.

  • A continuación, declara un tipo para el contexto de vista incrustado de la plantilla de fila *ngFor, denominada _t2, con un tipo inicial de cualquier valor.

  • Usando un if con una llamada de guardia de tipo, usa la función auxiliar ngTemplateContextGuard de NgForOf para restringir el tipo de _t2 según cómo funciona NgForOf.

  • La variable de bucle implícita (usuario en la plantilla) se extrae de este contexto y se le asigna el nombre _t3.

  • Finalmente, se expresa el acceso _t3.name.

Si el acceso _t3.name no es legal según las reglas de TypeScript, TypeScript generará un error de diagnóstico para este código. El verificador de tipos de plantilla de Angular puede ver la ubicación de este error en el TCB y usar los comentarios incrustados para mapear el error a la plantilla original antes de mostrárselo al desarrollador.

Dado que las plantillas de Angular contienen referencias a las propiedades de las clases de componentes, tienen tipos del programa del usuario. Por lo tanto, el código de verificación de tipo de plantilla no se puede verificar de forma independiente y debe verificarse dentro del contexto de todo el programa del usuario (en el ejemplo anterior, el tipo de componente se importa del archivo test.ts del usuario). ngc logra esto agregando los TCB generados al programa del usuario a través de un paso de compilación incremental de TypeScript (generando un nuevo ts.Program). Para evitar la hiperpaginación de la caché de compilación incremental, se agrega código de verificación de tipo a archivos .ngtypecheck.ts separados que el compilador agrega al ts.Program en la creación en lugar de directamente a los archivos de usuario.

Paso 5: Emitir

Cuando comienza este paso, ngc ha entendido el programa y validado que no hay errores fatales. Luego se le dice al compilador de TypeScript que genere código JavaScript para el programa. Durante el proceso de generación, los decoradores de Angular se eliminan y, en su lugar, se agregan varios campos estáticos a las clases, con el código Angular generado listo para escribirse en JavaScript.

Si el programa que se compila es una biblioteca, también se generan archivos .d.ts. Los archivos contienen metadatos angulares incrustados que describen cómo una compilación futura puede usar esos tipos como dependencias.

Ser rápido incrementalmente

Si lo anterior suena como mucho trabajo antes de generar el código, es porque lo es. Si bien la lógica de TypeScript y Angular es eficiente, aún puede llevar varios segundos realizar todo el análisis, análisis y síntesis necesarios para producir una salida de JavaScript para el programa de entrada. Por esta razón, tanto TypeScript como Angular admiten un modo de compilación incremental, donde el trabajo realizado anteriormente se reutiliza para actualizar de manera más eficiente un programa compilado cuando se realiza un pequeño cambio en la entrada.
El principal problema de la compilación incremental es: dado un cambio específico en un archivo de entrada, el compilador necesita determinar qué salidas pueden haber cambiado y qué salidas son seguras para reutilizar. El compilador debe ser perfecto y errar por volver a compilar una salida si no puede estar seguro de que no ha cambiado.
Para resolver este problema, el compilador de Angular tiene dos herramientas principales: el gráfico de importación y el gráfico de dependencia semántica.

Importar gráfico

Como el compilador realiza operaciones de evaluación parcial mientras analiza el programa por primera vez, crea un gráfico de importaciones críticas entre archivos. Esto permite que el compilador comprenda las dependencias entre archivos cuando algo cambia.

Por ejemplo, si el archivo my.component.ts tiene un componente y el selector de ese componente está definido por una constante importada de selector.ts, el gráfico de importación muestra que my.component.ts depende de selector.ts. Si selector.ts cambia, el compilador puede consultar este gráfico y saber que los resultados del análisis de my.component.ts ya no son correctos y deben rehacerse.

El gráfico de importación es importante para comprender lo que podría cambiar, pero tiene dos problemas principales:

  • Es demasiado sensible a los cambios no relacionados. Si se cambia selector.ts, pero ese cambio solo agrega un comentario, entonces my.component.ts realmente no necesita volver a compilarse.

  • No todas las dependencias en las aplicaciones de Angular se expresan a través de importaciones. Si el selector de MyCmp cambia, otros componentes que usan MyCmp en su plantilla pueden verse afectados, aunque nunca importen MyCmp directamente.

Ambos problemas se abordan a través de la segunda herramienta incremental del compilador:

Gráfico de dependencia semántica 

El gráfico de dependencia semántica comienza donde termina el gráfico de importación. Este gráfico captura la semántica real de la compilación: cómo los componentes y las directivas se relacionan entre sí. Su trabajo es saber qué cambios semánticos requerirían que se reprodujera una salida determinada.

Por ejemplo, si se cambia selector.ts, pero el selector de MyCmp no cambia, entonces el gráfico de profundidad semántica sabrá que nada que afecte semánticamente a MyCmp ha cambiado, y la salida anterior de MyCmp se puede reutilizar. Por el contrario, si el selector cambia, entonces el conjunto de componentes/directivas utilizadas en otros componentes puede cambiar, y el gráfico semántico sabrá que esos componentes necesitan volver a compilarse.

Incrementalidad

Por lo tanto, ambos gráficos funcionan juntos para proporcionar una compilación incremental rápida. El gráfico de importación se utiliza para determinar qué análisis se debe volver a realizar y, a continuación, se aplica el gráfico semántico para comprender cómo los cambios en los datos del análisis se propagan a través del programa y requieren que se vuelvan a compilar los resultados. El resultado es un compilador que puede reaccionar eficientemente a los cambios en las entradas y solo hacer la cantidad mínima de trabajo para actualizar correctamente sus salidas en respuesta.

Resumen

El compilador de Angular aprovecha la flexibilidad de las API del compilador de TypeScript para ofrecer una compilación correcta y eficiente de plantillas y clases de Angular. La compilación de aplicaciones Angular nos permite ofrecer una experiencia de desarrollador deseable en el IDE, brindar comentarios en el tiempo de compilación sobre problemas en el código y transformar ese código durante el proceso de compilación en el JavaScript más eficiente para ejecutar en el navegador.

Discussion (0)