.NET雲原生應用實踐(五):使用Blazor WebAssembly實現前端頁面

dax.net發表於2024-11-03

本章目標

  • 使用Blazor WebAssembly實現管理“貼紙”頁面
  • 整合認證與授權機制

如果你對Blazor WebAssembly的使用不感興趣,可以跳過本章的閱讀。你也可以使用自己熟悉的前端技術完成案例的介面部分,之前我們開發的後端API比較簡單,所以自己實現一套前端介面並不會是一個困難的事情。

完成本章內容後,我們會得到下面的效果(點選檢視大圖),是不是跟第一章中所畫的概念圖已經很接近了?

.NET雲原生應用實踐(五):使用Blazor WebAssembly實現前端頁面

我們到哪裡了?

在進一步介紹後續內容之前,先看看目前實現了哪些內容。回顧之前的一張架構簡圖(其實也不算是架構圖),彩色部分是目前我們已經實現的部分,雖然目前有些地方還並不完善,只是在開發環境能夠正常執行起來,並且我們開發的RESTful API都還沒有容器化。

.NET雲原生應用實踐(五):使用Blazor WebAssembly實現前端頁面

本章會完成“Sticker前端應用”這個部分,在完成這部分內容後,我們就可以在開發環境中除錯執行整個應用程式了,由於還沒有引入基於nginx的API閘道器,所以,整個系統的結構跟上圖相比還是會有些差異。

Blazor WebAssembly是什麼?

如果問ChatGPT,它的回答是這樣的:Blazor WebAssembly是一個基於WebAssembly的現代Web應用程式框架,由微軟開發。它允許開發人員使用C#和.NET技術構建客戶端Web應用程式,而無需使用JavaScript。Blazor WebAssembly利用WebAssembly的效能優勢,將C#程式碼編譯為WebAssembly位元組碼,從而在瀏覽器中執行高效能的客戶端應用程式。開發人員可以使用Blazor元件模型構建互動式和動態的使用者介面,同時利用.NET的強大功能和生態系統。Blazor WebAssembly還支援與伺服器端Blazor應用程式的通訊,以及與現有JavaScript庫的整合,為開發人員提供了靈活和強大的工具來構建現代的Web應用程式。

Blazor應用程式基本上可以分為兩種:

  • Blazor服務端應用:它基於ASP.NET Core基礎設施實現服務端Hosting,並透過一種通訊方式(比如SignalR)實現使用者互動
  • Blazor WebAssembly:它是在客戶端瀏覽器中執行的Web應用程式,它將C#程式碼編譯為WebAssembly位元組碼,直接在瀏覽器中執行。Blazor WebAssembly應用程式完全在客戶端執行,可以實現更快的載入速度和更高的效能,適用於需要在客戶端獨立執行的應用程式,以及對實時性要求較高的應用

從.NET 8開始,Visual Studio引入新的Blazor應用程式構建模板:Blazor Web App,它整合了Blazor服務端和Blazor WebAssembly的優勢,並且利用了.NET 8中新引入的Blazor相關功能,比如靜態服務端渲染(static SSR)、流式渲染(Streaming Rendering)等。原有的Blazor Server App和Blazor WebAssembly Standalone App在.NET 8 中仍然支援,只不過可以考慮將這些型別的應用遷移到Blazor Web App上。詳見:https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-7.0#new-blazor-web-app-template

本系列文章案例程式碼選用Blazor WebAssembly專案模板作為基礎進行開發。

為什麼選擇Blazor WebAssembly?

現在前端技術非常成熟,體系也很龐大,為何拋棄React、Angular、Vue這些前端框架不選,偏偏選擇Blazor WebAssembly呢?我想大概幾個方面吧:

  • 我想嘗試一下微軟原生WebAssembly的開發框架,看看開發者體驗如何
  • 我打算仍然選擇微軟技術來展示案例(之前有讀者對我使用Java系的Keycloak有質疑,其實Keycloak在整個案例架構中只是一個IdP,它跟PostgreSQL、Redis這些元件等價,這麼說能理解吧?)
  • 我對C#技術棧更為熟悉,功能開發和問題調查都會更加方便快捷,而且不容易出錯。在微服務的開發模式中,技術選擇其實跟團隊成員的偏好也有一定的關係,在能夠滿足各種功能性和非功能性需求的前提下,團隊當然希望採用更為熟知的技術來完成研發。聊到我的前端技術,我個人對Angular比較熟悉,因為之前做過Angular的前端專案,React和Vue一直都沒有機會實踐(或許我也不應該再“卷”下去了)

除了微軟的Microsoft Learn和線上教育平臺Edink之外,還是有不少站點是基於Blazor技術構建的,微軟官方也給了幾個客戶案例,它們大多數都是US的公司,國內很少使用。

從上面三點可以看到,我在這個案例中選擇Blazor WebAssembly,主觀因素更多一些,在實際專案中,大機率大家也不會選擇Blazor WebAssembly來構建自己的前端應用,原因也會是多方面的。由於本系列文章所介紹的案例比較簡單,前端部分暫時也不會有特別高的要求,所以我就基於自己的主觀需求,選擇了Blazor WebAssembly。讀者完全可以基於本案例的服務端程式碼,使用自己熟悉的前端技術來重製“貼紙牆”的前端部分。

構建Stickers.Web應用

首先就是建立一個Blazor WebAssembly的應用,並啟用認證機制,因為後面需要整合認證和授權流程。此外,我還在專案中使用了Blazor Bootstrap元件庫,這個元件庫對主要的Bootstrap元件進行了封裝,並讓其在Blazor應用中完美執行。使用Blazor Bootstrap需要有一些配置工作,這裡不多介紹了,官方文件有Get Started操作流程。

Blazor WebAssembly的開發過程這裡也不多做介紹了,請直接參考本文的原始碼。這裡主要介紹三個話題:自定義元件、使用HttpClient訪問後端服務,以及認證與授權。

自定義元件

通常我們會把一些能夠重複使用的前端程式碼封裝成一個元件,並透過引數來接受資料並定製業務邏輯,執行過程中又透過事件與其它元件互動。比如,一個分頁功能就可以封裝成一個元件,它可以透過引數來設定分頁按鈕的樣式以及一次展現多少個分頁按鈕,當使用者點選某個頁碼時,它又以事件的方式通知相關的其它元件(比如父頁面)被點選的頁碼數,以便觸發頁面更新等後續操作。

下面的程式碼是案例中的“編輯貼紙”的元件,這個元件有一個引數:StickerEditModel,用來指定使用者操作行為型別(新建/編輯)以及將要新建/被編輯貼紙的資料模型,此外還包含兩個事件:OnCloseClickCallbackOnSaveClickCallback,當元件介面上的“關閉”和“儲存”按鈕被點選時,會觸發這兩個事件。StickerEditModel的定義如下:

public enum EditMode
{
    Create,
    Edit
}

public class StickerEditModel
{
    public string? Content { get; set; }
    public int Id { get; set; }
    public string? Title { get; set; }
    public EditMode EditMode { get; set; }
}

StickerEditModel看起來跟Sticker業務物件很像,但它只關注介面上所需的資料,所以,在StickerEditModel中,並沒有CreatedOnModifiedOn這些屬性,因為這些屬性都是在建立或者修改貼紙時由系統自動生成的,新建/編輯貼紙的介面上並不需要這些資訊。以下是“編輯貼紙”的元件EditStickerComponent的程式碼:

@using Stickers.Web.ViewModels

@if (Model is not null)
{
    <div class="mb-3">
        <input @ref="_txtTitleRef" type="text" class="form-control" placeholder="請輸入貼紙標題" @bind-value="Model.Title">
    </div>
    <div class="mb-3">
        <InputTextArea class="form-control" placeholder="請輸入貼紙內容" @bind-Value="Model.Content"/>
    </div>

    <div class="d-grid gap-2 d-md-flex justify-content-md-end mt-2">
        <Button Color="ButtonColor.Secondary" @onclick="OnCloseClickCallback"> 取消 </Button>
        <Button Color="ButtonColor.Primary" @onclick="OnSaveClick"> 儲存 </Button>
    </div>
}

@code {
    [Parameter]
    public StickerEditModel? Model { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnCloseClickCallback { get; set; }

    [Parameter]
    public EventCallback<StickerEditModel> OnSaveClickCallback { get; set; }

    private ElementReference? _txtTitleRef;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (_txtTitleRef.HasValue)
        {
            await _txtTitleRef.Value.FocusAsync(true);
        }
    }

    private async Task OnSaveClick()
    {
        await InvokeAsync(() => OnSaveClickCallback.InvokeAsync(Model));
    }
}

它提供了兩個文字框,分別讓使用者輸入貼紙的標題和內容,還有兩個按鈕,讓使用者儲存所做的修改或者取消所做的操作。呼叫元件會生成一個StickerEditModel的例項,透過Model引數傳入這個元件,然後以對話方塊的形式顯示該元件以接收使用者輸入,當使用者完成操作並點選儲存或者取消按鈕時,透過事件將使用者的輸入返回給呼叫方。

使用HttpClient訪問後端服務

在Blazor WebAssembly中訪問後端服務是非常方便的,只需在Program.cs中加入HttpClient的支援,比如:

builder.Services.AddHttpClient(
    "myHttpClient", 
    client => client.BaseAddress = new Uri("http://localhost:5000")
);

然後,在Razor頁面或者元件中,透過注入HttpClientFactory,就可以使用註冊的HttpClient了:

@inject IHttpClientFactory HttpClientFactory

@code {
    private override async Task OnInitializedAsync()
    {
        // ...
        using var httpClient = HttpClientFactory.GetClient("myHttpClient");
        var responseMessage = await httpClient.GetAsync("api/any-api");
        // ...
    }
}

HttpClient在Blazor中的使用,跟ASP.NET Core中非常類似,可以直接閱讀官方文件來了解詳細內容,這裡就不多做介紹了。

認證與授權

Stickers.Web專案中需要呼叫後端的Stickers.WebApi RESTful API來實現其功能,而後端API是需要認證和授權的,所以,前端介面在HttpClient傳送API呼叫請求時,就需要把access token帶上,否則API呼叫是不會成功的。在Blazor WebAssembly中,要實現這個邏輯,就需要自定義DelegatingHandler,然後在HttpClient中使用這個自定義的Handler。

Blazor WebAssembly支援一種稱之為AuthorizationMessageHandlerDelegatingHandler,它可以直接拿來使用,以便將access token附加到發出的HTTP請求上。只需要在新增HttpClient的時候,指定HttpMessageHandler即可:

builder.Services.AddHttpClient(
    "myHttpClient", 
    client => client.BaseAddress = new Uri("http://localhost:5000")
).AddHttpMessageHandler<AuthorizationMessageHandler>();

認證使用者可以登入站點,並不表示該使用者可以訪問所有的頁面並進行所有的操作,比如前文中所建立的nobody使用者,它只能被認證,卻沒有任何授權,所以,對於該使用者而言,它是無法使用“貼紙”功能的。在這個使用者登入之後,即便登入沒有問題,使用該使用者的access token去訪問後端API服務仍然會得到403 Forbidden的錯誤,比如,在這個使用者點選“我的貼紙牆”頁面時,下面的程式碼就會丟擲未授權異常:

@code {
    protected override async Task OnInitializedAsync()
    {
        CurrentPage = await ReadStickersAsync();
        // 此處由於異常未被處理,造成頁面出錯
        await base.OnInitializedAsync();
    }

    private async Task<StickersPage?> ReadStickersAsync(
        int pageNumber = 1,
        int pageSize = DefaultPageSize)
    {
        using var httpClient = HttpClientFactory.CreateClient("stickersHttpClient");
        var httpResponseMessage = await httpClient
            .GetAsync($"api/stickers?page={pageNumber}&size={pageSize}");
        httpResponseMessage.EnsureSuccessStatusCode(); // 此處丟擲異常
        var responseJson = await httpResponseMessage.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<StickersPage>(responseJson);
    }
}

解決這個問題的思路有兩種:

  1. 由於WebAssembly是可以得到使用者的access token的,所以也可以像之前Stickers API裡設計的那樣,獲得使用者的授權資訊,然後根據使用者的授權資訊來設計前端的授權機制(Blazor WebAssembly預設基於角色授權,也可以自己開發自定義的Policy來實現更為靈活的授權方案),再根據這套機制和使用者本身的授權資訊以判定某個元件是否應該顯示、是否可以被該使用者使用
  2. 簡單粗暴,在呼叫API時,如果異常,則捕獲異常並直接跳轉到登入介面或者錯誤介面,提示使用者沒有許可權

第一種方案其實更為合理,一方面如果使用者本來就沒有許可權,那就可以直接把不可以訪問的元件隱藏掉或者禁用,沒必要等到使用者點選的時候才報錯;另一方面,設計一個前端授權機制也會使得元件和頁面的訪問控制變得更為靈活,如果設計合理,還可以跟Blazor WebAssembly的授權機制無縫整合,大大減少出錯的機率。而第二種方案則相對簡單一些,適用於像本文這樣的demo場景(Blazor應用的授權設計不是本案例的重點)。

首先可以自定義一個AuthorizationMessageHandler,然後透過AddHttpMessageHandler方法,將這個Handler註冊到HttpClient上:

public class StickersMessageHandler : AuthorizationMessageHandler
{
    private readonly NavigationManager _navigationManager;
    
    public StickersMessageHandler(IAccessTokenProvider provider, NavigationManager navigation) : base(provider,
        navigation)
    {
        _navigationManager = navigation;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        try
        {
            var responseMessage = await base.SendAsync(request, cancellationToken);
            if (responseMessage.StatusCode == HttpStatusCode.Forbidden)
            {
                _navigationManager.NavigateTo("/forbidden");
            }
            return responseMessage;
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
            return new HttpResponseMessage();
        }
    }
}

這個類首先注入一個NavigationManager例項,然後在過載的SendAsync方法中,判斷返回的狀態碼是否為403 Forbidden,如果是的話,就直接跳轉到/forbidden頁面就可以了。這裡的程式碼雖然對狀態碼進行了判斷,但是在呼叫端的EnsureSuccessStatusCode方法仍然會因為狀態碼不是2XX而丟擲異常。這裡只要稍微處理一下就可以了:

protected override async Task OnInitializedAsync()
{
    try
    {
        CurrentPage = await ReadStickersAsync();
    }
    catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Forbidden)
    {
        return;
    }
    
    await base.OnInitializedAsync();
}

總結

本文簡單介紹了基於Blazor WebAssembly實現前端的幾個主要方面,前端程式碼很多,一篇文章也無法全部介紹完整,有興趣的讀者請直接下載原始碼閱讀。在執行本案例的過程中,你會發現,登入使用者之前還能互相看到對方所建立的貼紙,這是一個bug,在下一講中,我將透過引入多租戶的初步設計,將這個bug修復掉。

原始碼

本章原始碼在chapter_5這個分支中:https://gitee.com/daxnet/stickers/tree/chapter_5/

下載原始碼前,請先刪除已有的stickers-pgsql:devstickers-keycloak:dev兩個容器映象,並刪除docker_stickers_postgres_data資料卷。

下載原始碼後,進入docker目錄,然後編譯並啟動容器:

$ docker compose -f docker-compose.dev.yaml build
$ docker compose -f docker-compose.dev.yaml up

現在就可以直接用Visual Studio 2022或者JetBrains Rider開啟stickers.sln解決方案檔案,然後同時啟動Stickers.WebApi和Stickers.Web兩個專案進行除錯執行了。

相關文章