Blazor组件开发 dotnet-blazor-components

这个技能专注于Blazor组件开发,涵盖生命周期管理、状态管理、JavaScript互操作、表单验证和QuickGrid组件。它提供了针对不同渲染模式(如Static SSR、InteractiveServer、WASM)的行为差异指导,适用于.NET开发人员构建高效、交互式的Web应用程序。关键词:Blazor, .NET, 组件开发, 前端, 状态管理, JS互操作, EditForm, QuickGrid

前端开发 0 次安装 0 次浏览 更新于 3/6/2026

name: dotnet-blazor-components description: “实现Blazor组件。生命周期、状态管理、JS互操作、EditForm、QuickGrid。” user-invocable: false

dotnet-blazor-components

Blazor组件架构:生命周期方法、状态管理(级联值、依赖注入、浏览器存储)、JavaScript互操作(AOT安全)、EditForm验证和QuickGrid。涵盖相关渲染模式的行为差异。

范围

  • 组件生命周期方法(SetParametersAsync, OnInitialized, OnAfterRender)
  • 状态管理(级联值、依赖注入、浏览器存储)
  • JavaScript互操作(AOT安全模块导入)
  • EditForm验证和输入组件
  • QuickGrid数据绑定和虚拟化
  • 每渲染模式行为差异(Static SSR, InteractiveServer, WASM)

超出范围

  • 托管模型选择和渲染模式 – 参见 [skill:dotnet-blazor-patterns]
  • 认证组件(AuthorizeView, CascadingAuthenticationState) – 参见 [skill:dotnet-blazor-auth]
  • bUnit测试 – 参见 [skill:dotnet-blazor-testing]
  • 独立SignalR hub模式 – 参见 [skill:dotnet-realtime-communication]
  • 端到端测试 – 参见 [skill:dotnet-playwright]
  • UI框架选择 – 参见 [skill:dotnet-ui-chooser]
  • 可访问性模式(ARIA, 键盘导航) – 参见 [skill:dotnet-accessibility]

交叉引用: [skill:dotnet-blazor-patterns] 用于托管模型和渲染模式, [skill:dotnet-blazor-auth] 用于认证, [skill:dotnet-blazor-testing] 用于bUnit测试, [skill:dotnet-realtime-communication] 用于独立SignalR, [skill:dotnet-playwright] 用于端到端测试, [skill:dotnet-ui-chooser] 用于框架选择, [skill:dotnet-accessibility] 用于可访问性模式(ARIA, 键盘导航, 屏幕阅读器)。


组件生命周期

生命周期方法

@code {
    // 1. 当参数被设置或更新时调用
    public override async Task SetParametersAsync(ParameterView parameters)
    {
        // 在应用参数之前访问原始参数
        await base.SetParametersAsync(parameters);
    }

    // 2. 参数分配后调用(同步)
    protected override void OnInitialized()
    {
        // 一次性初始化(每个组件实例运行一次)
    }

    // 3. 参数分配后调用(异步)
    protected override async Task OnInitializedAsync()
    {
        // 异步初始化(数据获取、服务调用)
        products = await ProductService.GetProductsAsync();
    }

    // 4. 每次参数更改时调用
    protected override void OnParametersSet()
    {
        // 响应参数变化
    }

    // 5. 每次渲染后调用
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            // JS互操作安全在这里 -- DOM可用
        }
    }

    // 6. OnAfterRender的异步版本
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSRuntime.InvokeVoidAsync("initializeChart", chartElement);
        }
    }

    // 7. 清理
    public void Dispose()
    {
        // 取消事件订阅,释放资源
    }

    // 8. 异步清理
    public async ValueTask DisposeAsync()
    {
        // 异步清理(释放JS对象引用)
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

每渲染模式生命周期行为

生命周期事件 Static SSR InteractiveServer InteractiveWebAssembly InteractiveAuto Hybrid
OnInitialized(Async) 在服务器上运行 在服务器上运行 在浏览器中运行 首次加载在服务器上,WASM缓存后在浏览器中 在进程中运行
OnAfterRender(Async) 从不调用 服务器在SignalR确认渲染后运行 浏览器在DOM更新后运行 服务器端然后浏览器端(匹配活动运行时) 在WebView渲染后运行
Dispose(Async) 响应后调用 电路结束时调用 组件移除时调用 电路结束时(服务器阶段)或移除时(WASM阶段)调用 组件移除时调用

注意: 在Static SSR中,OnAfterRender 从不执行,因为没有持久连接。不要在Static SSR页面的 OnAfterRender 中放置关键逻辑。


状态管理

级联值

级联值在不显式传递参数的情况下,将数据沿组件树向下流动。

<!-- 父组件:提供级联值 -->
<CascadingValue Value="@theme" Name="AppTheme">
    <Router AppAssembly="typeof(App).Assembly">
        <!-- 所有后代组件可以接收AppTheme -->
    </Router>
</CascadingValue>

@code {
    private ThemeSettings theme = new() { IsDarkMode = false, AccentColor = "#0078d4" };
}
<!-- 子组件:消费级联值 -->
@code {
    [CascadingParameter(Name = "AppTheme")]
    public ThemeSettings? Theme { get; set; }
}

固定级联值(.NET 8+): 对于在初始渲染后永不更改的值,使用 IsFixed="true" 以避免重新渲染开销:

<CascadingValue Value="@config" IsFixed="true">
    <ChildComponent />
</CascadingValue>

依赖注入

// 在Program.cs中注册服务
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddSingleton<AppState>();

// 在组件中注入
@inject IProductService ProductService
@inject AppState State

每渲染模式DI生命周期行为:

生命周期 InteractiveServer InteractiveWebAssembly InteractiveAuto Hybrid
单例 服务器上所有电路共享 每个浏览器标签一个 服务器阶段共享;WASM切换后每标签 每个应用实例一个
作用域 每个电路一个(类似每用户) 每个浏览器标签一个(与单例相同) 每电路(服务器阶段),每标签(WASM阶段) – 状态不在阶段间传输 每个应用实例一个(与单例相同)
瞬态 每次注入新实例 每次注入新实例 每次注入新实例 每次注入新实例

注意: 在Blazor Server中,Scoped 服务在整个电路持续时间内存活(不像MVC中每请求)。电路持续到用户导航离开或连接断开。长期作用域服务可能积累状态 – 使用 OwningComponentBase<T> 用于组件作用域DI。

浏览器存储

// ProtectedBrowserStorage -- 加密的每用户存储
// 仅在InteractiveServer中可用(不是WASM -- 服务器加密/解密)
@inject ProtectedSessionStorage SessionStorage
@inject ProtectedLocalStorage LocalStorage

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // 会话存储(标签关闭时清除)
        await SessionStorage.SetAsync("cart", cartItems);
        var result = await SessionStorage.GetAsync<List<CartItem>>("cart");
        if (result.Success) { cartItems = result.Value!; }

        // 本地存储(跨会话持久)
        await LocalStorage.SetAsync("preferences", userPrefs);
    }
}

对于InteractiveWebAssembly,使用JS互操作直接访问浏览器存储:

// WASM:通过JS互操作直接浏览器存储
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key",
    JsonSerializer.Serialize(value, AppJsonContext.Default.UserPrefs));

var json = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "key");
if (json is not null)
{
    value = JsonSerializer.Deserialize(json, AppJsonContext.Default.UserPrefs);
}

注意: ProtectedBrowserStorage 在预渲染期间不可用。总是在 OnAfterRenderAsync(firstRender: true) 中访问它,绝不在 OnInitializedAsync 中。


JavaScript互操作

从.NET调用JavaScript

@inject IJSRuntime JSRuntime

// 调用全局JS函数
await JSRuntime.InvokeVoidAsync("console.log", "Hello from Blazor");

// 调用并获取返回值
var width = await JSRuntime.InvokeAsync<int>("getWindowWidth");

// 带超时(对于Server很重要,避免挂起电路)
var result = await JSRuntime.InvokeAsync<string>(
    "expensiveOperation",
    TimeSpan.FromSeconds(10),
    inputData);

JavaScript模块导入(AOT安全)

// 导入JS模块 -- 裁剪安全,无反射
private IJSObjectReference? module;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "import", "./js/interop.js");
        await module.InvokeVoidAsync("initialize", elementRef);
    }
}

// 总是释放模块引用
public async ValueTask DisposeAsync()
{
    if (module is not null)
    {
        await module.DisposeAsync();
    }
}
// wwwroot/js/interop.js
export function initialize(element) {
    // 设置元素
}

export function getValue(element) {
    return element.value;
}

从JavaScript调用.NET

// 实例方法回调
private DotNetObjectReference<MyComponent>? dotNetRef;

protected override void OnInitialized()
{
    dotNetRef = DotNetObjectReference.Create(this);
}

[JSInvokable]
public void OnJsEvent(string data)
{
    message = data;
    StateHasChanged();
}

public void Dispose()
{
    dotNetRef?.Dispose();
}
// 从JS调用.NET
export function registerCallback(dotNetRef) {
    document.addEventListener('custom-event', (e) => {
        dotNetRef.invokeMethodAsync('OnJsEvent', e.detail);
    });
}

每渲染模式JS互操作

关注点 InteractiveServer InteractiveWebAssembly InteractiveAuto Hybrid
JS调用时机 SignalR确认渲染后 WASM运行时加载后 初始通过SignalR,WASM切换后直接 WebView加载后
OnAfterRender 可用
IJSRuntime 同步调用 不支持(仅异步) IJSInProcessRuntime 可用 服务器阶段仅异步;WASM切换后 IJSInProcessRuntime 可用 IJSInProcessRuntime 可用
模块导入 通过SignalR(延迟) 直接(快速) SignalR(服务器阶段),直接(WASM阶段) 直接(快速)

注意: 在InteractiveServer中,所有JS互操作调用通过SignalR传输,增加网络延迟。通过将操作批处理到单个JS函数调用中来最小化往返次数。


EditForm验证

基本EditForm与数据注解

<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="product-form">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div>
        <label for="name">名称:</label>
        <InputText id="name" @bind-Value="product.Name" />
        <ValidationMessage For="() => product.Name" />
    </div>

    <div>
        <label for="price">价格:</label>
        <InputNumber id="price" @bind-Value="product.Price" />
        <ValidationMessage For="() => product.Price" />
    </div>

    <div>
        <label for="category">类别:</label>
        <InputSelect id="category" @bind-Value="product.Category">
            <option value="">选择...</option>
            <option value="Electronics">电子产品</option>
            <option value="Clothing">服装</option>
        </InputSelect>
        <ValidationMessage For="() => product.Category" />
    </div>

    <button type="submit">保存</button>
</EditForm>

@code {
    private ProductModel product = new();

    private async Task HandleSubmit()
    {
        await ProductService.CreateAsync(product);
        Navigation.NavigateTo("/products");
    }
}

带验证属性的模型

public sealed class ProductModel
{
    [Required(ErrorMessage = "产品名称是必需的")]
    [StringLength(200, MinimumLength = 1)]
    public string Name { get; set; } = "";

    [Range(0.01, 1_000_000, ErrorMessage = "价格必须在{1}和{2}之间")]
    public decimal Price { get; set; }

    [Required(ErrorMessage = "类别是必需的")]
    public string Category { get; set; } = "";
}

EditForm与增强表单处理(.NET 8+)

Static SSR表单需要 FormName 并使用 [SupplyParameterFromForm]

@page "/products/create"

<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="create-product" Enhance>
    <DataAnnotationsValidator />
    <!-- 表单字段 -->
    <button type="submit">创建</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private ProductModel product { get; set; } = new();

    private async Task HandleSubmit()
    {
        await ProductService.CreateAsync(product);
        Navigation.NavigateTo("/products");
    }
}

Enhance 属性启用增强表单处理 – 表单通过fetch提交并修补DOM,无需完整页面重载。

注意: FormName 必须在页面所有表单中唯一。重复的 FormName 值会导致表单提交错误。


QuickGrid

QuickGrid是Blazor内置的高性能网格组件(.NET 8+)。它支持排序、过滤、分页和虚拟化。

基本QuickGrid

@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid Items="products">
    <PropertyColumn Property="p => p.Name" Sortable="true" />
    <PropertyColumn Property="p => p.Price" Format="C2" Sortable="true" />
    <PropertyColumn Property="p => p.Category" Sortable="true" />
    <TemplateColumn Title="操作">
        <button @onclick="() => Edit(context)">编辑</button>
    </TemplateColumn>
</QuickGrid>

@code {
    private IQueryable<Product> products = Enumerable.Empty<Product>().AsQueryable();

    protected override async Task OnInitializedAsync()
    {
        var list = await ProductService.GetAllAsync();
        products = list.AsQueryable();
    }

    private void Edit(Product product) => Navigation.NavigateTo($"/products/{product.Id}/edit");
}

QuickGrid与分页

<QuickGrid Items="products" Pagination="pagination">
    <PropertyColumn Property="p => p.Name" Sortable="true" />
    <PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>

<Paginator State="pagination" />

@code {
    private PaginationState pagination = new() { ItemsPerPage = 20 };
    private IQueryable<Product> products = default!;
}

QuickGrid与虚拟化

对于大型数据集,虚拟化仅渲染可见行:

<QuickGrid Items="products" Virtualize="true" ItemSize="50">
    <PropertyColumn Property="p => p.Name" />
    <PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>

<!-- net11-preview -->

QuickGrid OnRowClick(.NET 11预览)

.NET 11添加了 OnRowClick 到QuickGrid,用于行级点击处理,无需模板列:

<QuickGrid Items="products" OnRowClick="HandleRowClick">
    <PropertyColumn Property="p => p.Name" />
    <PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>

@code {
    private void HandleRowClick(GridRowClickEventArgs<Product> args)
    {
        Navigation.NavigateTo($"/products/{args.Item.Id}");
    }
}

回退(net10.0): 使用带点击处理程序的 TemplateColumn 或将每行包装在可点击元素中。

来源: ASP.NET Core .NET 11 Preview - QuickGrid enhancements


<!-- net11-preview -->

.NET 11预览功能

EnvironmentBoundary组件

EnvironmentBoundary 根据托管环境(开发、暂存、生产)条件渲染内容:

<EnvironmentBoundary Include="Development">
    <p>调试面板 -- 仅在开发环境中可见</p>
    <DebugToolbar />
</EnvironmentBoundary>

<EnvironmentBoundary Exclude="Production">
    <p>测试控件 -- 在生产环境中隐藏</p>
</EnvironmentBoundary>

回退(net10.0): 注入 IWebHostEnvironment 并在 @code 中使用条件渲染。

来源: ASP.NET Core .NET 11 Preview - EnvironmentBoundary

Label和DisplayName支持

.NET 11添加了 [DisplayName] 支持输入组件,自动生成 <label> 元素:

<EditForm Model="model" FormName="contact">
    <!-- 自动从[DisplayName]渲染<label> -->
    <InputText @bind-Value="model.FullName" />
    <InputText @bind-Value="model.EmailAddress" />
</EditForm>

@code {
    private ContactModel model = new();
}

// 模型
public sealed class ContactModel
{
    [DisplayName("全名")]
    [Required]
    public string FullName { get; set; } = "";

    [DisplayName("电子邮件地址")]
    [EmailAddress]
    public string EmailAddress { get; set; } = "";
}

回退(net10.0): 手动添加显式 <label for="..."> 元素。

来源: ASP.NET Core .NET 11 Preview - Label/DisplayName

IHostedService在WebAssembly中

.NET 11允许 IHostedService 实现在Blazor WebAssembly中运行,启用浏览器中的后台任务:

// 在WASM Program.cs中注册
builder.Services.AddHostedService<DataSyncService>();

public sealed class DataSyncService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await SyncDataFromServer();
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

回退(net10.0): 在组件中使用 Timer 或注入在首次使用时启动后台工作的单例服务。

来源: ASP.NET Core .NET 11 Preview - IHostedService in WASM

<!-- net11-preview -->

SignalR ConfigureConnection

.NET 11添加了 ConfigureConnection 到Blazor Server电路hub,允许自定义SignalR连接(例如,添加自定义头、配置重新连接):

// Program.cs
app.MapBlazorHub(options =>
{
    options.ConfigureConnection = connection =>
    {
        connection.Metadata["tenant"] = "default";
    };
});

回退(net10.0): 使用 IHubFilter 或中间件在hub级别检查/修改连接。

来源: ASP.NET Core .NET 11 Preview - SignalR ConfigureConnection


代理注意事项

  1. 不要在 OnInitializedAsync 中调用JS互操作。 DOM尚不可用。对于需要DOM元素的JS调用,使用 OnAfterRenderAsync(firstRender: true)
  2. 不要忘记在外部状态更改后调用 StateHasChanged() 当状态从非Blazor上下文(定时器、事件处理程序、JS回调)更改时,调用 StateHasChanged()InvokeAsync(StateHasChanged) 以触发重新渲染。
  3. 不要在预渲染期间使用 ProtectedBrowserStorage 它抛出异常,因为没有交互式电路存在。仅在 OnAfterRenderAsync 中访问它。
  4. 不要忘记在Static SSR表单上使用 FormName 没有它,Static SSR模式中的表单提交不会路由到正确的处理程序。
  5. 不要过早处置 DotNetObjectReference 过早处置导致JavaScript尝试调用回调时抛出 JSException。在 Dispose()DisposeAsync() 中处置。
  6. 不要假设在Blazor Server中作用域服务是每请求的。 作用域服务在整个电路持续时间内存活。当您需要组件作用域服务生命周期时,使用 OwningComponentBase<T>

先决条件

  • .NET 8.0+(QuickGrid、增强表单处理、带 IsFixed 的级联值)
  • Microsoft.AspNetCore.Components.QuickGrid 包用于QuickGrid
  • .NET 11预览用于EnvironmentBoundary、Label/DisplayName、QuickGrid OnRowClick、WASM中的IHostedService

知识来源

此技能中的Blazor组件模式基于以下指导:

  • Damian Edwards – Razor和Blazor组件设计模式、渲染模式架构和性能最佳实践。ASP.NET团队的首席架构师。

这些来源为上述模式和原理提供了信息。此技能不代表或代言任何个人。


参考文献