天天看点

为Blazor添加一个全局加载指示器

到目前为止,.NET 5已经出来了,Blazor也迎来了重大更新,之后的学习就要基于最新的.NET 5了

这次主要基于Web Assembly实现一个全局的加载指示器,她主要应用于两种场景:

  1. 应用第一次加载
  2. HTTP请求

1. 应用加载

众所周知,Web Assembly体积比较大,因此第一次加载会比较耗时,默认的程序模板中,在index.html页面里面有这么一段:

其实她就是一个简化版的加载指示器,她的作用就是在程序加载完之前在页面上显示文字"Loading",虽然功能有了,但是还是寒酸了一点。

1.1 准备

网上有很多加载指示器的样式代码,挑选自己喜欢的即可,我找了Bootstrap 5里面的Spinner

https://v5.getbootstrap.com/docs/5.0/components/spinners/#growing-spinner

Bootstrap 5.0目前还是alpha阶段

1.2 修改index.html

先贴代码:

<head>
    <link href="xxxxx/bootstrap.min.css" rel="stylesheet"/>
    <style>
        #globalLoadingSpinnerBg, #globalLoadingSpinner {
            display:none;
        }
        .loading #globalLoadingSpinnerBg {
            display:block;
        }
        .loading #globalLoadingSpinner {
            display: flex;
        }
    </style>
</head>

<body class="loading">
    <div id="globalLoadingSpinnerBg" class="modal-backdrop fade show"></div>
    <div id="globalLoadingSpinner" class="modal fade show" role="dialog">
        <div class="text-center;" style="margin: auto;">
            <div class="spinner-grow text-primary" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-secondary" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-success" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-danger" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-warning" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-info" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-light" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
            <div class="spinner-grow text-dark" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
    </div>

    <div id="app"></div>
</body>
           

要点:

  1. 引入Bootstrap的样式表
  2. 添加指示器的HTML代码,注意是作为body里面的第一个元素,这样才能保证在加载Blazor程序集之前显示。这里把指示器放入了模态对话框里面,目的是想在加载的时候阻止用户的其他操作,比如频繁的重复点击按钮等。
  3. 为body添加样式名“loading", 通过head里面的样式定义可以看到,当为body添加“loading”样式的时候,显示指示器,否则隐藏指示器。

这样只要页面开始加载的时候就会显示加载指示器。

1.3 隐藏指示器

如何隐藏指示器?

其实只要移除body里面的loading样式名就可以了,我们可以借助Js的互操作,通过javascript函数来实现。

function hideGlobalLoadingSpinner() {
    document.body.classList.remove('loading');
}
           

何时隐藏?

回顾一下Blazor的页面周期, 我们需要在页面组件全部加载完以后隐藏指示器,那么可以选择的有OnAfterRender,OnAfterRenderAsync。

由于App组件是整个Blazor的根组件,因此我们可以修改她的OnAfterRenderAsync,调用上面的JS函数:hideGlobalLoadingSpinner。

protected async override Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    await jsRuntime.InvokeVoidAsync("hideGlobalLoadingSpinner");
}
           

2. HTTP请求

Web Assembly应用离不开HTTP请求,那么如何在请求开始前显示加载指示,请求结束后隐藏呢?

显然不可能每个HTTP请求都写代码进行这些操作,幸运的是.NET Core提供了IHttpClientFactory,可以帮助我们在请求的上下文中嵌入自己的处理程序。

2.1 封装Javascript调用

前面已经有了隐藏指示器的Javascript函数,我们还需要一个函数来显示指示器, 很简单,为body添加loading样式名就行了:

function showGlobalLoadingSpinner() {
    document.body.classList.add('loading');
}
           

为了隐藏Javascript的调用细节,并方便在组件内使用,有必要封装成c#类。

先定义一个接口,包含显示和隐藏两个方法:

public interface IGlobalLoadingSpinner
{
    Task ShowAsync();
    Task HideAsync();
}
           

接下来看实现:

public class DefaultGlobalLoadingSpinner : IGlobalLoadingSpinner
{
    static object Locker = new object();
    int SpinnerCount = 1;

    IJSRuntime _jsRuntime;

    public DefaultGlobalLoadingSpinner(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }

    public async Task ShowAsync()
    {
        lock (Locker)
        {
            SpinnerCount++;
        }

        await this._jsRuntime.InvokeVoidAsync("showGlobalLoadingSpinner");
    }

    public async Task HideAsync()
    {
        lock (Locker)
        {
            SpinnerCount--;
            if (SpinnerCount < 0)
            {
                SpinnerCount = 0;
            }
        }

        if (SpinnerCount == 0)
        {
            await this._jsRuntime.InvokeVoidAsync("hideGlobalLoadingSpinner");
        }
    }
}
           

要点:

  • 注入IJSRuntime,方便Javascript函数的调用。
  • 加入Locker以及SpinnerCount,这里的考虑是可能一次发起多个HTTP请求,可能会调用多次Show函数,因此我们使用计数器来记录请求数量的变化,当计数器变为0的时候才真正隐藏指示器。
  • SpinnerCount的初始值为什么是1? 因为index.html刚加载的时候是默认显示指示器的。

如何使用该接口?

首先修改Program.cs,注入IGlobalLoadingSpinner

builder.Services.AddSingleton<IGlobalLoadingSpinner, DefaultGlobalLoadingSpinner>();
           

然后在组件内注入该接口,比如上面的App组件我们可以修改为

@inject IGlobalLoadingSpinner globalLoadingSpinner

protected async override Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    await globalLoadingSpinner.HideAsync();
}
           

2.2 添加自定义HTTP处理程序

该处理程序用来在HTTP请求的上下文中显示和隐藏加载指示器:

public class LoadingSpinnerMessageHandler : DelegatingHandler
{
    private readonly ILogger<LoadingSpinnerMessageHandler> _logger;
    private readonly IGlobalLoadingSpinner _loadingSpinnerService;

    public LoadingSpinnerMessageHandler(ILogger<LoadingSpinnerMessageHandler> logger, IGlobalLoadingSpinner loadingSpinnerService)
    {
        _logger = logger;
        _loadingSpinnerService = loadingSpinnerService;
    }

    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //发送请求之前显示加载指示器
        await _loadingSpinnerService.ShowAsync();

        //发送请求
        var response = await base.SendAsync(request, cancellationToken);

        //收到结果之后隐藏加载指示器
        await _loadingSpinnerService.HideAsync();
        return response;
    }
}
           

自定义处理程序继承自DelegatingHandler(来自命名空间System.Net.Http),主要实现了SendAsync方法,该方面里面使用前面定义的接口来显示和隐藏加载指示器。

2.3 使用HttpClient请求

Blazor里面的Http请求都需要用到HttpClient,这里我们利用HttpClientFactory来创建HttpClient了,首先还是修改Program.cs:

//注入前面创建的处理程序
builder.Services.AddTransient<LoadingSpinnerMessageHandler>();

//定义HttpClientFactory,并添加处理程序
//这里使用了命名的HttpClient,当然你也可以定义所有HttpClient全部使用该处理程序
//详情查看:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0
//需要先安装包Microsoft.Extensions.Http
builder.Services.AddHttpClient("sampleapi", client =>
{
    client.BaseAddress = new Uri("http://localhost:5000");
})
.AddHttpMessageHandler<LoadingSpinnerMessageHandler>();
           

组件内使用也很简单:

@inject IHttpClientFactory httpFac

protected async Task LoadData()
{
    //通过名称创建HttpClient
    var httpClient = this.httpFac.CreateClient("sampleapi");
    //发送请求的时候,自动显示和隐藏加载指示器
    var forecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
           

最终效果如下:

为Blazor添加一个全局加载指示器