前言

实际项目中总能遇到一个"组件"不是基础组件但是又会频繁复用的情况,在开发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定义带参数组件,使用时默认通过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,欢迎联系我们

Masa Blazor自定义组件封装的更多相关文章

  1. 微信小程序自定义组件封装及父子间组件传值

    首先在我们可以直接写到需要的 page 中,然后再进行抽取组件,自定义组件建议 wxzx-xxx 命名 官网地址:https://developers.weixin.qq.com/miniprogra ...

  2. Vue.js 自定义组件封装实录——基于现有控件的二次封装(以计时器为例)

    在本人着手开发一个考试系统的过程中,出现了如下一个需求:制作一个倒计时的控件显示在试卷页面上.本文所记录的就是这样的一个过程. 前期工作 对于这个需求,自然我想到的是有没有现成的组件可以直接使用(本着 ...

  3. Angular 4 自定义组件封装遇见的一些事儿

    你用Angular 吗? 一.介绍 说说封装Angular 组建过程中遇见的一些问题和感悟.用久了Angular,就会遇见很多坑,许多基于Angular开发的框架最喜欢做的事情就是封装组件,然后复用. ...

  4. 使用MASA Blazor开发一个标准的查询表格页

    前言 大家好,我是开源项目 MASA Blazor 主要开发者之一,如果你还不了解MASA Blazor,可以访问我们的 官网 和博客 <初识MASA Blazor> 一探究竟.简单来说, ...

  5. MASA Blazor入门这一篇就够了

    1.什么是Blazor? 有什么优势? ASP.NET Core Blazor 简介 Blazor 是一个使用 Blazor 生成交互式客户端 Web UI 的框架: 使用 C# 代替 JavaScr ...

  6. Masa Blazor in Blazor Day

    2022年第一场Blazor中文社区的开发者分享活动,我们的团队也全程参与其中,在议程中,也分享了我们团队的Blazor 管理后台模板,针对于Blazor,先科普一波,避免有些朋友不了解,Blazor ...

  7. iOS开发之自定义表情键盘(组件封装与自动布局)

    下面的东西是编写自定义的表情键盘,话不多说,开门见山吧!下面主要用到的知识有MVC, iOS开发中的自动布局,自定义组件的封装与使用,Block回调,CoreData的使用.有的小伙伴可能会问写一个自 ...

  8. vue2.0 如何自定义组件(vue组件的封装)

    一.前言 之前的博客聊过 vue2.0和react的技术选型:聊过vue的axios封装和vuex使用.今天简单聊聊 vue 组件的封装. vue 的ui框架现在是很多的,但是鉴于移动设备的复杂性,兼 ...

  9. 初识MASA Blazor

    MASA Blazor是一个Blazor的UI组件库.就像大家写前端熟知的Bootstrap, Ant Design一样. MASA Blazor官网地址:https://blazor.masasta ...

随机推荐

  1. 面试问题之数据结构与算法:map与unordered_map

    转载于:https://blog.csdn.net/u011475134/article/details/75810085 map map是STL的一个关联容器,它提供一对一数据处理能力.map内部自 ...

  2. 服务端处理 Watcher 实现 ?

    1.服务端接收 Watcher 并存储 接收到客户端请求,处理请求判断是否需要注册 Watcher,需要的话将数据节点 的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端 ...

  3. 为什么 Java 中的 String 是不可变的(Immutable)?

    Java 中的 String 不可变是因为 Java 的设计者认为字符串使用非常频繁,将字 符串设置为不可变可以允许多个客户端之间共享相同的字符串.

  4. SQL语句之Column 'Status' in where clause is ambiguous错误

    问题: AND created_by IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) limit 0, 10]; Column 'created_by' in where cla ...

  5. Web最佳实践阅读总结(1)

    介绍 最近开始刷一些书和题,此系列是介绍在读Web最佳实践的一些收获和体会. web前端发展现状 存在问题: 代码组织混乱 代码格式的问题突出 页面布局随意 网站整体性能差,没有意识到应用诸如缓存,动 ...

  6. 在小程序中Tabbar显示和隐藏的秘密

    其实对Tabbar 的用法的理解总结下来分这几个阶段: 第一阶段:在 app.json 中配置 "tabBar": { "list": [{ "pag ...

  7. Issues with position fixed & scroll(移动端 fixed 和 scroll 问题)

    转载请注明英文原文及译文出处 原文地址:Issues with position fixed & scrolling on iOS 原文作者:Remy Sharp译文地址:移动端 fixed ...

  8. 【每日日报】第三十二天---DataOutputStream写文件

    1 今天继续看书 DataOutputStream写文件 1 package File; 2 import java.io.IOException; 3 import java.io.FileOutp ...

  9. 布局框架frameset

    <!DOCTYPE html>//demo.html <html> <head> <meta charset="UTF-8"> &l ...

  10. springboot插件

    目前spring官网(http://spring.io/tools/sts/all)上可下载的spring插件只有:springsource-tool-suite-3.8.4(sts-3.8.4).但 ...