关于 dotnet-blazor-patterns
这项技能提供 Blazor 项目设置与实现模式,帮助 .NET 开发者选择托管模型并配置渲染、路由和导航,也涵盖流式渲染以及适用于 WebAssembly AOT 和裁剪的模式。
主要功能
- 01五种 Blazor 托管与渲染模式
- 02项目设置和路由模式
- 03增强导航与流式渲染
- 04兼容 AOT 和裁剪的编码模式
- 05预渲染与状态持久化
适用场景
- 01为应用选择 Blazor 托管模型
- 02在应用、页面或组件级配置渲染
- 03渲染数据源响应较慢的页面
- 04为 WebAssembly 应用准备 AOT 或裁剪
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-blazor-patterns安装后在 Claude Code 中输入/dotnet-blazor-patterns确认已加载。
详细介绍
dotnet-blazor-patterns
Blazor 托管模型、渲染模式、项目设置、路由、增强导航、流式渲染和 AOT 安全模式。涵盖全部五种托管模型(InteractiveServer、InteractiveWebAssembly、InteractiveAuto、Static SSR、Hybrid),并分析各自的权衡。
范围边界: 此技能负责 Blazor 项目设置、托管模型选择、渲染模式配置、路由、增强导航、流式渲染和 AOT 安全模式。组件架构(生命周期、状态管理、JS 互操作、EditForm)由 [skill:dotnet-blazor-components] 负责。跨托管模型的身份验证由 [skill:dotnet-blazor-auth] 负责。
不在范围内: bUnit 组件测试——请参阅 [skill:dotnet-blazor-testing]。独立 SignalR 模式——请参阅 [skill:dotnet-realtime-communication]。基于浏览器的 E2E 测试——请参阅 [skill:dotnet-playwright]。UI 框架选择决策树——请参阅 [skill:dotnet-ui-chooser]。
交叉引用:[skill:dotnet-blazor-components](组件架构)、[skill:dotnet-blazor-auth](身份验证)、[skill:dotnet-blazor-testing](bUnit 测试)、[skill:dotnet-realtime-communication](独立 SignalR)、[skill:dotnet-playwright](E2E 测试)、[skill:dotnet-ui-chooser](框架选择)、[skill:dotnet-accessibility](无障碍模式,包括 ARIA、键盘导航和屏幕阅读器)。
托管模型与渲染模式
Blazor Web App(.NET 8+)是默认项目模板,取代了独立的 Blazor Server 和 Blazor WebAssembly 模板。渲染模式可以全局设置,也可以按页面或组件设置。
渲染模式概览
| 渲染模式 | 属性 | 交互性 | 连接 | 最适合 |
|---|---|---|---|---|
| Static SSR | (无/默认) | 无——服务器渲染 HTML,不具备交互性 | 仅 HTTP 请求 | 内容页面、SEO、交互性要求较低的表单 |
| InteractiveServer | @rendermode InteractiveServer | 完整 | SignalR circuit | 低延迟交互、完整服务器访问权限、用户规模较小 |
| InteractiveWebAssembly | @rendermode InteractiveWebAssembly | 完整(下载后) | 无(在浏览器中运行) | 支持离线、用户规模较大、降低服务器负载 |
| InteractiveAuto | @rendermode InteractiveAuto | 完整 | 初始使用 SignalR,之后使用 WASM | 兼具两者优势——立即交互,最终转为客户端运行 |
| Blazor Hybrid | MAUI/WPF/WinForms 中的 BlazorWebView | 完整(原生) | 无(在进程内运行) | 使用 Web UI 的桌面/移动应用、访问原生 API |
各模式的权衡
| 关注点 | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|---|
| 首次加载 | 快 | 快 | 慢(下载 WASM) | 快(先使用 Server) | 即时(本地) |
| 服务器资源 | 极少 | 每位用户一个 circuit | 下载后无需 | 先使用 circuit,之后无需 | 无 |
| 离线支持 | 否 | 否 | 是 | 部分支持 | 是 |
| 完整 .NET API 访问权限 | 有(服务器端) | 有(服务器端) | 受限(浏览器沙箱) | 因阶段而异 | 有(原生) |
| 可扩展性 | 高 | 受 circuit 数量限制 | 高 | 高(WASM 启动后) | 不适用(本地) |
| SEO | 支持 | 预渲染 | 预渲染 | 预渲染 | 不适用 |
设置渲染模式
全局设置(App.razor):
<Routes @rendermode="InteractiveServer" />
按页面设置:
@page "/dashboard"
@rendermode InteractiveServer
<h1>Dashboard</h1>
按组件设置:
<Counter @rendermode="InteractiveWebAssembly" />
注意: 如果没有显式的渲染模式边界,子组件不能请求比父组件更具交互性的渲染模式。不过,交互式岛屿受到支持:你可以在嵌入 Static SSR 页面中的组件上放置 @rendermode 属性,以创建渲染模式边界,从而让静态内容中包含交互式子组件。
项目设置
Blazor Web App(默认模板)
# Creates a Blazor Web App with InteractiveServer render mode
dotnet new blazor -n MyApp
# With specific interactivity options
dotnet new blazor -n MyApp --interactivity Auto # InteractiveAuto
dotnet new blazor -n MyApp --interactivity WebAssembly # InteractiveWebAssembly
dotnet new blazor -n MyApp --interactivity Server # InteractiveServer (default)
dotnet new blazor -n MyApp --interactivity None # Static SSR only
Blazor Web App 项目结构
MyApp/
MyApp/ # Server project
Program.cs # Host builder, services, middleware
Components/
App.razor # Root component (sets global render mode)
Routes.razor # Router component
Layout/
MainLayout.razor # Main layout
Pages/
Home.razor # Static SSR by default
Counter.razor # Can set per-page render mode
MyApp.Client/ # Client project (only if WASM or Auto)
Pages/
Counter.razor # Components that run in browser
Program.cs # WASM entry point
使用 InteractiveAuto 或 InteractiveWebAssembly 时,必须在浏览器中运行的组件应放在 .Client 项目中。服务器项目中的组件仅在服务器上运行。
Blazor Hybrid 设置(MAUI)
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
</PropertyGroup>
</Project>
// MainPage.xaml.cs hosts BlazorWebView
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">
<b:BlazorWebView HostPage="wwwroot/index.html">
<b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />
</b:BlazorWebView.RootComponents>
</b:BlazorWebView>
</ContentPage>
路由
基本路由
@page "/products"
@page "/products/{Category}"
<h1>Products</h1>
@if (!string.IsNullOrEmpty(Category))
{
<p>Category: @Category</p>
}
@code {
[Parameter]
public string? Category { get; set; }
}
路由约束
@page "/products/{Id:int}"
@page "/orders/{Date:datetime}"
@page "/search/{Query:minlength(3)}"
@code {
[Parameter] public int Id { get; set; }
[Parameter] public DateTime Date { get; set; }
[Parameter] public string Query { get; set; } = "";
}
查询字符串参数
@page "/search"
@code {
[SupplyParameterFromQuery]
public string? Term { get; set; }
[SupplyParameterFromQuery(Name = "page")]
public int CurrentPage { get; set; } = 1;
}
NavigationManager
@inject NavigationManager Navigation
// Programmatic navigation
Navigation.NavigateTo("/products/electronics");
// With query string
Navigation.NavigateTo("/search?term=keyboard&page=2");
// Force full page reload (bypasses enhanced navigation)
Navigation.NavigateTo("/external-page", forceLoad: true);
增强导航(.NET 8+)
增强导航会拦截链接点击和表单提交,仅更新发生变化的 DOM 内容,从而保留页面状态并避免完整页面重新加载。此功能适用于静态 SSR 和预渲染页面。
工作原理
- 用户点击 Blazor 应用中的链接
- Blazor 拦截导航
- 通过 fetch 请求加载新页面内容
- Blazor 仅将差异部分修补到 DOM 中
- 保留滚动位置和焦点状态
选择退出
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>
<form method="post" data-enhance="false">
...
</form>
注意: 增强导航可能会干扰那些要求完整页面加载的第三方 JavaScript 库。对于导航到包含在 DOMContentLoaded 时初始化的 JS 的页面的链接,请使用 data-enhance-nav="false"。
流式渲染(.NET 8+)
流式渲染会立即发送初始 HTML(包含占位内容),然后在异步操作完成时流式传输更新内容。适用于数据源响应缓慢的页面。
@page "/dashboard"
@attribute [StreamRendering]
<h1>Dashboard</h1>
@if (orders is null)
{
<p>Loading orders...</p>
}
else
{
<table>
@foreach (var order in orders)
{
<tr><td>@order.Id</td><td>@order.Total</td></tr>
}
</table>
}
@code {
private List<OrderDto>? orders;
protected override async Task OnInitializedAsync()
{
// Initial HTML sent immediately with "Loading orders..."
// Updated HTML streamed when this completes
orders = await OrderService.GetRecentOrdersAsync();
}
}
各渲染模式下的行为:
- 静态 SSR: 流式渲染会发送初始响应,然后通过分块传输编码修补 DOM。页面不可交互。
- InteractiveServer/WebAssembly/Auto: 流式渲染的效果较小,因为组件会在异步操作后自动重新渲染。
[StreamRendering]特性主要有利于预渲染阶段。
AOT 安全模式
以 Blazor WebAssembly 为目标并使用 Native AOT(预先编译)或 IL 修剪时,应避免依赖运行时反射的模式。
优先使用源生成器进行序列化
// CORRECT: Source-generated JSON serialization (AOT-compatible)
[JsonSerializable(typeof(ProductDto))]
[JsonSerializable(typeof(List<ProductDto>))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext { }
// Register in Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
// Usage in HttpClient calls
var products = await Http.GetFromJsonAsync<List<ProductDto>>(
"/api/products",
AppJsonContext.Default.ListProductDto);
// WRONG: Reflection-based serialization (fails under AOT/trimming)
var products = await Http.GetFromJsonAsync<List<ProductDto>>("/api/products");
修剪安全的 JS 互操作
// CORRECT: Use IJSRuntime with explicit method names (no dynamic dispatch)
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key", "value");
var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key");
// CORRECT: Use IJSObjectReference for module imports (.NET 8+)
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/chart.js");
await module.InvokeVoidAsync("initChart", elementRef, data);
await module.DisposeAsync();
// WRONG: Dynamic dispatch via reflection (trimmed away)
// var method = typeof(JSRuntime).GetMethod("InvokeAsync");
// method.MakeGenericMethod(returnType).Invoke(...)
链接器配置
<ItemGroup>
<TrimmerRootAssembly Include="MyApp.Client" />
</ItemGroup>
对于必须避免被修剪的类型:
// Mark types that are accessed via reflection
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class DynamicFormModel
{
// Properties discovered at runtime for form generation
public string Name { get; set; } = "";
public int Age { get; set; }
}
应避免的反模式
- 基于反射的 DI —— 不要使用
Activator.CreateInstance或Type.GetType来解析服务。请使用内置 DI 容器并显式注册。 - 动态加载类型 —— 不要在运行时使用
Assembly.Load或Assembly.GetTypes()。请在启动时注册所有类型。 - 运行时代码生成 —— 不要使用
System.Reflection.Emit或System.Linq.Expressions.Expression.Compile()。请改用源生成器。 - 非类型化 JSON 反序列化 —— 不要在没有
JsonSerializerContext的情况下使用JsonSerializer.Deserialize<T>(json)。请务必提供源生成的上下文。
预渲染
预渲染会在交互式运行时加载之前于服务器上生成 HTML。这能提升感知性能和 SEO 效果。
使用交互模式进行预渲染
<Counter @rendermode="InteractiveServer" />
默认情况下,交互式组件会进行预渲染。若要禁用:
@rendermode @(new InteractiveServerRenderMode(prerender: false))
保留预渲染期间的状态
组件在预渲染期间计算的状态会在组件以交互方式重新初始化时丢失。使用 PersistentComponentState 保留状态:
@inject PersistentComponentState ApplicationState
@implements IDisposable
@code {
private List<ProductDto>? products;
private PersistingComponentStateSubscription _subscription;
protected override async Task OnInitializedAsync()
{
_subscription = ApplicationState.RegisterOnPersisting(PersistState);
if (!ApplicationState.TryTakeFromJson<List<ProductDto>>(
"products", out var restored))
{
products = await ProductService.GetProductsAsync();
}
else
{
products = restored;
}
}
private Task PersistState()
{
ApplicationState.PersistAsJson("products", products);
return Task.CompletedTask;
}
public void Dispose() => _subscription.Dispose();
}
.NET 10 稳定功能
检测到 net10.0 TFM 时即可使用这些功能。它们已稳定,无需选择启用预览功能。
WebAssembly 预加载
.NET 10 新增了在 InteractiveAuto 的 Server 阶段通过 blazor.web.js 预加载 WebAssembly 程序集的功能。用户首次加载页面时,WASM 运行时和应用程序集会在后台下载,同时 Server circuit 负责处理交互。由于程序集已缓存,后续导航切换到 WASM 的速度会更快。
增强表单验证
.NET 10 扩展了 EditForm 验证功能,改进了错误消息格式,并支持在静态 SSR 表单中使用 IValidatableObject。使用增强表单处理(Enhance 特性)时,验证消息可正确呈现,无需完整重新加载页面。
// IValidatableObject works in Static SSR enhanced forms in .NET 10
public sealed class OrderModel : IValidatableObject
{
[Required]
public string ProductId { get; set; } = "";
[Range(1, 100)]
public int Quantity { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (ProductId == "DISCONTINUED" && Quantity > 0)
{
yield return new ValidationResult(
"Cannot order discontinued products",
[nameof(ProductId), nameof(Quantity)]);
}
}
}
Blazor 诊断中间件
.NET 10 新增了 MapBlazorDiagnostics 中间件,可在开发期间检查 Blazor 电路和组件状态:
// Program.cs -- available in .NET 10
if (app.Environment.IsDevelopment())
{
app.MapBlazorDiagnostics(); // Exposes /_blazor/diagnostics endpoint
}
诊断端点会显示活动电路、组件树、渲染模式分配情况和计时数据。可在开发期间使用它来调试渲染模式边界和组件生命周期问题。
Agent 注意事项
- 不要将 InteractiveServer 设为所有页面的默认模式。 静态 SSR 是默认且效率最高的渲染模式。仅在用户交互需要时才添加交互功能。
- 不要将面向 WASM 的组件放入服务器项目。 必须在浏览器中运行的组件(InteractiveWebAssembly 或 InteractiveAuto)应放在
.Client项目中。 - 预渲染时不要忘记使用
PersistentComponentState。 否则,预渲染期间获取的数据会被丢弃,并在组件变为交互式后重新获取,导致明显闪烁。 - 不要在 WASM 中使用基于反射的序列化。 始终使用
JsonSerializerContext和源生成的序列化器,以确保兼容 AOT 并避免修剪问题。 - 除非要离开 Blazor 应用,否则不要强制加载导航。
NavigateTo("/page", forceLoad: true)会绕过增强导航并导致整页重新加载。 - 不要错误地嵌套交互式渲染模式。 子组件无法请求比其父组件更具交互性的模式。请从布局开始规划渲染模式边界。
先决条件
- .NET 8.0+(Blazor Web App 模板、渲染模式、增强导航、流式渲染)
- .NET 10.0,用于稳定的增强功能(WebAssembly 预加载、增强表单验证、诊断中间件)
- 用于 Blazor Hybrid 的 MAUI 工作负载(
dotnet workload install maui)