Angular 9 e Ivy: carga de componentes diferidos

¿Carga de componentes perezosa en angular? ¿Tal vez estamos hablando de módulos de carga diferida utilizando el enrutador angular? No, estamos hablando de los componentes. La versión actual de Angular solo admite la carga lenta de módulos. Pero Ivy le da al desarrollador nuevas oportunidades para trabajar con componentes.



La carga perezosa que usamos hasta ahora: rutas


La carga diferida es un gran mecanismo. En Angular, puede usar este mecanismo sin tener que hacer casi ningún esfuerzo declarando rutas que admitan la carga diferida. Aquí hay un ejemplo de la documentación angular que demuestra esto:

const routes: Routes = [     { path: 'customer-list',       loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule) }     ]; 

Gracias al código anterior, se creará un fragmento separado para customers.module , que se cargará cuando el usuario navegue por la ruta de la customer-list .

Esta es una forma muy agradable de reducir el tamaño del paquete principal del proyecto y acelerar la carga inicial de la aplicación.

Pero a pesar de esto, ¿no sería bueno poder controlar con mayor precisión la carga diferida? Por ejemplo, ¿qué pasaría si pudiéramos organizar la carga diferida de componentes individuales?

Hasta ahora, esto no ha sido posible. Pero todo eso cambió con la llegada de Ivy.

Ivy y el concepto de "localidad"


Los módulos son el concepto básico y el componente básico de cada aplicación angular. Los módulos declaran componentes, directivas, tuberías, servicios.

Una aplicación angular moderna no puede existir sin módulos. Una razón para esto es el hecho de que ViewEngine agrega todos los metadatos necesarios a los módulos.

Ivy, por otro lado, adopta un enfoque diferente. En Ivy, un componente puede existir sin un módulo. Esto es posible gracias al concepto de localidad. Su esencia es que todos los metadatos son locales para el componente.

Expliquemos esto analizando el paquete es2015 generado con Ivy.


Paquete Es2015 generado con Ivy

En la sección Component code , puede ver que el sistema Ivy ha guardado el código de componente. No hay nada especial aquí. Pero luego Ivy agrega algunos metadatos al código.

La primera pieza de metadatos se muestra en la figura como una Component factory . La fábrica sabe cómo instanciar un componente. En la sección de Component metadata del Component metadata , Ivy coloca atributos adicionales, como type y selectors , es decir, todo lo que el componente necesita durante la ejecución del programa.

Vale la pena mencionar que Ivy agrega la función de template aquí. Se muestra en la Compiled version of your template sección Compiled version of your template . Detengámonos en este hecho interesante con más detalle.

La función de template es una versión compilada de nuestro código HTML. Ella sigue las instrucciones de Ivy para crear un DOM. Esto es diferente de cómo funciona ViewEngine.

El sistema ViewEngine toma el código y lo omite. Angular ejecutará el código si lo usamos.

Y con el enfoque utilizado por Ivy, el componente que invoca los comandos angulares se encarga de todo. Este cambio permite que los componentes existan independientemente, esto lleva al hecho de que el algoritmo de sacudida de árbol se puede aplicar al código base angular.

Un ejemplo real de carga de componentes diferidos


Ahora que sabemos que la carga de componentes diferidos es posible, considérelo con un ejemplo real. Es decir, crearemos una aplicación de prueba que le haga preguntas al usuario con opciones de respuesta.

La aplicación muestra una imagen de la ciudad y las opciones entre las que debe elegir el nombre de esta ciudad. Tan pronto como el usuario seleccione una opción haciendo clic en el botón correspondiente, este botón cambiará inmediatamente, indicando si la respuesta fue correcta o incorrecta. Si el fondo del botón se vuelve verde, la respuesta fue correcta. Si el fondo se vuelve rojo, significa que la respuesta fue incorrecta.

Después de recibir la respuesta a la pregunta actual, el programa muestra la siguiente pregunta. Así es como se ve.


Prueba de demostración

Las preguntas que el programa hace al usuario están representadas por el componente QuizCardComponent .

Concepto de carga de componentes perezosos


Primero QuizCardComponent la idea general de la carga QuizCardComponent componente QuizCardComponent .


El proceso de componentes de QuizCardComponent

Después de que el usuario comienza la prueba haciendo clic en el botón Start quiz , comenzamos la carga diferida del componente. Una vez que el componente está a nuestra disposición, lo colocamos en un contenedor.

Reaccionamos a la questionAnsvered eventos detectados de un componente "vago" de la misma manera que responderíamos a los eventos de un componente regular. Es decir, después de la ocurrencia del evento, mostramos en la pantalla la siguiente tarjeta con la pregunta.

Análisis de código


Para comprender lo que sucede durante la carga diferida de un componente, comenzamos con una versión simplificada de QuizCardComponent , que muestra las propiedades de la pregunta.

Luego expandiremos este componente usando componentes de material angular en él. Y al final, ajustaremos la reacción a los eventos generados por el componente.

Organicemos la carga diferida de una versión simplificada del componente QuizCardComponent , que tiene la siguiente plantilla:

 <h1>Here's the question</h1> <ul>    <li><b>Image: </b> {{ question.image }}</li>    <li><b>Possible selections: </b> {{ question.possibleSelections.toString() }}</li>    <li><b>Correct answer: </b> {{ question.correctAnswer }}</li> </ul> 

El primer paso es crear un elemento contenedor. Para hacer esto, podemos usar un elemento real, como <div> , o - recurrir a ng-container , que nos permite prescindir de un nivel adicional de código HTML. Así es como se ve la declaración del elemento contenedor en el que ponemos el componente "vago":

 <mat-toolbar color="primary">  <span>City quiz</span> </mat-toolbar> <button *ngIf="!quizStarted"        mat-raised-button color="primary"        class="start-quiz-button"        (click)="startQuiz()">Start quiz</button> <ng-container #quizContainer class="quiz-card-container"></ng-container> 

En el componente necesita acceder al contenedor. Para hacer esto, usamos la anotación @ViewChild y nos @ViewChild que queremos leer ViewContainerRef .

Tenga en cuenta que en Angular 9, la propiedad static en la anotación @ViewChild se establece en false de forma predeterminada:

 @ViewChild('quizContainer', {read: ViewContainerRef}) quizContainer: ViewContainerRef; 

Ahora tenemos un contenedor en el que queremos agregar el componente "perezoso". A continuación, necesitamos ComponentFactoryResolver e Injector . Ambos pueden adquirirse recurriendo a la metodología de inyección de dependencia.

La entidad ComponentFactoryResolver es un registro simple que establece la relación entre los componentes y las clases ComponentFactory generadas automáticamente que se pueden usar para crear instancias de componentes:

 constructor(private quizservice: QuizService,            private cfr: ComponentFactoryResolver,            private injector: Injector) { } 

Ahora tenemos todo lo que se necesita para lograr el objetivo. startQuiz en el contenido del método startQuiz y organicemos la carga diferida del componente:

 const {QuizCardComponent} = await import('./quiz-card/quiz-card.component'); const quizCardFactory = this.cfr.resolveComponentFactory(QuizCardComponent); const {instance} = this.quizContainer.createComponent(quizCardFactory, null, this.injector); instance.question = this.quizservice.getNextQuestion(); 

Podemos usar el comando import ECMAScript para organizar la carga diferida de QuizCardComponent . Una expresión de importación devuelve una promesa. Puede trabajar con él utilizando la construcción async/await .then o utilizando el controlador .then . Después de resolver la promesa, usamos la desestructuración para crear una instancia del componente.

En estos días, debe usar ComponentFactory para proporcionar compatibilidad con versiones anteriores. En el futuro, no se requiere la línea correspondiente, ya que podremos trabajar directamente con el componente.

La fábrica ComponentFactory nos da componentRef . Pasamos componentRef e Injector al método createComponent del contenedor.

El método createComponent devuelve un ComponentRef donde está contenida la instancia del componente. Usamos instance para pasar las @Input del componente @Input .

En el futuro, todo esto probablemente se puede hacer utilizando el método de renderComponent angular renderComponent . Este método aún es experimental. Sin embargo, es muy probable que se convierta en un método angular regular. Aquí hay materiales útiles sobre este tema.

Esto es todo lo que se necesita para organizar la carga diferida de un componente.


Carga diferida de componentes

Después de presionar el botón Start quiz , comienza la carga diferida del componente. Si abre la pestaña Network de las herramientas del desarrollador, puede ver el proceso de cargar perezosamente el fragmento de código representado por el archivo quiz-card-quiz-card-component.js . Después de cargar y procesar el componente que se muestra, el usuario ve una tarjeta de preguntas.

Extensión de componente


Actualmente estamos cargando el componente QuizCardComponent . Esto es muy bueno Pero nuestra aplicación aún no es particularmente funcional.

Arreglemos esto agregando características y componentes adicionales de material angular:

 <mat-card class="quiz-card">  <mat-card-header>    <div mat-card-avatar class="quiz-header-image"></div>    <mat-card-title>Which city is this?</mat-card-title>    <mat-card-subtitle>Click on the correct answer below</mat-card-subtitle>  </mat-card-header>  <img class="image" mat-card-image [src]="'assets/' + question.image" alt="Photo of a Shiba Inu">  <mat-card-actions class="answer-section">    <button [disabled]="answeredCorrectly !== undefined" *ngFor="let selection of question.possibleSelections"            mat-stroked-button color="primary"            [ngClass]="{              'correct': answeredCorrectly && selection === question.correctAnswer,              'wrong': answeredCorrectly === false && selection === question.correctAnswer             }"            (click)="answer(selection)">      {{selection}}    </button>  </mat-card-actions> </mat-card> 

Hemos incluido algunos hermosos componentes materiales en el componente. ¿Qué pasa con los módulos correspondientes?

Por supuesto, se pueden agregar al AppModule . Pero esto significa que estos módulos se cargarán en modo "codicioso". Y esta no es una buena idea. Además, el montaje del proyecto fallará, con el siguiente mensaje:

 ERROR in src/app/quiz-card/quiz-card.component.html:9:1 - error TS-998001: 'mat-card' is not a known element: 1. If 'mat-card' is an Angular component, then verify that it is part of this module. 2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 

Que hacer Como probablemente ya haya entendido, este problema es completamente solucionable. Esto se puede hacer usando módulos.

Pero esta vez los usaremos un poco diferente que antes. QuizCardComponent un pequeño módulo al mismo archivo que QuizCardComponent (en el archivo quizcard.component.ts ):

 @NgModule({  declarations: [QuizCardComponent],  imports: [CommonModule, MatCardModule, MatButtonModule] }) class QuizCardModule { } 

Tenga en cuenta que no hay declaración de export .

La especificación de este módulo pertenece exclusivamente a nuestro componente, cargado en modo diferido. Como resultado, el único componente que se declara en el módulo será QuizCardComponent . La sección de importación solo importa los módulos que nuestro componente necesita.

No exportamos el nuevo módulo para que los módulos cargados en modo codicioso no puedan importarlo.

Reinicie la aplicación y observe cómo se comporta cuando hace clic en el Start quiz .


Análisis de aplicación modificado

Genial El componente QuizCardComponent carga en modo QuizCardComponent y se agrega al ViewContainer . Todas las dependencias necesarias se cargan con él.

Analizaremos el paquete de la aplicación utilizando la webpack-bundle-analyze proporcionada por el módulo npm correspondiente. Le permite visualizar el contenido de los archivos que produce Webpack y examinar el esquema resultante.


Análisis de paquetes de aplicaciones

El tamaño del paquete principal de la aplicación es de aproximadamente 260 Kb. Si descargamos el componente QuizCardComponent junto con él, el tamaño de los datos descargados sería de aproximadamente 270 Kb. Resulta que pudimos reducir el tamaño del paquete principal en 10 Kb, cargando solo un componente en modo perezoso. Buen resultado!

El código de QuizCardComponent después del ensamblado llega a un archivo separado. Si analiza el contenido de este archivo, resulta que no solo QuizCardComponent código QuizCardComponent , sino también los módulos de Material utilizados en este componente.

Aunque QuizCardComponent usa MatButtonModule y MatCardModule , solo MatCardModule al MatCardModule código terminado. La razón de esto es que también usamos MatButtonModule en el AppModule , que muestra el botón Start quiz . Como resultado, el código correspondiente cae en otro fragmento del paquete.

Ahora hemos organizado la carga diferida de QuizCardComponent . Este componente muestra una tarjeta, diseñada al estilo de Material, que contiene una imagen, una pregunta y botones con opciones de respuesta. ¿Qué sucede ahora si hace clic en uno de estos botones?

El botón, dependiendo de si contiene la respuesta correcta o incorrecta, se vuelve verde o rojo cuando se presiona. ¿Qué más está pasando? Nada Y necesitamos que después de responder una pregunta, se muestre una tarjeta de otra pregunta. Arreglarlo

Manejo de eventos


La aplicación no muestra una nueva tarjeta de preguntas cuando hace clic en el botón de respuesta debido al hecho de que aún no hemos configurado mecanismos para responder a eventos de componentes cargados en modo diferido. Ya sabemos que el componente QuizCardComponent genera eventos utilizando EventEmitter . Si observa la definición de la clase EventEmitter , puede descubrir que es un descendiente del Subject :

 export declare class EventEmitter<T extends any> extends Subject<T> 

Esto significa que EventEmitter tiene un método de subscribe , que le permite configurar la respuesta del sistema a los eventos que ocurren:

 instance.questionAnswered.pipe(    takeUntil(instance.destroy$) ).subscribe(() => this.showNewQuestion()); 

Aquí nos suscribimos a la secuencia questionAnswered y llamamos al método showNextQuestion , que ejecuta la lógica lazyLoadQuizCard :

 async showNewQuestion() {  this.lazyLoadQuizCard(); } 

La takeUntil(instance.destroy$) es necesaria para borrar la suscripción que se ejecuta después de que se destruye el componente. Si se ngOnDestroy gancho ngOnDestroy ciclo de vida del componente ngOnDestroy , destroy$ se llama con next y complete .

Como el componente ya está cargado, el sistema no realiza una solicitud HTTP adicional. Usamos el contenido del fragmento de código que ya tenemos, creamos un nuevo componente y lo colocamos en el contenedor.

Ganchos de ciclo de vida de componentes


Casi todos los ganchos del ciclo de vida del componente se QuizCardComponent automáticamente cuando se trabaja con el componente QuizCardComponent utilizando la técnica de carga diferida. Pero un gancho no es suficiente. ¿Puedes entender cuál?


Ganchos de ciclo de vida de componentes

Este es el ngOnChanges más importante de ngOnChanges . Como nosotros mismos actualizamos las propiedades de entrada de la instancia del componente, también somos responsables de llamar al ngOnChanges ciclo de vida ngOnChanges .

Para llamar al ngOnChanges de la instancia del ngOnChanges , debe construir un objeto SimpleChange usted mismo:

 (instance as any).ngOnChanges({    question: new SimpleChange(null, instance.question, true) }); 

Llamamos manualmente ngOnChanges instancia ngOnChanges componente y le pasamos un objeto SimpleChange . Este objeto indica que este cambio es el primero, que el valor anterior es null y que el valor actual es una pregunta.

Genial Cargamos el componente con módulos de terceros, respondimos a los eventos que generó y configuramos la llamada de los ganchos necesarios del ciclo de vida del componente.

Aquí está el código fuente del proyecto terminado en el que estábamos trabajando.

Resumen


La carga lenta de componentes ofrece a los desarrolladores de Angular grandes oportunidades para optimizar el rendimiento de la aplicación. A su disposición están las herramientas para ajustar muy bien la composición de los materiales cargados en modo perezoso. Anteriormente, cuando era posible cargar solo rutas en modo perezoso, no teníamos tanta precisión.

Desafortunadamente, cuando usamos módulos de terceros en el componente, también debemos cuidar los módulos. Sin embargo, vale la pena recordar que en el futuro esto puede cambiar.

El motor Ivy introdujo el concepto de localidad, gracias al cual los componentes pueden existir independientemente. Este cambio es la base del futuro de Angular.

Estimados lectores! ¿Planea utilizar la técnica de carga de componentes diferidos en sus proyectos angulares?

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


All Articles