Приветствую, друзья! Сегодня мы обсудим одну из самых популярных техник оптимизации производительности веб-приложений - бесконечную прокрутку (или "ленивую загрузку") в Blazor. Никогда не слышали об этом методе? Не беда, мы сейчас все разложим по полочкам!
В чем суть бесконечной прокрутки? Это метод загрузки данных по мере их необходимости, а не сразу все сразу. Ведь зачем загружать все данные, если пользователь всего лишь хочет просмотреть первые несколько элементов? Таким образом, мы уменьшаем нагрузку на сервер и увеличиваем производительность нашего приложения. Кроме того, это также позволяет нам значительно сократить время загрузки страницы. Звучит интересно, не правда ли?
Так что же нам нужно, чтобы реализовать эту технику в Blazor? Для этого мы можем использовать подход, описанный в посте. Автор этого поста предлагает нам создать компонент Blazor, который будет загружать данные по мере необходимости при прокрутке страницы. Он использует для этого Intersection Observer API и некоторые другие технологии.
Автор оригинальной статьи предлагает создать компонент следующего вида:
<InfiniteScrolling ItemsProvider="GetItems">
<ItemTemplate Context="item">
<p>Item @item</p>
</ItemTemplate>
<LoadingTemplate>
<div>Loading...</div>
</LoadingTemplate>
</InfiniteScrolling>
@code {
async Task<IEnumerable<int>> GetItems(InfiniteScrollingItemsProviderRequest request)
{
await Task.Delay(1000); // Simulate async loading
return Enumerable.Range(request.StartIndex, 50);
}
}
У компонента Virtualize, описанного в документации Microsoft, и компонента InfiniteScrolling, описанного в блоге Meziantou, есть кое что общее. Они оба решают проблему динамической загрузки элементов данных при скроллинге страницы, и используют параметр ItemsProvider.
ItemsProvider - это делегат, который принимает на вход экземпляр класса с информацией о запрашиваемых элементах и возвращает асинхронную операцию, возвращающую коллекцию элементов. В Virtualize этот делегат имеет сигнатуру Func<ItemsProviderRequest, Task<ItemsProviderResult>>, где ItemsProviderRequest содержит информацию о запрашиваемых элементах (индекс первого и последнего элемента), а ItemsProviderResult содержит полученные элементы и информацию о том, является ли это последняя порция запрошенных элементов.
В InfiniteScrolling, ItemsProvider имеет сигнатуру ItemsProviderRequestDelegate, которая принимает на вход экземпляр класса InfiniteScrollingItemsProviderRequest, содержащего информацию о запрашиваемых элементах, и возвращает асинхронную операцию, возвращающую коллекцию элементов.
Реализация
В статье говорится, что идея реализации заключается в том, чтобы иметь невидимый элемент после элемента, который отображается на экране. Когда этот элемент входит в область просмотра, это означает, что необходимо загрузить новые элементы. Для определения того, когда элемент входит в область просмотра, используется API IntersectionObserver.
IntersectionObserver – это API, чтобы отслеживать перемещение элемента внутри окна браузера. Оно позволяет проверять, видит ли пользователь элемент на экране, а также отслеживать, когда элемент появляется или исчезает из видимости.
Начать автор статьи предлагает с создания файла infinite-scrolling.js в директории wwwroot следующего содержания:
export function initialize(lastItemIndicator, componentInstance) {
const options = {
root: findClosestScrollContainer(lastItemIndicator),
rootMargin: '0px',
threshold: 0,
};
const observer = new IntersectionObserver(async (entries) => {
// When the lastItemIndicator element is visible => invoke the C# method `LoadMoreItems`
for (const entry of entries) {
if (entry.isIntersecting) {
observer.unobserve(lastIndicator);
await componentInstance.invokeMethodAsync("LoadMoreItems");
}
}
}, options);
observer.observe(lastItemIndicator);
// Allow to cleanup resources when the Razor component is removed from the page
return {
dispose: () => dispose(observer),
onNewItems: () => {
observer.unobserve(lastIndicator);
observer.observe(lastIndicator);
},
};
}
// Cleanup resources
function dispose(observer) {
observer.disconnect();
}
// Find the parent element with a vertical scrollbar
// This container should be use as the root for the IntersectionObserver
function findClosestScrollContainer(element) {
while (element) {
const style = getComputedStyle(element);
if (style.overflowY !== 'visible') {
return element;
}
element = element.parentElement;
}
return null;
}
Давайте разберем его немного подробнее:
Функция initialize создает IntersectionObserver, который будет наблюдать за переданным элементом lastItemIndicator. Когда элемент становится видимым (пересекает область видимости), вызывается метод LoadMoreItems у компонента componentInstance (например, C# метод в Razor компоненте). Функция также возвращает объект с двумя методами:
- dispose: который отключает IntersectionObserver и очищает ресурсы.
- onNewItems: который обновляет наблюдение IntersectionObserver для новых элементов, которые были добавлены в DOM.
Функция findClosestScrollContainer возвращает ближайший родительский элемент, у которого есть вертикальная прокрутка, чтобы использовать его в качестве корня для IntersectionObserver. Она начинает с переданного элемента и проверяет каждого родителя до тех пор, пока не найдет тот, у которого свойство CSS overflowY не равно visible, т.е. у которого есть вертикальная прокрутка. Если такого элемента нет, то функция возвращает null.
На Следующем шаге автор реализует класс InfiniteScrollingItemsProviderRequest :
public sealed class InfiniteScrollingItemsProviderRequest
{
public InfiniteScrollingItemsProviderRequest(int startIndex, CancellationToken cancellationToken)
{
StartIndex = startIndex;
CancellationToken = cancellationToken;
}
public int StartIndex { get; }
public CancellationToken CancellationToken { get; }
}
public delegate Task<IEnumerable<int>> ItemsProviderRequestDelegate(InfiniteScrollingItemsProviderRequest request);
Класс InfiniteScrollingItemsProviderRequest представляет объект запроса на получение элементов для бесконечной прокрутки. Он имеет два свойства:
- StartIndex - индекс первого элемента, который необходимо получить. Это используется для пагинации или отображения бесконечного списка элементов.
- CancellationToken - объект CancellationToken, который может быть использован для отмены операции получения элементов.
Класс используется в делегате ItemsProviderRequestDelegate, который принимает в качестве параметра объект InfiniteScrollingItemsProviderRequest и возвращает Task<IEnumerable<int>>. Этот делегат представляет метод для получения элементов, которые будут отображаться в списке при прокрутке страницы.
И наконец код компонента Blazor:
@using System.Threading
@typeparam T
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable
@foreach (var item in _items)
{
@ItemTemplate(item)
}
@if (_loading)
{
@LoadingTemplate
}
@if (!_enumerationCompleted)
{
<div @ref="_lastItemIndicator" style="height:1px;flex-shrink:0"></div>
}
@code {
private List<T> _items = new();
private ElementReference _lastItemIndicator;
private DotNetObjectReference<InfiniteScrolling<T>> _currentComponentReference;
private IJSObjectReference _module;
private IJSObjectReference _instance;
private bool _loading = false;
private bool _enumerationCompleted = false;
private CancellationTokenSource _loadItemsCts;
[Parameter]
public ItemsProviderRequestDelegate<T> ItemsProvider { get; set; }
[Parameter]
public RenderFragment<T> ItemTemplate { get; set; }
[Parameter]
public RenderFragment LoadingTemplate { get; set; }
[JSInvokable]
public async Task LoadMoreItems()
{
if (_loading)
return;
_loading = true;
try
{
_loadItemsCts ??= new CancellationTokenSource();
StateHasChanged(); // Allow the UI to display the loading indicator
try
{
var newItems = await ItemsProvider(new InfiniteScrollingItemsProviderRequest(_items.Count, _loadItemsCts.Token));
var previousCount = items.Count;
items.AddRange(newItems);
if (items.Count == previousCount)
{
_enumerationCompleted = true;
}
else
{
await _instance.InvokeVoidAsync("onNewItems");
}
}
catch (OperationCanceledException oce) when (oce.CancellationToken == _loadItemsCts.Token)
{
// No-op; we canceled the operation, so it's fine to suppress this exception.
}
}
finally
{
_loading = false;
}
StateHasChanged(); // Display the new items and hide the loading indicator
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Initialize the IntersectionObserver
if (firstRender)
{
_module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./infinite-scrolling.js");
_currentComponentReference = DotNetObjectReference.Create(this);
_instance = await _module.InvokeAsync<IJSObjectReference>("initialize", _lastItemIndicator, _currentComponentReference);
}
}
public async ValueTask DisposeAsync()
{
// Cancel the current load items operation
if (_loadItemsCts != null)
{
_loadItemsCts.Dispose();
_loadItemsCts = null;
}
// Stop the IntersectionObserver
if (_instance != null)
{
await _instance.InvokeVoidAsync("dispose");
await _instance.DisposeAsync();
_instance = null;
}
if (_module != null)
{
await _module.DisposeAsync();
}
_currentComponentReference?.Dispose();
}
}
Он содержит поля и методы для инициализации и управления IntersectionObserver, который отслеживает прокрутку страницы и вызывает метод LoadMoreItems(), когда элемент с индикатором последнего элемента становится видимым.
В этом методе происходит загрузка дополнительных элементов, используя делегат ItemsProviderRequestDelegate, который получает новый объект InfiniteScrollingItemsProviderRequest. Этот объект содержит стартовый индекс и токен отмены для загрузки элементов с сервера.
Когда новые элементы загружены, они добавляются к существующему списку элементов _items. Если новые элементы не были добавлены, значит список элементов закончился и больше не будет загружаться. Если же элементы добавлены, вызывается метод onNewItems() для обновления IntersectionObserver.
При отображении элементов в компоненте Blazor используется RenderFragment, который может быть переопределен пользователем через параметры ItemTemplate и LoadingTemplate.
Компонент также содержит методы для инициализации IntersectionObserver и его очистки при удалении компонента с страницы. Он также реализует интерфейс IAsyncDisposable, чтобы можно было корректно освободить ресурсы при удалении компонента.
Заключение
В общем, это было неплохое погружение в мир бесконечной прокрутки в Blazor. Надеюсь, теперь у тебя хватит знаний и уверенности, чтобы самостоятельно реализовать это в своем проекте. А пока что я бы хотел выразить огромную благодарность ChatGPT за помощь в написании этого поста. Без тебя, я бы точно не справился со всеми этими терминами и техническими деталями.
Кстати, если интересен C# и dotnet, то у меня еще есть заметка про то, как парсить eml-ки и статья про непотребства со строками.