Masa Blazor自定義元件封裝

語言: CN / TW / HK

前言

實際專案中總能遇到一個"元件"不是基礎元件但是又會頻繁複用的情況,在開發MASA Auth時也封裝了幾個元件。既有簡單定義CSS樣式和介面封裝的元件(GroupBox),也有帶一定元件內部邏輯的元件(ColorGroup)。 本文將一步步演示如何封裝出一個如下圖所示的ColorGroup元件,將MItemGroup改造為ColorGroup,點選選擇預設的顏色值。

MASA Blazor介紹

元件展示

MASA Blazor 提供豐富的元件(還在增加中),篇幅限制下面展示一些我常用到的元件

Material Design + BlazorComponent

BlazorComponent是一個底層元件框架,只提供功能邏輯沒有樣式定義,MASA Blazor就是BlazorComponent基礎實現了Material Design樣式標準。如下圖所示,你可以基於Ant Design樣式標準實現一套Ant Design Blazor(雖然已經有了,如果你想這麼做完全可以實現)。

專案建立

首先確保已安裝Masa Template(避免手動引用MASA Blazor),如沒有安裝執行如下命令: dotnet new --install Masa.Template 建立一個簡單的Masa Blazor Server App專案: dotnet new masab -o MasaBlazorApp

元件封裝

Blazor元件封裝很簡單,不需要和vue一樣進行註冊,新建一個XXX.razor元件就是實現了XXX元件的封裝,稍微複雜些的是需要自定義元件內部邏輯以及定義開放給使用者(不同的使用場景)的介面(引數),即根據需求增加XXX.razor.cs和XXX.razor.css檔案。

介面封裝

在熟悉各種元件功能的前提下找出需要的元件組裝起來簡單實現想要的效果。這裡我使用MItemGroup、MCard及MButton實現ColorGroup的效果。MItemGroup做顏色分組,且本身提供每一項啟用的功能。MCard 作為顏色未選擇之前的遮罩層,實現模糊效果。MButton作為顏色展示載體及啟用MItem。通過MCard的style設定透明度區分選中、未選中兩種狀態。

也可通過增加一個對比色的圓形邊框標記選中狀態,相關CSS參考:https://www.dailytoolz.com/css-border-radius-generator/

新建ColorGroup.Razor檔案,程式碼如下:

```

<MItem>
    <MCard Class="elevation-0" Style="@($"transition: opacity .4s ease-in-out; {(context.Active ? "" : "opacity: 0.5;")}")">
        <MButton Fab class="mx-1 rounded-circle" OnClick="context.Toggle"
                 Width=20 Height=20 MinWidth=20 MinHeight=20 Color="blue">
        </MButton>
    </MCard>
</MItem>

<MItem>
    <MCard Class="elevation-0" Style="@($"transition: opacity .4s ease-in-out; {(context.Active ? "" : "opacity: 0.5;")}")">
        <MButton Fab class="mx-1 rounded-circle" OnClick="context.Toggle"
                 Width=20 Height=20 MinWidth=20 MinHeight=20 Color="green">
        </MButton>
    </MCard>
</MItem>

``` 修改Index.Blazor 檔案 增加ColorGroup使用程式碼,Masa.Blazor.Custom.Shared.Presets為自定義元件路徑,即名稱空間:

<Masa.Blazor.Custom.Shared.Presets.ColorGroup> </Masa.Blazor.Custom.Shared.Presets.ColorGroup> 執行程式碼,看到多出三個不同顏色的圓型:

Masa Blazor是Vuetify的Blazor實現,所有的Class除了m-color-group都是Vuetify提供的class樣式。

自定義引數

通過第一部分可以看到封裝的元件面子(介面)有了,但是這個面子是“死”的,不能根據不同的使用場景展示不同的效果,對於ColorGroup而言,最基本的需求就是使用時可以自定義顯示的顏色值。 Blazor中通過[Parameter]特性來宣告引數,通過引數的方式將上敘程式碼中寫死的值改為通過引數傳入。如按鈕的大小、顏色以及MItemGroup的class和style屬性等。同時增加元件的裡子(元件邏輯),點選不同顏色按鈕更新Value。

新建ColorGroup.Razor.cs檔案,新增如下程式碼:

``` public partial class ColorGroup { [Parameter] public List Colors { get; set; } = new();

[Parameter]
public string Value { get; set; } = string.Empty;

[Parameter]
public EventCallback<string> ValueChanged { get; set; }

[Parameter]
public string? Class { get; set; }

[Parameter]
public string? Style { get; set; }

[Parameter]
public int Size { get; set; } = 24;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        if (Colors.Any())
        {
            await ValueChanged.InvokeAsync(Colors.First());
        }
    }
    await base.OnAfterRenderAsync(firstRender);
}

} ```

上面的程式碼可以看到Value引數有個與之對應的ValueChanged引數,目的是為了能在元件外部接收Value值的變更,通過呼叫ValueChanged.InvokeAsync通知元件外部Value值更新。

需要注意的是應儘量減少引數定義,太多的引數會增加元件呈現的開銷。 減少引數傳遞,可以自定義引數類(本文示例為單獨定義多個引數)。如: ``` @code { [Parameter] public TItem? Data { get; set; }

[Parameter]
public GridOptions? Options { get; set; }

} ```

同時更新ColorGroup.Razor檔案中程式碼,迴圈Colors 屬性顯示子元素以及增加MButton的點選事件,更新Value值:

<MItemGroup Mandatory Class="@($"m-color-group d-flex mx-n1 {@Class}")" style="@Style"> @foreach (var color in Colors) { <MItem> <MCard Class="elevation-0" Style="@($"transition: opacity .4s ease-in-out; {(context.Active ? "" : "opacity: 0.5;")}")"> <MButton Fab class="mx-1 rounded-circle" OnClick="()=>{ context.Toggle();ValueChanged.InvokeAsync(color); }" Width=Size Height=Size MinWidth=Size MinHeight=Size Color="@color"> </MButton> </MCard> </MItem> } </MItemGroup>

此時使用ColorGroup的程式碼變為如下程式碼,可以靈活的指定顏色組資料以及ColorGroup的Class和Style等: <Masa.Blazor.Custom.Shared.Presets.ColorGroup Colors='new List<string>{"blue","green","yellow","red"}'> </Masa.Blazor.Custom.Shared.Presets.ColorGroup>

啟用隔離樣式

第一部分末尾提到了所有的Class除了m-color-group都是Vuetify提供的class樣式,那麼m-color-group是哪來的? 新增ColorGroup.Razor.css 檔案,ColorGroup.Razor.css 檔案內的css將被限定在ColorGroup.Razor元件內不會影響其它元件。最終會ColorGroup.Razor.css輸出到一個名為{ASSEMBLY NAME}.styles.css的捆綁檔案中,{ASSEMBLY NAME} 是專案的程式集名稱。 本文示例並沒有增加ColorGroup.Razor.css,只是覺得作為封裝元件現有樣式夠看了,增加m-color-group class 只是為了外部使用時方便css樣式重寫,並沒有做任何定義。

更多隔離樣式內容參考官方文件.

自定義插槽

目前為止,自定義的ColorGroup元件可以說已經夠看了,但是不夠打。因為形式單一,如果要在顏色選擇按鈕後增加文字或者圖片怎麼辦?這就又引入另外一個概念:插槽。 插槽(Slot)為vue中的叫法,Vuetify元件提供了大量的插槽如文字輸入框內的前後插槽和輸入框外的前後插槽(預設為Icon),MASA Blazor 同樣實現了插槽的功能,這也使得我們更容易定義和擴充套件自己的元件。

Blazor面向C#開發者更願意稱之為Template或者Content,通過RenderFragment實現插槽的效果。 若你的元件需要定義子元素,為了捕獲子內容,需要定義一個名為ChildContent型別為RenderFragment 的元件引數。

ColorGroup.Razor.cs檔案中增加RenderFragment屬性來定義每項末尾追加的插槽,並定義string引數,接收當前的顏色值。

[Parameter] public RenderFragment<string>? ItemAppendContent { get; set; }

RenderFragment定義帶引數元件,使用時預設通過context獲取引數值。更多內容參考官方文件

ColorGroup.Razor檔案中定義插槽位置

<MItem> <MCard Class="elevation-0" Style="@($"transition: opacity .4s ease-in-out; {(context.Active ? "" : "opacity: 0.5;")}")"> <MButton Fab class="mx-1 rounded-circle" OnClick="()=>{ context.Toggle();ValueChanged.InvokeAsync(color); }" Width=Size Height=Size MinWidth=Size MinHeight=Size Color="@color"> </MButton> </MCard> @if (ItemAppendContent is not null) { <div class="m-color-item-append d-flex align-center mr-1"> @ItemAppendContent(color) </div> } </MItem>

最終的效果如下:

元件優化

最後為元件在保證功能和美觀的同時也要保證效能,以下只是列舉了一些筆者認為比較常規的優化方式。

減少元件重新渲染

合理重寫ShouldRender方法,避免成本高昂的重新呈現。 貼一下官網程式碼自行體會,即一定條件都符合時才重新渲染: ``` @code { private int prevInboundFlightId = 0; private int prevOutboundFlightId = 0; private bool shouldRender;

[Parameter]
public FlightInfo? InboundFlight { get; set; }

[Parameter]
public FlightInfo? OutboundFlight { get; set; }

protected override void OnParametersSet()
{
    shouldRender = InboundFlight?.FlightId != prevInboundFlightId
        || OutboundFlight?.FlightId != prevOutboundFlightId;

    prevInboundFlightId = InboundFlight?.FlightId ?? 0;
    prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
}

protected override bool ShouldRender() => shouldRender;

} ```

減少不必要的StateHasChanged方法呼叫,預設情況下,元件繼承自 ComponentBase,會在呼叫元件的事件處理程式後自動呼叫StateHasChanged,對於某些事件處理程式可能不會修改元件狀態的情況,應用程式可以利用 IHandleEvent 介面來控制 Blazor 事件處理的行為。示例程式碼見官方文件

合理重寫元件生命週期方法

首先要理解元件生命週期,特別是OnInitialized(元件接收 SetParametersAsync 中的初始引數後呼叫)、OnParametersSet(接收到引數變更時呼叫)、OnAfterRender(元件完成呈現後呼叫)。 以上方法每個都會執行兩次及以上(render-mode="ServerPrerendered")。 元件初始化的邏輯合理的分配到各個生命週期方法內,最常見的就是OnAfterRender方法內,firstRender為true時呼叫js或者載入資料: protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync( "setElementText1", divElement, "Text after render"); } }

OnInitialized生命週期: * 在靜態預呈現元件時執行一次。 * 在建立伺服器連線後執行一次。 避免雙重呈現行為,應傳遞一個識別符號以在預呈現期間快取狀態並在預呈現後檢索狀態。

定義可重用的 RenderFragment

將重複的呈現邏輯定義為RenderFragment,無需每個元件開銷即可重複使用呈現邏輯。缺點就是重用RenderFragment缺少元件邊界,無法單獨重新整理。

```

Hello, world!

@RenderWelcomeInfo

Render the welcome info a second time:

@RenderWelcomeInfo

@code { private RenderFragment RenderWelcomeInfo = __builder => {

Welcome to your new app!

}; } ```

避免為重複的元素重新建立委託

Blazor 中過多重複的建立 lambda 表示式委託可能會導致效能不佳,如對一個按鈕組每個按鈕的OnClick分配一個委託。可以將表示式委託改為Action減少分配開銷。

實現IDisposable 或 IAsyncDisposable介面

元件實現IDisposable 或 IAsyncDisposable介面,會在元件從UI中被刪除時釋放非託管資源,事件登出操作等。

元件不需要同時實現 IDisposable 和 IAsyncDisposable。 如果兩者均已實現,則框架僅執行非同步過載。

更多內容參考:https://docs.microsoft.com/zh-cn/aspnet/core/blazor/performance?view=aspnetcore-6.0#define-reusable-renderfragments-in-code

總結

這裡只演示了一個ColorGroup很簡單的例子,當然你也可以把這個元件做的足夠“複雜”,其實元件的封裝並沒有想象的那麼複雜,無外乎上面提到的四個要素:介面、引數、樣式、插槽。既然有些元件官方不提供,只能自己動手豐衣足食(當然還是希望官方提供更多標準組件之外的擴充套件元件)。

示例專案地址,更多內容參考Masa Blazor 預置元件 實現。