名称: dotnet-blazor-patterns 描述: “构建Blazor应用。托管模型、渲染模式、路由、流式渲染、预渲染。” 用户可调用: false
dotnet-blazor-模式
Blazor托管模型、渲染模式、项目设置、路由、增强导航、流式渲染和AOT安全模式。涵盖所有五种托管模型(InteractiveServer、InteractiveWebAssembly、InteractiveAuto、Static SSR、Hybrid),并提供每种模型的权衡分析。
范围
- Blazor Web应用项目设置和配置
- 托管模型选择(Server、WASM、Auto、SSR、Hybrid)
- 渲染模式配置(全局、每页、每组件)
- 路由和增强导航
- 流式渲染和预渲染
- AOT安全Blazor模式
范围外
- 组件架构(生命周期、状态、JS互操作)–参见[技能:dotnet-blazor-components]
- 跨托管模型的身份验证–参见[技能:dotnet-blazor-auth]
- bUnit组件测试–参见[技能:dotnet-blazor-testing]
- 独立SignalR模式–参见[技能:dotnet-realtime-communication]
- 基于浏览器的端到端测试–参见[技能:dotnet-playwright]
- UI框架选择决策树–参见[技能:dotnet-ui-chooser]
交叉引用:[技能:dotnet-blazor-components]用于组件架构,[技能:dotnet-blazor-auth]用于身份验证,[技能:dotnet-blazor-testing]用于bUnit测试,[技能:dotnet-realtime-communication]用于独立SignalR,[技能:dotnet-playwright]用于端到端测试,[技能:dotnet-ui-chooser]用于框架选择,[技能:dotnet-accessibility]用于无障碍模式(ARIA、键盘导航、屏幕阅读器)。
托管模型与渲染模式
Blazor Web应用(.NET 8+)是默认项目模板,取代了独立的Blazor Server和Blazor WebAssembly模板。渲染模式可以全局设置、每页设置或每组件设置。
渲染模式概述
| 渲染模式 | 属性 | 交互性 | 连接 | 最佳用途 |
|---|---|---|---|---|
| 静态SSR | (无/默认) | 无–服务器渲染HTML,无交互性 | 仅HTTP请求 | 内容页面、SEO、交互性最小的表单 |
| InteractiveServer | @rendermode InteractiveServer |
完整 | SignalR电路 | 低延迟交互性、完全服务器访问、小用户群 |
| InteractiveWebAssembly | @rendermode InteractiveWebAssembly |
完整(下载后) | 无(在浏览器中运行) | 离线能力、大用户群、减少服务器负载 |
| InteractiveAuto | @rendermode InteractiveAuto |
完整 | 初始SignalR,然后WASM | 最佳结合–即时交互性,最终客户端侧 |
| Blazor Hybrid | MAUI/WPF/WinForms中的BlazorWebView |
完整(原生) | 无(在进程中运行) | 具有Web UI的桌面/移动应用,原生API访问 |
每种模式的权衡
| 关注点 | 静态SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|---|
| 首次加载 | 快速 | 快速 | 慢(WASM下载) | 快速(服务器优先) | 即时(本地) |
| 服务器资源 | 最小 | 每用户电路 | 下载后无 | 电路然后无 | 无 |
| 离线支持 | 否 | 否 | 是 | 部分 | 是 |
| 完全.NET API访问 | 是(服务器) | 是(服务器) | 有限(浏览器沙盒) | 按阶段变化 | 是(原生) |
| 可扩展性 | 高 | 受电路限制 | 高 | 高(WASM后) | 不适用(本地) |
| SEO | 是 | 预渲染 | 预渲染 | 预渲染 | 不适用 |
设置渲染模式
全局(App.razor):
<!-- 为所有页面设置默认渲染模式 -->
<Routes @rendermode="InteractiveServer" />
每页:
@page "/dashboard"
@rendermode InteractiveServer
<h1>仪表板</h1>
每组件:
<Counter @rendermode="InteractiveWebAssembly" />
注意事项: 没有显式的渲染模式边界时,子组件不能请求比其父组件更交互的渲染模式。然而,支持交互式岛屿:您可以在静态SSR页面上嵌入的组件上放置@rendermode属性来创建渲染模式边界,从而在静态内容下启用交互式子组件。
项目设置
Blazor Web应用(默认模板)
# 使用InteractiveServer渲染模式创建Blazor Web应用
dotnet new blazor -n MyApp
# 带特定交互性选项
dotnet new blazor -n MyApp --interactivity Auto # InteractiveAuto
dotnet new blazor -n MyApp --interactivity WebAssembly # InteractiveWebAssembly
dotnet new blazor -n MyApp --interactivity Server # InteractiveServer(默认)
dotnet new blazor -n MyApp --interactivity None # 仅静态SSR
Blazor Web应用项目结构
MyApp/
MyApp/ # 服务器项目
Program.cs # 主机构建器、服务、中间件
Components/
App.razor # 根组件(设置全局渲染模式)
Routes.razor # 路由器组件
Layout/
MainLayout.razor # 主布局
Pages/
Home.razor # 默认静态SSR
Counter.razor # 可设置每页渲染模式
MyApp.Client/ # 客户端项目(仅当WASM或Auto时)
Pages/
Counter.razor # 在浏览器中运行的组件
Program.cs # WASM入口点
使用InteractiveAuto或InteractiveWebAssembly时,必须在浏览器中运行的组件放入.Client项目。服务器项目中的组件仅在服务器上运行。
Blazor Hybrid设置(MAUI)
<!-- MAUI Blazor Hybrid的.csproj -->
<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托管BlazorWebView
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
<!-- MainPage.xaml -->
<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>产品</h1>
@if (!string.IsNullOrEmpty(Category))
{
<p>类别: @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
// 程序化导航
Navigation.NavigateTo("/products/electronics");
// 带查询字符串
Navigation.NavigateTo("/search?term=keyboard&page=2");
// 强制完整页面重载(绕过增强导航)
Navigation.NavigateTo("/external-page", forceLoad: true);
增强导航(.NET 8+)
增强导航拦截链接点击和表单提交,以仅更新更改的DOM内容,保留页面状态并避免完整页面重载。这适用于静态SSR和预渲染页面。
工作原理
- 用户在Blazor应用内点击链接
- Blazor拦截导航
- 获取请求加载新页面内容
- Blazor仅用差异修补DOM
- 滚动位置和焦点状态被保留
退出
<!-- 为特定链接禁用增强导航 -->
<a href="/legacy-page" data-enhance-nav="false">旧页面</a>
<!-- 为特定表单禁用增强表单处理 -->
<form method="post" data-enhance="false">
...
</form>
注意事项: 增强导航可能会干扰期望完整页面加载的第三方JavaScript库。在导航到在DOMContentLoaded上初始化的JS页面时,使用data-enhance-nav="false"。
流式渲染(.NET 8+)
流式渲染立即发送初始HTML(带有占位符内容),然后在异步操作完成时流式更新。适用于有慢数据源的页面。
@page "/dashboard"
@attribute [StreamRendering]
<h1>仪表板</h1>
@if (orders is null)
{
<p>加载订单中...</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()
{
// 立即发送带有“加载订单中...”的初始HTML
// 当此完成时流式更新HTML
orders = await OrderService.GetRecentOrdersAsync();
}
}
每种渲染模式的行为:
- 静态SSR: 流式渲染发送初始响应,然后通过分块传输编码修补DOM。页面不交互。
- InteractiveServer/WebAssembly/Auto: 流式渲染影响较小,因为组件在异步操作后自动重新渲染。
[StreamRendering]属性主要受益于预渲染阶段。
AOT安全模式
当针对Blazor WebAssembly使用原生AOT(提前编译)或IL修剪时,避免依赖运行时反射的模式。
源生成器优先的序列化
// 正确:源生成的JSON序列化(AOT兼容)
[JsonSerializable(typeof(ProductDto))]
[JsonSerializable(typeof(List<ProductDto>))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext { }
// 在Program.cs中注册
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
// 在HttpClient调用中使用
var products = await Http.GetFromJsonAsync<List<ProductDto>>(
"/api/products",
AppJsonContext.Default.ListProductDto);
// 错误:基于反射的序列化(AOT/修剪下失败)
var products = await Http.GetFromJsonAsync<List<ProductDto>>("/api/products");
修剪安全的JS互操作
// 正确:使用IJSRuntime和显式方法名(无动态分派)
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key", "value");
var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key");
// 正确:使用IJSObjectReference进行模块导入(.NET 8+)
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/chart.js");
await module.InvokeVoidAsync("initChart", elementRef, data);
await module.DisposeAsync();
// 错误:通过反射的动态分派(被修剪掉)
// var method = typeof(JSRuntime).GetMethod("InvokeAsync");
// method.MakeGenericMethod(returnType).Invoke(...)
链接器配置
<!-- 保留在组件中动态使用的类型 -->
<ItemGroup>
<TrimmerRootAssembly Include="MyApp.Client" />
</ItemGroup>
对于必须从修剪中保留的类型:
// 标记通过反射访问的类型
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class DynamicFormModel
{
// 运行时发现的表单生成属性
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反序列化 – 不要使用
JsonSerializer.Deserialize<T>(json)而不带JsonSerializerContext。始终提供源生成的上下文。
预渲染
预渲染在交互式运行时加载之前在服务器上生成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的服务器阶段添加blazor.web.js预加载WebAssembly程序集。当用户首次加载页面时,WASM运行时和应用程序集在后台下载,同时服务器电路处理交互性。后续导航更快地切换到WASM,因为程序集已缓存。
<!-- 无需代码更改--预加载在.NET 10中是自动的 -->
<!-- 在浏览器DevTools网络选项卡中验证:程序集在服务器阶段下载 -->
增强表单验证
.NET 10扩展EditForm验证,改进错误消息格式化,并在静态SSR表单中支持IValidatableObject。验证消息与增强表单处理(Enhance属性)正确渲染,无需完整页面重载。
// IValidatableObject在.NET 10的静态SSR增强表单中工作
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(
"无法订购已停产的产品",
[nameof(ProductId), nameof(Quantity)]);
}
}
}
Blazor诊断中间件
.NET 10添加MapBlazorDiagnostics中间件,用于在开发中检查Blazor电路和组件状态:
// Program.cs -- 在.NET 10中可用
if (app.Environment.IsDevelopment())
{
app.MapBlazorDiagnostics(); // 暴露/_blazor/diagnostics端点
}
诊断端点显示活动电路、组件树、渲染模式分配和计时数据。用于调试开发期间的渲染模式边界和组件生命周期问题。
代理注意事项
- 不要默认将每个页面设置为InteractiveServer。 静态SSR是默认且最高效的渲染模式。仅在用户交互需要时添加交互性。
- 不要把WASM目标组件放在服务器项目中。 必须在浏览器中运行的组件(InteractiveWebAssembly或InteractiveAuto)属于
.Client项目。 - 预渲染时不要忘记
PersistentComponentState。 没有它,预渲染期间获取的数据被丢弃,并在组件变得交互式时重新获取,导致可见闪烁。 - 不要在WASM中使用基于反射的序列化。 始终使用
JsonSerializerContext和源生成的序列化器以确保AOT兼容性和修剪安全。 - 除非离开Blazor应用,否则不要强制加载导航。
NavigateTo("/page", forceLoad: true)绕过增强导航并导致完整页面重载。 - 不要错误嵌套交互式渲染模式。 子组件不能请求比其父组件更交互的模式。从布局向下规划渲染模式边界。
先决条件
- .NET 8.0+(Blazor Web应用模板、渲染模式、增强导航、流式渲染)
- .NET 10.0用于稳定增强功能(WebAssembly预加载、增强表单验证、诊断中间件)
- MAUI工作负载用于Blazor Hybrid(
dotnet workload install maui)