¿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 IvyEn 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ónLas 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 QuizCardComponentDespué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 componentesDespué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 modificadoGenial 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 aplicacionesEl 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 componentesEste 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?
