写点什么

Masa Blazor 自定义组件封装

  • 2022 年 5 月 06 日
  • 本文字数:5461 字

    阅读完需:约 18 分钟

前言

实际项目中总能遇到一个"组件"不是基础组件但是又会频繁复用的情况,在开发 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 文件,代码如下:


<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>
复制代码


运行代码,看到多出三个不同颜色的圆型:



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<T>定义带参数组件,使用时默认通过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 缺少组件边界,无法单独刷新。


<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。 如果两者均已实现,则框架仅执行异步重载。


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

总结

这里只演示了一个 ColorGroup 很简单的例子,当然你也可以把这个组件做的足够“复杂”,其实组件的封装并没有想象的那么复杂,无外乎上面提到的四个要素:界面、参数、样式、插槽。既然有些组件官方不提供,只能自己动手丰衣足食(当然还是希望官方提供更多标准组件之外的扩展组件)。


示例项目地址,更多内容参考Masa Blazor 预置组件 实现。

开源地址

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


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


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


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


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


如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们



发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2021.10.26 加入

还未添加个人简介

评论

发布
暂无评论
Masa Blazor自定义组件封装_C#_MASA技术团队_InfoQ写作社区