Blazor 从零开始:第 3 章 —— 可扩展的组件设计

· 2 分钟阅读

欢迎来到 Blazor 从零开始 第 3 章。如果你还没看 第 2 章,建议先看完并准备好基础项目。

第 2 章我们让应用跑起来了;第 3 章我们要让它 更易维护

当每个页面都变成巨大 .razor 文件时,Blazor 项目会很快失控。组件化可以避免这一点:更一致、更可复用、边界更清晰。


Blazor 组件到底是什么

组件本质上是一个 .razor 文件,它可以:

  • 渲染标记
  • 保存本地状态
  • 通过参数接收输入
  • 向父组件发出事件
  • 渲染子内容

运行时,Blazor 会把每个组件当作一个小型状态机。状态变化后,框架会重新渲染并应用 DOM 差异。

所以你的目标是:输入清晰、行为可预测。


第一步:从职责单一的组件开始

创建 Components/Common/SectionHeader.razor

<header class="section-header">
    <h2>@Title</h2>
    @if (!string.IsNullOrWhiteSpace(Subtitle))
    {
        <p>@Subtitle</p>
    }
</header>

@code {
    [Parameter] public string Title { get; set; } = string.Empty;
    [Parameter] public string? Subtitle { get; set; }
}

Components/Pages/Home.razor 中使用:

@page "/"

<PageTitle>Blazor 从零开始</PageTitle>

<SectionHeader
    Title="Blazor 从零开始"
    Subtitle="第 3 章聚焦组件设计。" />

这个例子很小,但核心思想很关键:只看参数就应该能理解组件用途。


第二步:用参数显式表达行为

我们做一个可复用按钮组件。

Components/Common/AppButton.razor

<button class="app-button @VariantCssClass" @onclick="OnClick">
    @Text
</button>

@code {
    [Parameter] public string Text { get; set; } = "Button";
    [Parameter] public string Variant { get; set; } = "primary";
    [Parameter] public EventCallback OnClick { get; set; }

    private string VariantCssClass => Variant.ToLowerInvariant() switch
    {
        "secondary" => "app-button--secondary",
        "danger" => "app-button--danger",
        _ => "app-button--primary"
    };
}

用法:

@code {
    private int _savedCount;

    private void Save()
    {
        _savedCount++;
    }
}

<AppButton Text="Save" Variant="primary" OnClick="Save" />
<p>已保存 @_savedCount 次。</p>

重点在于“契约设计”:

  • 参数要少且清晰
  • 命名要显式,避免“魔法行为”
  • 默认值要安全

第三步:用 RenderFragment 做布局组合

RenderFragment 允许父组件把一段 UI 传给子组件。

创建 Components/Common/Card.razor

<article class="card">
    <header class="card__header">@Title</header>
    <section class="card__body">
        @ChildContent
    </section>
</article>

@code {
    [Parameter] public string Title { get; set; } = string.Empty;
    [Parameter] public RenderFragment? ChildContent { get; set; }
}

用法:

<Card Title="Roadmap">
    <ul>
        <li>组件</li>
        <li>Data binding</li>
        <li>Routing</li>
    </ul>
</Card>

这个模式可以在不重复外层包装标记的前提下,保持页面结构一致。


第四步:优先组合,而不是巨型页面

当页面持续变大时,按职责拆分:

  • ProfileSummary:顶部信息块
  • ProfileStats:指标展示
  • ProfileActivityList:最近活动

这样页面就变成“编排层”,而不是塞满实现细节的地方。


第五步:保持标记与逻辑的平衡

简单组件使用内联 @code 就够了。

复杂组件建议使用 code-behind:

  • UserCard.razor
  • UserCard.razor.cs

这样既能保持标记可读,也能让 C# 逻辑更清晰。


第六步:实用的目录结构

一个较容易扩展的结构:

  • Components/Pages/ -> 可路由页面
  • Components/Layout/ -> 应用壳层与导航
  • Components/Common/ -> 通用共享组件
  • Components/Features/<FeatureName>/ -> 功能域组件

Blazor 初期常见问题

  • 参数过多,而不是抽出专用模型
  • 把业务规则直接写进页面组件
  • 做出一个数百行的“上帝组件”
  • 重复粘贴标记,而不是提取可复用组件

下一章

第 4 章我们将进入 数据绑定与事件@bind、事件处理、双向绑定的权衡,以及如何让状态保持可预测。