Привет Хабр. На связи Даня, Angular-разработчик из команды Т-Бизнеса.
У меня для вас статья по работе с динамическими компонентами. Мы подробно рассмотрим процесс создания этих компонентов, будут детальные примеры кода и пошаговое руководство всего процесса создания. Я добавил комментарии и описания, чтобы прояснить важные моменты и улучшить понимание концепций.
Добро пожаловать под кат!
Для чего оно нужно
Фреймворк Angular преподносит немало трудностей во время работы с ним. Большое влияние на это оказывает то, что из коробки Angular доступно много инструментов для разработки и можно вообще ничего не подключая извне делать сложные и навороченные проекты.
Второй фактор, производный от первого, — из-за того, что фреймворк содержит множество сущностей, он становится тяжеловесным и лучшим его применением является разработка сложных систем с большим количеством логики и страниц.
В таких больших системах остро встает вопрос оптимизации работы приложения, так как даже несущественные упущения в этих моментах способны привести к серьезным задержкам на масштабе всего проекта.
Один из вариантов оптимизации — сокращение объема изначально загружаемого кода, а также сокращение количества сущностей при изначальном рендере приложения. В этом нам помогут динамические компоненты. Суть оптимизации заключается в том, что компоненты создаются в уже скомпилированном и отрендеренном приложении, не перегружая браузер во время выполнения стартовых сложных задач.
У меня есть статья, в которой подробно описаны шаги создания динамических компонентов и дальнейшее взаимодействие с ними:
Предлагаю на базе этих знаний поговорить о более приближенных к реальной жизни кейсах, где нас сильно выручат динамические компоненты.
Создаем первый динамический компонент
За примерами далеко ходить не нужно, возьмем базовый интернет-магазин или онлайн-витрину, где большую часть экрана занимают однотипные блоки с карточками товаров.
Карточки товаров одинаковые по структуре и являются одним и тем же компонентом.
Прекрасным решением будет вывести в отдельный процесс рендер этих элементов и, помимо этого, асинхронно подгружать сам код компонента, который отвечает за карточку товара.
Процесс по созданию динамического компонента: нам нужен компонент, который мы собираемся создавать.
Представим, что у нас в приложении есть такая сущность, как финансовые транзакции, которые описывают, сколько и зачем денег было отправлено или получено.
У каждой транзакции есть своя категория, которая описывает суть транзакции. Категорий может быть много, их можно создавать и редактировать. Именно такой компонент, который содержит информацию об определенной категории, мы и будем создавать.
Вот файловая структура нашего проекта
Код компонента category.component.ts:
category.component.ts @Component({ selector: 'app-category', templateUrl: './category.component.html', styleUrls: ['./category.component.scss'], standalone: true, }) export class CategoryComponent { @Input() categoryName!: string; id!: number; author!: string; updatedDate!: string; }
Начнем с простого варианта, где есть несколько полей, которые через интерполяцию отображаются в шаблоне.
Основой для создания компонента является класс ViewContainerRef, который содержит множество полезных методов, в том числе интересующий нас createComponent.
Можно инжектировать его в компонент, внутри которого будут создаваться динамические компоненты, создать шаблонную переменную и использовать ее как контейнер. Но лучше вынести это в отдельную директиву:
dynamic-components-loader.directive.ts @Directive({ selector: '[dynamicComponentLoader]', }) export class DynamicComponentsLoaderDirective { viewContainerRef = inject(ViewContainerRef); }
Созданную директиву используем с элементом, внутри которого мы хотим создавать компоненты.
categories.component.html:
<div class="dynamic"> <ng-template dynamicComponentLoader></ng-template> </div>
А внутри самого компонента контейнера через декоратор @ViewChild получаем доступ к нашей директиве.
categories.component.ts @ViewChild(DynamicComponentsLoaderDirective, { static: true }) dynamicComponentContainer!: DynamicComponentsLoaderDirective;
Итак, базовые вещи сделаны, теперь давайте создавать сами компоненты. Код функции для создания компонента:
categories.component.ts async loadSingleCategory() { const vcr = this.dynamicComponentContainer.viewContainerRef; vcr.clear(); const { CategoryComponent } = await import( '../../shared/components/category/category.component' ); const categoryComp: ComponentRef<CategoryDynamicComponent> = vcr.createComponent<CategoryDynamicComponent>(CategoryComponent); }
Получаем доступ к классу ViewContainerRef, который инжектирован в нашу директиву, и записываем его в переменную vcr, далее будем называть ее контейнером:
const vcr = this.dynamicComponentContainer.viewContainerRef;
Чистим все содержимое контейнера при помощи метода clear, ведь автоматически он этого не сделает и при каждом вызове функции к уже существующим будет добавляться новый динамический компонент:
vcr.clear();
После того как мы подготовили контейнер, необходимо загрузить сам компонент, который будем создавать. Сделаем это через функцию import, она асинхронная, поэтому объявим функцию loadSingleCategory через async:
const { CategoryComponent } = await import( '../../shared/components/category/category.component' );
Теперь можно создавать компонент. Вызываем у контейнера метод createComponent, куда параметром передаем наш компонент:
const categoryComp: ComponentRef<CategoryDynamicComponent> = vcr.createComponent<CategoryDynamicComponent>(CategoryComponent);
Взаимодействие с созданным компонентом
У нас есть динамически созданный компонент, который уже отображается на странице и с которым мы можем взаимодействовать.
При создании компонента мы можем получить доступ к его полям и проинициализировать их с помощью объекта categoryData:
const categoryData = { categoryName: 'New Name', id: 1, author: 'Daniel', updatedDate: '11.08.2001', isRedact: false, }; categories.component.ts async loadSingleCategory() { const vcr = this.dynamicComponentContainer.viewContainerRef; vcr.clear(); const { CategoryComponent } = await import( '../../shared/components/category/category.component' ); const categoryComp: ComponentRef<CategoryDynamicComponent> = vcr.createComponent<CategoryDynamicComponent>(CategoryComponent); // Обращаемся к полям categoryComp.instance.categoryName = categoryData.categoryName; categoryComp.instance.id = categoryData.id; categoryComp.instance.isRedact = false; categoryComp.instance.updatedDate = categoryData.updatedDate; categoryComp.instance.author = categoryData.author; }
После инициализации компонента так изменить его данные уже не получится. К примеру, создадим отдельную функцию, которая будет менять название категории, и будем вызывать ее при клике на кнопку:
categories.component.ts changeName() { this.categoryComp.instance.categoryName = 'custom'; }
Для отслеживания процесса добавим хук ngOnChanges в динамический компонент, ведь именно он вызывается при изменении @Input свойств. А еще добавим в хук OnInit вывод в консоль, чтобы знать, когда создается компонент:
category.component.ts ngOnInit(): void { console.log('OnInit'); } ngOnChanges(changes: SimpleChanges) { console.log(changes, 'OnChanges'); }
Если поменять поля компонента извне, то изменения никак не отобразятся, так как не будет запущен цикл обнаружения изменений. Наши изменения будут проигнорированы и отобразятся только на следующем шаге обнаружения изменений.
Есть уже встроенный метод для работы с Input-свойствами таких компонентов, он называется setInput. Метод setInput принимает в себя два параметра: название свойства и его значение.
Доработаем код функции changeName:
categories.component.ts changeName() { this.categoryComp.setInput('categoryName', 'custom'); }
Видим, что в консоли выводится сообщение от onInit, помимо него срабатывает onChanges — и мы получаем доступ к объекту changes. Это может понадобиться для выполнения дополнительной логики, связанной с изменениями Input-значений, например повторной инициализации формы, где это значение используется.
Что касается обработки исходящих событий из компонента, то тут выбора не так много, поскольку встроенных механизмов для этого нет и придется пользоваться базовым eventEmitter.
Добавим компоненту категории возможность редактировать его название и сделаем этот процесс в духе глупых компонентов. То есть при редактировании и нажатии кнопки «Сохранить» компонент категории будет отправлять данные в родительский компонент, который в свою очередь будет общаться с сервером. Добавим Output-свойство компоненту категории:
category.component.ts @Output() redactCategory: EventEmitter<any> = new EventEmitter<any>(); И саму функцию для эмита значений category.component.ts sendRedactedCategoryData() { this.redactCategory.emit({ newTitle: this.categoryFormControl.value, id: this.id, }); }
А в родительском компоненте внутри функции loadSingleCategory добавим логику подписки на исходящие из компонента категории события
categories.component.ts:
this.categoryComp.instance.redactCategory.subscribe( (redactedCategoryData) => { // логика по обработке данных тут } );
Теперь у нас есть рабочий код, который может создавать динамический компонент, а также полноценно взаимодействовать с ним, передавая ему данные и получая обратно события.
Еще больше компонентов
Обычно однотипных компонентов у нас на странице много, и рендерить их по одному за раз — как-то не очень. Более того, с каждым из них нужно как-то взаимодействовать, передавать им данные и прослушивать события.
Предлагаемое решение заключается в том, чтобы пройтись по массиву с данными и для каждого элемента вызывать логику по созданию компонента и наполнению его данными.
После создания и наполнения компонента данными мы добавляем его в массив со всеми остальными динамическими компонентами. Подписываемся на события редактирования, которые эмитит компонент категории. А чтобы понимать, какой конкретно компонент пробросил событие, будем передавать его id и выполнять поиск по массиву динамических компонентов:
categories.component.ts async loadCategories(categoriesData: any[]) { const vcr = this.dynamicComponentContainer.viewContainerRef; vcr.clear(); const { CategoryComponent } = await import( '../../shared/components/category/category.component' ); // Проходимся по данным categoriesData.forEach((categoryData) => { // Создаем компонент и наполняем его данными const categoryComp: ComponentRef<CategoryDynamicComponent> = vcr.createComponent<CategoryDynamicComponent>(CategoryComponent); categoryComp.instance.categoryName = categoryData.title; categoryComp.instance.id = categoryData.id; categoryComp.instance.isRedact = false; categoryComp.instance.updatedDate = categoryData.updatedAt; categoryComp.instance.author = categoryData.user.email; // Добавляем новый компонент в массив с уже созданными this.categoryComponents.push(categoryComp); // Подписываемся на события и обрабатываем их categoryComp?.instance.redactCategory.subscribe( ({ newTitle, id }: RedactCategory) => { const redactedCategory = this.categoryComponents.find( (category) => category.instance.id === id ); if (newTitle !== redactedCategory?.instance.categoryName) { this.store.dispatch( CategoriesActions.redactCategory({ data: { newTitle, id } }) ); } redactedCategory!.instance.isRedact = false; } ); }); } }
Предлагаю перейти на StackBlitz и проверить, как это все работает.
Я слегка упростил структуру проекта, чтобы лишние детали не отвлекали вас от экспериментов.
Итоги
В результате на странице отображается много однотипных глупых компонентов, с которыми мы можем взаимодействовать. Весь процесс происходит в рантайме, а еще код создаваемого компонента подгружается отдельно и при необходимости, что позволяет сократить время стартовой загрузки страницы
Стоит сказать о том, что в Angular уже из коробки присутствует директива ngComponentOutlet, которая может решать задачу динамического рендеринга компонентов гораздо проще и быстрее. Однако, используя ngComponentOutlet, мы теряем возможность общаться с создаваемыми компонентами через @Input — и — @Output свойства. Выходом может стать использование общего сервиса, но это уже не слишком ложится в парадигму умных и глупых компонентов.
Если же столь гибкого взаимодействия с динамическими компонентами не требуется, рекомендую посмотреть в сторону библиотеки ng-polymorpheus: она решает те же самые задачи, но в более декларативном стиле. Вот ссылка на репозиторий:
Чем больше разработчики ориентируются на результат, тем большим количеством сложных концепций обрастает код. Я надеюсь, что после прочтения статьи одна из них стала более понятной и привычной для дальнейшего использования в ваших проектах. А если есть вопросы или желание поделиться своим опытом — добро пожаловать в комментарии!
ссылка на оригинал статьи https://habr.com/ru/articles/836036/
Добавить комментарий