[Перевод] Angular 9 и Ivy: ленивая загрузка компонентов
Ленивая загрузка компонентов в Angular? Может, речь идёт о ленивой загрузке модулей с помощью маршрутизатора Angular? Нет, мы говорим именно о компонентах. Текущая версия Angular поддерживает лишь ленивую загрузку модулей. Но Ivy даёт разработчику новые возможности в работе с компонентами.
Ленивая загрузка, которой мы пользовались до сих пор: маршруты
Ленивая загрузка — это замечательный механизм. В Angular пользоваться этим механизмом можно, не прилагая к этому практически никаких усилий, путём объявления маршрутов, поддерживающих ленивую загрузку. Вот пример из документации Angular, демонстрирующий это:
const routes: Routes = [
{ path: 'customer-list',
loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule) }
];
Благодаря вышеприведённому коду будет создан отдельный фрагмент для customers.module
, который будет загружен тогда, когда пользователь перейдёт по маршруту customer-list
.
Это — очень приятный способ уменьшения размера главного бандла проекта и ускорения первоначальной загрузки приложения.
Но, несмотря на это, разве не здорово было бы получить возможность более точного управления ленивой загрузкой? Например, что если бы мы могли организовать ленивую загрузку отдельных компонентов?
До сих пор сделать этого было нельзя. Но всё изменилось с приходом Ivy.
Ivy и концепция «локальности»
Модули — это базовая концепция и основной строительный блок каждого Angular-приложения. В модулях объявляются компоненты, директивы, пайпы, сервисы.
Современное Angular-приложение не может существовать без модулей. Одной из причин этого является тот факт, что ViewEngine добавляет все необходимые метаданные к модулям.
Ivy, с другой стороны, использует другой подход. В Ivy компонент может существовать без модуля. Это возможно благодаря концепции локальности (Locality). Её суть заключается в том, что все метаданные являются локальными для компонента.
Поясним это, проанализировав es2015-бандл, сгенерированный с использованием Ivy.
Es2015-бандл, сгенерированный с использованием Ivy
В разделе Component code
(Код компонента) можно видеть, что система Ivy сохранила код компонента. Тут нет ничего особенного. Но затем Ivy добавляет к коду некоторые метаданные.
Первый фрагмент метаданных представлен на рисунке как Component factory
(Фабрика компонента). Фабрика знает о том, как создавать экземпляры компонента. В разделе Component metadata
(Метаданные компонента) Ivy размещает дополнительные атрибуты, наподобие type
и selectors
, то есть — всё, что нужно компоненту во время выполнения программы.
Достойно отдельного упоминания то, что Ivy добавляет сюда и функцию template
. Она показана в разделе Compiled version of your template
(Скомпилированная версия шаблона). Остановимся на этом интересном факте подробнее.
Функция template
— это скомпилированная версия нашего HTML-кода. Она выполняет инструкции Ivy для создания DOM. Это отличается от того, как работает ViewEngine.
Система ViewEngine берёт код и обходит его. Затем Angular выполнит код в том случае, если мы им воспользуемся.
А при подходе, используемом Ivy, всем заправляет компонент, вызывающий команды Angular. Это изменение позволяет компонентам существовать самостоятельно, это приводит к тому, что к базовому коду Angular можно применять алгоритм tree-shake.
Реальный пример ленивой загрузки компонента
Теперь, когда мы знаем о том, что ленивая загрузка компонентов возможна, рассмотрим её на реальном примере. А именно — создадим приложение Quiz, которое задаёт пользователю вопросы с вариантами ответов.
Приложение выводит изображение города и варианты, из которых нужно выбрать название этого города. Как только пользователь выберет вариант, нажав на соответствующую кнопку, эта кнопка немедленно изменится, указывая на то, правильным или неправильным был ответ. Если фон кнопки станет зелёным — значит ответ был правильным. Если фон станет красным — значит ответ был неправильным.
После того, как получен ответ на текущий вопрос, программа показывает следующий вопрос. Вот как это выглядит.
Демонстрация работы приложения Quiz
Вопросы, которые программа задаёт пользователю, представлены компонентом QuizCardComponent
.
Концепция ленивой загрузки компонента
Давайте сначала проиллюстрируем общую идею ленивой загрузки компонента QuizCardComponent
.
Процесс работы с компонентом QuizCardComponent
После того, как пользователь запускает викторину, нажав на кнопку Start quiz
, мы начинаем ленивую загрузку компонента. После того, как компонент оказывается в нашем распоряжении, мы помещаем его в контейнер.
Мы реагируем на события questionAnsvered
«ленивого» компонента так же, как реагировали бы на события обычного компонента. А именно, после возникновения события мы выводим на экране очередную карточку с вопросом.
Анализ кода
Для того чтобы разобраться в том, что происходит в ходе ленивой загрузки компонента, начнём с упрощённой версии QuizCardComponent
, которая выводит свойства вопроса.
Затем мы расширим этот компонент, использовав в нём компоненты Angular Material. А в итоге — настроим реакцию на события, генерируемые компонентом.
Давайте организуем ленивую загрузку упрощённой версии компонента QuizCardComponent
, которая имеет следующий шаблон:
Here's the question
- Image: {{ question.image }}
- Possible selections: {{ question.possibleSelections.toString() }}
- Correct answer: {{ question.correctAnswer }}
Обратите внимание на то, что в Angular 9 свойство Сущность В наши дни для обеспечения обратной совместимости нужно пользоваться Фабрика Метод В будущем всё это, вероятно, можно будет сделать с помощью метода Angular Это — всё, что нужно для организации ленивой загрузки компонента. После того, как будет нажата кнопка Исправим это, добавив в него дополнительные возможности и компоненты Angular Material: Их, конечно, можно добавить к Но в этот раз мы воспользуемся ими немного не так, как раньше. Мы добавим маленький модуль в тот же файл, в котором находится Эта спецификация модуля принадлежит исключительно нашему компоненту, загружаемому в ленивом режиме. В результате единственным компонентом, который объявляется в модуле, будет Мы не экспортируем новый модуль для того, чтобы модули, загружаемые в «жадном» режиме, не смогли бы его импортировать. Перезапустим приложение и посмотрим на то, как оно ведёт себя при нажатии на Замечательно! Компонент Проанализируем бандл приложения с помощь инструмента Размер основного бандла приложения — примерно 260 Кб. Если бы мы загрузили вместе с ним и компонент Код Даже хотя Сейчас мы организовали ленивую загрузку Кнопка, в зависимости от того, правильный или неправильный ответ она содержит, становится при нажатии зелёной или красной. А что ещё происходит? Ничего. А нам надо, чтобы после ответа на один вопрос, выводилась бы карточка ещё одного вопроса. Исправим это. Так как компонент уже загружен — система не выполняет дополнительного HTTP-запроса. Мы пользуемся содержимым фрагмента кода, который уже у нас есть, создаём новый компонент и помещаем его в контейнер. Это — важнейший хук Для вызова хука Замечательно! Мы загрузили компонент со сторонними модулями, отреагировали на генерируемые им события и наладили вызов нужных хуков жизненного цикла компонента. Вот исходный код готового проекта, над котором мы тут работали. К сожалению, при использовании в компоненте сторонних модулей, нам нужно заботиться ещё и о модулях. Однако стоит помнить о том, что в будущем это может измениться. Движок Ivy ввёл концепцию локальности, благодаря которой компоненты могут существовать самостоятельно. Это изменение является основой будущего Angular. Уважаемые читатели! Планируете ли вы использовать методику ленивой загрузки компонентов в своих Angular-проектах?
Первый шаг работы заключается в создании элемента-контейнера. Для этого мы можем либо воспользоваться реальным элементом, вроде ng-container
, что позволит обойтись без дополнительного уровня HTML-кода. Вот как выглядит объявление элемента-контейнера, в который мы поместим «ленивый» компонент:
В компоненте нужно получить доступ к контейнеру. Для этого мы используем аннотацию @ViewChild
и сообщим ей, что хотим прочесть ViewContainerRef
.static
в аннотации @ViewChild
по умолчанию установлено в false
: @ViewChild('quizContainer', {read: ViewContainerRef}) quizContainer: ViewContainerRef;
Теперь у нас есть контейнер, в который мы хотим добавить «ленивый» компонент. Далее, нам нужны ComponentFactoryResolver
и Injector
. И тем и другим можно обзавестись, прибегнув к методике внедрения зависимостей.ComponentFactoryResolver
— это простой реестр, устанавливающий соотношения компонентов и автоматически генерируемых классов ComponentFactory
, которые могут быть использованы для создания экземпляров компонентов: constructor(private quizservice: QuizService,
private cfr: ComponentFactoryResolver,
private injector: Injector) {
}
Теперь в нашем распоряжении имеется всё, что нужно для достижения цели. Поработаем над содержимым метода startQuiz
и организуем ленивую загрузку компонента: 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();
Мы можем воспользоваться ECMAScript-командой import
для организации ленивой загрузки QuizCardComponent
. Выражение импорта возвращает промис. Работать с ним можно, либо воспользовавшись конструкцией async/await
, либо — с помощью обработчика .then
. После разрешения промиса мы, для создания экземпляра компонента, применяем деструктурирование.ComponentFactory
. В будущем соответствующая строка не потребуется, так как мы сможем работать с компонентом напрямую.ComponentFactory
даёт нам componentRef
. Мы передаём componentRef
и Injector
методу createComponent
контейнера.createComponent
возвращает ComponentRef
, где содержится экземпляр компонента. Мы используем instance
для передачи компоненту @Input
-свойств.renderComponent
. Это метод всё ещё носит статус экспериментального. Однако весьма вероятно то, что он превратится в обычный метод Angular. Вот полезные материалы на эту тему.
Ленивая загрузка компонентаStart quiz
, начинается ленивая загрузка компонента. Если открыть вкладку Network
инструментов разработчика, можно увидеть процесс ленивой загрузки фрагмента кода, представленного файлом quiz-card-quiz-card-component.js
. После загрузки и обработки компонент выводится на экран, пользователь видит карточку вопроса.Расширение компонента
Сейчас мы загружаем компонент QuizCardComponent
. Это очень хорошо. Но наше приложение пока не особенно функционально.
Мы включили в компонент несколько красивых компонентов Material. А как насчёт соответствующих модулей? AppModule
. Но это означает, что эти модули будут загружаться в «жадном» режиме. А это — не лучшая идея. Более того, сборка проекта завершится неудачно, с выдачей следующего сообщения: 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.
Что делать? Как вы уже, наверное, поняли, данная проблема вполне решаема. Сделать это можно с помощью модулей.QuizCardComponent
(в файл quizcard.component.ts
): @NgModule({
declarations: [QuizCardComponent],
imports: [CommonModule, MatCardModule, MatButtonModule]
})
class QuizCardModule {
}
Обратите внимание на то, что тут нет выражения export
.QuizCardComponent
. В разделе import
производится импорт лишь тех модулей, которые нужны нашему компоненту.кнопку Start quiz
.
Анализ доработанного приложенияQuizCardComponent
загружается в ленивом режиме и добавляется в ViewContainer
. Вместе с ним подгружаются и все необходимые зависимости.webpack-bundle-analyze
, представленного соответствующим npm-модулем. Он позволяет визуализировать содержимое файлов, которые выдаёт Webpack, и исследовать полученную схему.
Анализ бандла приложенияQuizCardComponent
, то размер загружаемых данных составил бы уже примерно 270 Кб. Получается, что нам удалось уменьшить размер основного бандла на 10 Кб, загрузив в ленивом режиме лишь один компонент. Неплохой результат! QuizCardComponent
после сборки попадает в отдельный файл. Если проанализировать содержимое этого файла, то окажется, что там находится не только код QuizCardComponent
, но и модули Material, использованные в этом компоненте.QuizCardComponent
использует MatButtonModule
и MatCardModule
, в готовый файл с кодом попадает только MatCardModule
. Причина этого заключается в том, что мы используем MatButtonModule
и в AppModule
, выводя кнопку Start quiz
. В результате соответствующий код попадает в другой фрагмент бандла.QuizCardComponent
. Этот компонент выводит карточку, оформленную в стиле Material, содержащую картинку, вопрос и кнопки с вариантами ответа. Что сейчас происходит в том случае, если нажать на одну из таких кнопок? Обработка событий
Приложение не показывает карточку нового вопроса при нажатии на кнопку ответа из-за того, что мы пока не настроили механизмы реагирования на события компонентов, загружаемых в ленивом режиме. Мы уже знаем о том, что компонент QuizCardComponent
генерирует события, используя EventEmitter
. Если посмотреть на определение класса EventEmitter
, можно узнать о том, что он является наследником Subject
: export declare class EventEmitter
Это значит, что у EventEmitter
есть метод subscribe
, который и позволяет настроить реакцию системы на возникающие события: instance.questionAnswered.pipe(
takeUntil(instance.destroy$)
).subscribe(() => this.showNewQuestion());
Тут мы подписываемся на поток questionAnswered
и вызываем метод showNextQuestion
, выполняющий логику lazyLoadQuizCard
: async showNewQuestion() {
this.lazyLoadQuizCard();
}
Конструкция takeUntil(instance.destroy$)
необходима для очистки подписки, выполняемой после уничтожения компонента. Если вызывается хук ngOnDestroy
жизненного цикла компонента QuizCardComponent
, то вызывается destroy$
с next
и complete
.Хуки жизненного цикла компонента
Почти все хуки жизненного цикла компонента автоматически вызываются при работе с компонентом QuizCardComponent
с использованием методики ленивой загрузки. Но одного хука не хватает. Можете понять — какого?
Хуки жизненного цикла компонентаngOnChanges
. Так как мы самостоятельно обновляем входные свойства экземпляра компонента, мы ответственны и за вызов хука жизненного цикла ngOnChanges
.ngOnChanges
экземпляра компонента нужно самостоятельно сконструировать объект SimpleChange
: (instance as any).ngOnChanges({
question: new SimpleChange(null, instance.question, true)
});
Мы вручную вызываем ngOnChanges
экземпляра компонента и передаём ему объект SimpleChange
. Этот объект указывает на то, что данное изменение является первым, что предыдущее значение равняется null
, и что текущее значение — это вопрос.Итоги
Ленивая загрузка компонентов даёт Angular-разработчику замечательные возможности по оптимизации производительности приложений. В его распоряжении оказываются инструменты для очень точной настройки состава материалов, загружаемых в ленивом режиме. Раньше, когда можно было загружать в ленивом режиме лишь маршруты, такой точности у нас не было.