
Всем привет! Меня зовут Павел Сапачёв, занимаюсь архитектурой и разработкой фронтенда в проекте «Тинькофф Лизинг». Мы любим создавать удобные, отзывчивые и производительные интерфейсы. Один из моментов улучшения — просмотр коллекций элементов. Самые популярные подходы к просмотру коллекций — постраничная разбивка и подгрузка при пролистывании страницы, которую называют бесконечными списками.
Если постраничная разбивка является классическим и понятным представлением навигации, то создание бесконечных списков содержит ряд подводных камней и технических сложностей.
В статье поделюсь реализацией списков на основе кастомные директивы, оборачивающие в себе обращения к API.
В недавнем релизе Angular 17 появились Deferrable Views, с помощью которых стало возможно добавить немного магии и удобства в свои компоненты.
Разработчики Angular при создании Deferrable Views заложили в их основу использование существующих браузерных API, таких как requestIdleCallback и Intersection Observer API, а еще обертки вокруг обработчиков событий click, keydown, mouseenter, focusin. Иначе говоря, появился синтаксический сахар, в значительной степени упрощающий работу с перечисленными функциями.
В документации Angular есть примеры использования Deferrable Views с разными триггерами, но в рамках этой статьи нас интересует только триггер on viewport, который срабатывает в момент, когда элемент появляется в зоне видимости благодаря использованию под капотом Intersection Observer API.
Реализация триггера загрузки
Реализация триггера загрузки — краеугольный камень в организации бесконечных списков. Применяя триггер on viewport, мы можем с помощью шаблона подсказать компоненту момент, когда нужно вызвать обращение к сервису данных для подгрузки следующей части.
Задачу можно решить разными способами: например, используя изображение, у которого будет прослушиваться событие load. Оно инициирует запуск вызова к сервису данных:
@defer (on viewport) { <img (load)=”loadMore()” src=”...” /> }
Недостаток использования изображения в том, что должно быть @defer (on viewport) { <app-load-trigger (init)="loadMore()" /> }
Код триггер-компонента примитивный: Очевидное преимущество этого способа — отсутствие каких-либо обращений по сети для решения задачи средствами самого Angular. Основная сложность по сути является первопричиной: как сделать так, чтобы компонент-триггер был в конце списка и срабатывал каждый раз, когда мы достигаем конца списка? Ответом будет использование структурной директивы Благодаря использованию В упрощенном виде без использования нового синтаксиса Control Flow код шаблона будет выглядеть так: Наличие Дополнительная проблема — определение момента, когда больше не требуется запрашивать данные. Иначе говоря: когда был достигнут конец списка? Частичным ответом будет предыдущий абзац: если массив элементов не обновился, то ничего перерисовывать не нужно и компонент-триггер переставлять тоже не нужно. В целях оптимизации хорошо бы иметь возможность заранее предусматривать необходимость установки компонента-триггера. Если не полагаться на ответы от сервиса данных, который может сообщить о том, что отдал последнюю часть коллекции элементов, можно самостоятельно проверять в том же Это можно сделать с помощью локальной переменной Суть метода простая: попытаться понять, является ли проверяемый элемент последним в списке, на основании того, кратен ли его индекс порции запрашиваемых элементов коллекции ( Я подготовил пример, чтобы показать вживую, как организовать ленивый бесконечный список: В примере намеренно добавлены задержки на дополнение коллекции для того, чтобы наглядно увидеть момент срабатывания загрузки и отображение сообщения о загрузке. С целью демонстрации список бесконечный и не содержит примеров остановки загрузки. Используя примеры кода, приведенные в статье, легко добавить эту логику в свое приложение. Организацию ленивого бесконечного списка через Deferrable Views нельзя назвать идеальной во всех отношениях, в первую очередь из-за несовместимости со старыми версиями Angular. Но, если можно использовать Angular 17, он будет неплохой альтернативой ранее существовавшим подходам, так как дает возможность навести чистоту в коде, избавившись от сложных обвязок для работы с Intersection Observer API или более примитивными методами. Отдельно стоит упомянуть совместимость подхода с использованием виртуального скролла, что открывает дополнительные возможности оптимизации работы с большими списками. В этой статье решил не рассматривать применение виртуализации именно с целью фокусировки на предлагаемом подходе, а не на описании кейсов применимости. В комментариях можем обсудить особенности подхода и возникшие вопросы.export class LoadTriggerComponent implements OnInit { @Output() readonly init = new EventEmitter(); ngOnInit(): void { this.init.emit(); } }Перестановка триггера в конец списка
NgFor, у которой есть ряд замечательных локальных переменных, в том числе boolean-переменная last. last можно описать такой шаблон, в котором представление всегда будет выводиться в конце списка и, что важно, перерисовываться каждый раз при обновлении списка, скрывая новое представление из области видимости.<app-list-item *ngFor="let item of list; last as isLast"> <ng-container *ngIf="isLast"> @defer (on viewport) { <app-load-trigger (init)="loadMore()" /> } @placeholder { <div></div> } </ng-container> </app-list-item>placeholder — обязательная часть синтаксиса @defer (on viewport) и содержит пустой блок, который для нашего случая бесполезен. Особенность реализации в том, что новый компонент-триггер не будет создан повторно, если список не был обновлен. Например, сервис данных не прислал других данных для дополнения коллекции.Остановка бесконечной загрузки
NgFor то, является ли элемент не только последним в списке, но и последним вообще.index и написать простой метод компонента, который будет помогать в определении:isLastInBunch(index): boolean { return (index + 1) % PAGE_SIZE === 0; }PAGE_SIZE). Такой подход не идеальный: может получиться так, что наш список содержит общее кратное количество элементов, но с некоторой долей вероятности решает проблему ненужного последнего запроса.Демо ленивого бесконечного списка
О подходе
ссылка на оригинал статьи https://habr.com/ru/articles/800201/
Добавить комментарий