Blazor应用由一个个组件堆叠而成,但真正决定可维护性的往往不是框架默认提供的页面模型,而是开发者能否把重复出现的UI片段抽象成稳定、易用的自定义组件。创建自定义组件并不只是把HTML复制到一个.razor文件里,它涉及参数设计、子内容投影、事件通信以及样式边界。下面从组件底层结构开始,给出可直接落地的实现方法。

一、先理解Blazor组件的基本结构
在Blazor中,一个组件本质上是一个C#类。.razor文件只是编译器友好的标记形式,编译后会生成继承自ComponentBase的类,并重写BuildRenderTree方法。组件内的HTML标记会被转换成RenderTreeBuilder指令,框架根据这些指令生成渲染树,再与上一次的渲染树做差异比较,最终只更新变化的那部分DOM。因此,自定义组件的核心不是写出更好看的HTML,而是设计好渲染树的构建逻辑和对外接口。
新建组件最直接的方式是在项目中添加一个.razor文件。比如创建MyCard.razor,文件名中的MyCard就是组件类名,使用时在父组件中写成<MyCard />。组件文件可以放在Shared、Components或任意目录下,但为了后续维护,建议按功能域建立Components/UI、Components/Layout等子目录。以下是一个最小组件,它接收标题参数并渲染自己的子内容。
<div class="panel">
<h3>@Title</h3>
<div class="panel-body">
@ChildContent
</div>
</div>
@code {
[Parameter]
public string? Title { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
这里的[Parameter]特性来自Microsoft.AspNetCore.Components命名空间。被该特性标注的属性会成为组件的公开接口,允许父组件以HTML属性的形式赋值。而RenderFragment类型的ChildContent是Blazor中约定俗成的子内容插槽,它让组件可以包裹任意标记。
二、通过参数让组件接收外部数据
参数是自定义组件复用的第一步。简单类型参数如string、int、bool可以直接在标签上赋值,Blazor会将特性值转换为目标类型。复杂对象参数则通常由父组件在@code块中准备数据,再通过属性绑定传入。假设要创建一个用户信息卡,先定义数据模型:
public class UserProfile
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
然后子组件接收该模型并显示:
<div class="user-card">
<h4>@Profile?.Name</h4>
<p>@Profile?.Email</p>
<span>状态:@(Profile?.IsActive == true ? "启用" : "禁用")</span>
</div>
@code {
[Parameter]
public UserProfile? Profile { get; set; }
}
父组件使用:
<UserCard Profile="currentUser" />
@code {
private UserProfile currentUser = new()
{
Name = "林晓",
Email = "linxiao@ipipp.com",
IsActive = true
};
}
需要注意,Blazor的参数传递遵循单向数据流。子组件不应该直接修改[Parameter]属性指向的对象内部状态,否则会破坏父组件的状态管理。比如子组件里调用Profile.Name = "新名字"可能不会触发父组件重新渲染,还可能导致状态不同步。正确做法是将需要变更的数据通过事件回调抛给父组件处理。
另外,如果参数类型是EventCallback或RenderFragment,赋值时不需要加@前缀;普通属性绑定在某些动态场景下需要写成Profile="@...",而静态赋值写不带@的形式即可。两者在编译结果上略有差异,但日常使用中优先选择简洁的静态绑定。
三、用事件回调建立子父组件通信
子组件通常只是UI的封装,业务动作的结果往往需要通知父组件。Blazor推荐使用EventCallback而不是C#事件或Action,因为EventCallback会参与组件生命周期,触发后会请求父组件重新渲染,同时支持异步调用而不会造成线程切换问题。来看一个确认按钮组件:
<button type="button" @onclick="HandleClick">
@ButtonText
</button>
@code {
[Parameter]
public string ButtonText { get; set; } = "确认";
[Parameter]
public EventCallback<string> OnConfirm { get; set; }
private async Task HandleClick()
{
await OnConfirm.InvokeAsync(ButtonText);
}
}
父组件接收回调结果:
<ConfirmButton ButtonText="保存修改" OnConfirm="HandleConfirm" />
<p>@lastAction</p>
@code {
private string lastAction = string.Empty;
private void HandleConfirm(string actionText)
{
lastAction = $"按钮【{actionText}】被点击";
}
}
EventCallback<T>的泛型参数表示要回传的数据类型。父组件的方法签名可以返回void或Task,如果返回Task,Blazor会等待异步方法执行完。这里@onclick是Blazor内置事件,但组件对外暴露的是OnConfirm,这是在原生事件之上做了一层业务语义抽象。
当回调需要传多个值时,可以把泛型参数换成复杂对象,或者定义多个回调属性。例如EventCallback<SearchArgs>,SearchArgs里包含关键词、页码、分类等条件。这样父组件不用关心子组件内部结构,只需处理一个明确的业务契约,组件边界也更清晰。
四、借助RenderFragment实现模板化组件
RenderFragment是一段可延迟执行的UI片段。组件可以把某个区域暴露成插槽,让父组件决定渲染什么内容。最常见的是默认子内容ChildContent,但很多时候一个组件需要多个自定义区域,比如列表项的模板、表格列模板或弹窗底部按钮区。为每个区域声明一个RenderFragment参数即可。
如果要让模板拿到每一项的数据,可以使用泛型版本RenderFragment<T>。下面的通用列表组件遍历字符串列表,并为每个条目调用ItemTemplate:
<div class="repeater">
@foreach (var item in Items)
{
<div class="repeater-item">
@ItemTemplate(item)
</div>
}
</div>
@code {
[Parameter]
public IReadOnlyList<string> Items { get; set; } = Array.Empty<string>();
[Parameter]
public RenderFragment<string> ItemTemplate { get; set; } = default!;
}
父组件使用命名模板:
<Repeater Items="memberNames">
<ItemTemplate Context="name">
<span class="badge">@name</span>
</ItemTemplate>
</Repeater>
@code {
private List<string> memberNames = new() { "Alice", "Bob", "Carol" };
}
在父组件中使用命名模板时,Context属性用于指定模板的上下文变量名,默认是context。如果模板参数是复杂类型,就可以在模板内访问该类型的属性。需要注意的是,RenderFragment参数不应在组件初始化阶段被调用,而是由Blazor在渲染流程中按需执行,因此不要在构造函数或字段初始化器里直接调用它。
利用RenderFragment还可以构建布局型组件,例如一个弹窗组件只负责遮罩、标题栏和关闭逻辑,中间内容和底部按钮交给父组件填充。这样业务页面可以复用同一个弹窗外壳,而不必重复编写定位、动画和关闭交互代码。
五、拆分组件时要注意样式隔离与边界
Blazor支持在.razor文件旁边放置同名的.razor.css文件,实现CSS隔离。构建后,这些样式会自动加上唯一的scope属性,避免与其他组件冲突。例如MyCard.razor.css里写.panel { border: 1px solid #ddd; },最终只作用于MyCard内部的.panel元素。如果需要影响子组件的根元素,可以使用::deep伪类穿透。
拆分组件时要优先按照“展示与业务状态分离”的原则。展示类组件一般只负责根据参数渲染UI,并通过事件回调向上通知;业务状态组件则负责调用服务、管理加载状态、组织多个展示组件。不要因为某个页面很长就盲目拆成多个组件,而应先识别重复出现的UI模式。例如列表卡片、筛选面板、空状态提示都可以作为独立组件。一个参数超过8到10个的组件往往意味着职责过重,可能需要抽取子组件,或者把相关参数封装成配置对象。
组件命名应使用PascalCase,避免与HTML原生元素撞名。例如不要创建名为Button或Input的组件,即便能编译,也会给阅读带来歧义。更推荐PrimaryButton、SearchInput这样的业务化名称。目录结构上,通用UI组件和页面级组件最好分开,方便后续打包成Razor类库供多个项目复用。只有把组件边界划分清楚,自定义组件才能真正成为团队中长期可维护的基础设施。
Blazor自定义组件组件参数事件回调修改时间:2026-10-02 16:04:47