Masa Blazor自定義組件封裝

前言

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

1211423-20220428200958133-2043602818.gif

MASA Blazor介紹

組件展示

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

1211423-20220505104137751-1660887198.png

1211423-20220505104146221-1105573081.png

Material Design + BlazorComponent

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

1211423-20220505105813442-276464999.png

項目創建

首先確保已安裝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參考://www.dailytoolz.com/css-border-radius-generator/

新建ColorGroup.Razor文件,代碼如下:

<MItemGroup Mandatory Class="m-color-group d-flex mx-n1">
    <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="red">
            </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="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>
</MItemGroup>

修改Index.Blazor 文件 增加ColorGroup使用代碼,Masa.Blazor.Custom.Shared.Presets為自定義組件路徑,即命名空間:

<Masa.Blazor.Custom.Shared.Presets.ColorGroup>
</Masa.Blazor.Custom.Shared.Presets.ColorGroup>

運行代碼,看到多出三個不同顏色的圓型:

1211423-20220327133906519-1839872226.png

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<string> 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>

最終的效果如下:

1211423-20220327152408340-610620636.png

組件優化

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

減少組件重新渲染

合理重寫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缺少組件邊界,無法單獨刷新。

<h1>Hello, world!</h1>

@RenderWelcomeInfo

<p>Render the welcome info a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo = __builder =>
    {
        <p>Welcome to your new app!</p>
    };
}

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

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

實現IDisposable 或 IAsyncDisposable接口

組件實現IDisposable 或 IAsyncDisposable接口,會在組件從UI中被刪除時釋放非託管資源,事件註銷操作等。

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

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

總結

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

示例項目地址,更多內容參考Masa Blazor 預置組件 實現。

開源地址

MASA.BuildingBlocks://github.com/masastack/MASA.BuildingBlocks

MASA.Contrib://github.com/masastack/MASA.Contrib

MASA.Utils://github.com/masastack/MASA.Utils

MASA.EShop://github.com/masalabs/MASA.EShop

MASA.Blazor://github.com/BlazorComponent/MASA.Blazor

如果你對我們的 MASA Framework 感興趣,無論是代碼貢獻、使用、提 Issue,歡迎聯繫我們

16373211753064.png

Tags: