5. 【Blazor全栈开发实战指南】--Blazor组件基础

一、Razor组件语法:.razor文件的结构

Blazor应用的基本构建单元是Razor组件 ,每个组件对应一个.razor文件。如果你曾经接触过React的单文件组件或Vue的.vue文件,你会对这种设计感到亲切------组件将UI模板、样式和逻辑封装到同一个文件中,形成自包含的代码单元。

一个典型的.razor文件由三个部分组成:指令区(Directives)HTML模板区代码块(@code)。我们来看一个完整的示例:

csharp 复制代码
@* 文件:Components/Pages/ProductCard.razor *@

@* 1. 指令区:声明路由、使用命名空间、注入服务 *@
@page "/products/{Id:int}"
@using MyApp.Models
@inject IProductService ProductService

@* 2. HTML模板区:描述UI结构,可以嵌入C#表达式 *@
<div class="product-card">
    @if (product is null)
    {
        <p>正在加载...</p>
    }
    else
    {
        <h2>@product.Name</h2>
        <p class="price">¥@product.Price.ToString("F2")</p>
        <button @onclick="AddToCart" disabled="@isAdding">
            @(isAdding ? "添加中..." : "加入购物车")
        </button>
    }
</div>

@* 3. 代码块:组件的C#逻辑 *@
@code {
    [Parameter]
    public int Id { get; set; }

    private Product? product;
    private bool isAdding = false;

    protected override async Task OnParametersSetAsync()
    {
        product = await ProductService.GetByIdAsync(Id);
    }

    private async Task AddToCart()
    {
        isAdding = true;
        await ProductService.AddToCartAsync(product!.Id);
        isAdding = false;
    }
}

指令区通常位于文件顶部,以@符号开头。@page声明路由模板,组件只有带@page才能作为可访问的页面;@using引入命名空间,与C#文件中的using语句效果相同;@inject从依赖注入容器中获取服务实例,等同于在构造函数中接收注入。如果每个组件都要重复写相同的@using语句会很繁琐,解决方案是在_Imports.razor文件中集中声明------Components目录下的_Imports.razor中的所有指令会自动应用到该目录及其所有子目录下的组件,相当于全局隐式using

HTML模板区是组件的视觉骨架,使用标准HTML语法,通过@前缀嵌入C#表达式。单个表达式直接写@变量名@方法名();多行语句或控制结构使用@{ ... }代码块;条件渲染使用@if/@else;列表渲染使用@foreach。这些语法我们在后续数据绑定章节会详细展开。

@code { ... }代码块包含了整个组件的C#逻辑------字段、属性、方法以及生命周期钩子都写在这里。对于逻辑较复杂的组件,可以采用"代码后置(code-behind) "模式,创建一个同名的.razor.cs文件,在其中定义一个继承自ComponentBase的分部类(partial class),将代码逻辑从.razor模板中分离,使两个文件各司其职:

csharp 复制代码
// ProductCard.razor.cs
public partial class ProductCard
{
    [Parameter]
    public int Id { get; set; }

    private Product? product;
    // ...其余逻辑
}

二、组件的生命周期

每个Blazor组件从被创建到销毁,都经历一个确定的生命周期,框架在各个阶段提供了可被重写的钩子方法。理解生命周期的顺序,是正确处理数据加载、副作用和资源清理的前提。

组件诞生时,框架首先调用构造函数 (通常是隐式的),紧接着是**SetParametersAsync**------这是参数注入的总入口,框架将父组件传入的参数值填充到标记了[Parameter]的属性上。绝大多数情况下不需要重写此方法,直接交给基类处理即可。参数设置完毕后,OnInitializedOnInitializedAsync被调用,这个钩子在组件整个生命周期内只执行一次,非常适合做一次性初始化工作,比如在页面首次加载时拉取数据:

csharp 复制代码
@code {
    private List<Article> articles = [];

    // 同步版本(适合不需要异步操作的初始化)
    protected override void OnInitialized()
    {
        // 同步初始化逻辑,例如读取缓存
    }

    // 异步版本(适合需要等待I/O的场景,如调用API)
    // 框架会先渲染一次(显示加载中状态),异步完成后再次渲染
    protected override async Task OnInitializedAsync()
    {
        articles = await ArticleService.GetAllAsync();
    }
}

每当父组件传入的参数发生变化,框架会重新调用OnParametersSetOnParametersSetAsync。这与OnInitialized的区别在于------后者只在首次创建时运行,前者在每次参数变化时都会运行。上文ProductCard示例中就利用了OnParametersSetAsync,确保路由参数Id发生变化时(用户从一个产品页导航到另一个产品页),能够重新加载对应的产品数据。

组件完成渲染并将UI提交给DOM之后,OnAfterRender(bool firstRender)被调用。firstRender参数标记这是否是首次渲染,利用这个标志可以避免在每次重渲染时重复执行操作。这个钩子适合需要访问真实DOM元素或调用JavaScript互操作的场景------在组件渲染之前调用JS通常是无效的,因为被操作的DOM节点可能还不存在:

csharp 复制代码
@code {
    [Inject]
    private IJSRuntime JS { get; set; } = default!;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 仅在首次渲染后执行,初始化需要依赖真实DOM的JS插件
            await JS.InvokeVoidAsync("initializeChart", "myChart");
        }
    }
}

当组件被从UI树中移除时,如果组件实现了IDisposable(或IAsyncDisposable)接口,Dispose方法会被调用。应该在这里释放订阅的事件、取消定时器或清理非托管资源,否则可能导致内存泄漏:

csharp 复制代码
@implements IDisposable

@code {
    private System.Threading.Timer? _timer;

    protected override void OnInitialized()
    {
        // 创建一个每秒触发一次的定时器
        _timer = new System.Threading.Timer(_ =>
        {
            // 在Timer回调中,需要通过InvokeAsync确保在Blazor同步上下文中更新UI
            InvokeAsync(StateHasChanged);
        }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
    }

    public void Dispose()
    {
        // 组件销毁时停止定时器,避免泄漏
        _timer?.Dispose();
    }
}

三、参数传递与事件处理

组件之间的通信是组合型UI的核心机制。在Blazor中,父组件通过参数(Parameter)向子组件传递数据,子组件通过EventCallback向父组件回传事件通知,形成单向数据流。

在子组件中,将一个公开属性标记[Parameter]特性,它就成为一个可从父组件接收数据的参数:

csharp 复制代码
@* 子组件:Components/Shared/AlertBox.razor *@
<div class="alert alert-@Type">
    <strong>@Title</strong>
    <p>@Message</p>
    <button @onclick="OnCloseClicked">关闭</button>
</div>

@code {
    // 基础参数:接收字符串和枚举类型
    [Parameter]
    public string Title { get; set; } = string.Empty;

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

    // 当参数是枚举等有限集合时,可提供更强的类型约束
    [Parameter]
    public string Type { get; set; } = "info"; // "info" | "warning" | "danger"

    // EventCallback用于向父组件通知事件
    // EventCallback<T>可以携带数据,EventCallback不携带数据
    [Parameter]
    public EventCallback OnClose { get; set; }

    private async Task OnCloseClicked()
    {
        // 触发父组件传入的回调,如果父组件没有传入,InvokeAsync是安全的空操作
        await OnClose.InvokeAsync();
    }
}

父组件使用子组件时,以HTML属性的形式传入参数值,用@前缀传递C#表达式,用@on事件名绑定回调方法:

csharp 复制代码
@* 父组件使用AlertBox *@
@if (showAlert)
{
    <AlertBox
        Title="操作成功"
        Message="@alertMessage"
        Type="info"
        OnClose="HandleAlertClose" />
}

@code {
    private bool showAlert = true;
    private string alertMessage = "您的数据已保存。";

    private void HandleAlertClose()
    {
        showAlert = false;
    }
}

注意EventCallback与C#原生Action/Func委托的重要区别:EventCallback在被触发时会自动调用StateHasChanged(),通知组件渲染引擎当前组件需要重新渲染;而普通委托触发后若不手动调用StateHasChanged(),UI不会自动更新。这是初学者常遇到的陷阱之一。

[Parameter]还有一个特殊的变体------[Parameter] public RenderFragment ChildContent { get; set; },它允许父组件向子组件传递一段任意的Razor内容(相当于Vue的slot或React的children属性),使子组件变成通用的"容器":

csharp 复制代码
@* 卡片容器组件 Card.razor *@
<div class="card">
    <div class="card-body">
        @ChildContent
    </div>
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}

@* 父组件中使用Card,将任意内容放入标签内 *@
<Card>
    <h3>这是卡片标题</h3>
    <p>这是卡片内容,可以放任意Razor内容。</p>
</Card>

四、UI渲染与数据绑定

Blazor提供了丰富的模板语法来控制UI的动态渲染,其中最常用的是条件渲染、列表渲染和双向数据绑定。

4.1 条件渲染

使用@if/@else if/@else,根据C#布尔表达式决定是否渲染某段HTML。对于多路分支,@switch更加清晰:

csharp 复制代码
@* 根据订单状态渲染不同UI *@
@switch (order.Status)
{
    case OrderStatus.Pending:
        <span class="badge bg-warning">待处理</span>
        break;
    case OrderStatus.Shipped:
        <span class="badge bg-info">已发货</span>
        break;
    case OrderStatus.Delivered:
        <span class="badge bg-success">已送达</span>
        break;
    default:
        <span class="badge bg-secondary">未知状态</span>
        break;
}
4.2 列表渲染

使用@foreach遍历集合。一个值得注意的实践是:在渲染列表时为每个元素提供唯一的@key属性。@key是Blazor差异算法的提示,有了它,框架在集合发生变化时(如项目排序,或插入新项)能够精确识别哪些DOM节点需要复用、哪些需要创建或销毁,避免不必要的DOM重建,提升性能:

csharp 复制代码
<ul>
    @foreach (var item in todoItems)
    {
        @* @key 告诉Blazor用 item.Id 来识别这个DOM节点 *@
        <li @key="item.Id">
            <input type="checkbox" checked="@item.IsDone" @onchange="() => ToggleTodo(item.Id)" />
            <span class="@(item.IsDone ? "done" : "")">@item.Text</span>
        </li>
    }
</ul>
4.3 双向数据绑定

是构建表单的核心机制。@bind指令将HTML表单元素的值与C#字段实现双向同步------用户在输入框中输入时,C#字段自动更新;C#字段值变化时,输入框的显示也会随之更新:

csharp 复制代码
<input @bind="searchText" placeholder="搜索..." />
<p>当前搜索词:@searchText</p>

@* 数字输入框,@bind会自动处理字符串到int的转换 *@
<input type="number" @bind="quantity" min="1" max="99" />

@* 下拉选择框 *@
<select @bind="selectedCategory">
    <option value="">全部分类</option>
    @foreach (var cat in categories)
    {
        <option value="@cat.Id">@cat.Name</option>
    }
</select>

@code {
    private string searchText = string.Empty;
    private int quantity = 1;
    private string selectedCategory = string.Empty;
    private List<Category> categories = [];
}

默认情况下,@bindonchange事件(失去焦点时或按Enter键)触发时更新绑定值。如果需要在用户每次击键时实时更新,应使用@bind:event="oninput"

csharp 复制代码
@* 实时搜索:每次键入字符都触发更新 *@
<input @bind="searchText" @bind:event="oninput" placeholder="实时搜索..." />
<p>结果数:@FilteredResults.Count()</p>

@code {
    private string searchText = string.Empty;

    private IEnumerable<string> FilteredResults => 
        allItems.Where(x => x.Contains(searchText, StringComparison.OrdinalIgnoreCase));
    
    private List<string> allItems = ["苹果", "香蕉", "橙子", "葡萄", "西瓜"];
}

这里FilteredResults是一个计算属性(C# property with getter),每次Blazor重新渲染组件时都会重新求值。当searchText通过oninput事件更新后,Blazor触发重渲染,FilteredResults得到重新计算,UI随即显示过滤结果------这种响应式的数据推导方式简洁而直观。

五、总结

本章我们系统学习了Blazor组件的基础体系:.razor文件的指令区、模板区和@code代码块三段式结构;从OnInitializedOnParametersSet再到OnAfterRender的完整生命周期,以及IDisposable的资源清理实践;通过[Parameter]EventCallback实现父子组件之间的数据传递与事件通知;最后掌握了@if@foreach@bind等核心模板指令。这些要素构成了Blazor开发的日常基础,几乎每一个真实的组件都会综合运用它们。

随着应用规模的增长,单纯的父子组件参数传递会面临"层层传递"的困境------当一个深层嵌套的组件需要一份来自顶层的数据時,中间每一层组件都不得不转发这个参数,代码变得冗余且难以维护。下一章,我们将探讨更高级的组件通信与共享状态模式:级联参数如何在组件树中广播数据,以及依赖注入服务如何成为全局状态的真正载体。

相关推荐
海奥华22 小时前
Rust初步学习
开发语言·学习·rust
卢锡荣2 小时前
LDR6021Q 车规级 Type‑C PD 控制芯片:一芯赋能,边充边传,稳驭全场景
c语言·开发语言·ios·计算机外设·电脑
、BeYourself2 小时前
Scala 基础语法
开发语言·scala
AMoon丶2 小时前
C++模版-函数模版,类模版基础
java·linux·c语言·开发语言·jvm·c++·算法
SugarFreeOixi2 小时前
Matlab多个图窗重叠问题解决,平铺函数TileFigs
开发语言·matlab
ujainu2 小时前
Electron 实战:将用户输入保存到本地文件 —— 基于 `fs.writeFileSync` 与 IPC 的安全写入方案
javascript·安全·electron
进击的尘埃3 小时前
基于 LangChain.js 的前端 Agent 工作流编排:Tool 注册、思维链可视化与多步推理的实时 DAG 渲染
javascript
码不停蹄Zzz3 小时前
C语言【结构体值传递问题】
c语言·开发语言
AMoon丶3 小时前
Golang--多种数据结构详解
linux·c语言·开发语言·数据结构·c++·后端·golang