.NET 9 和 .NET 10 中的 Blazor 交互:完整指南
如果您在过去几年中一直使用 Blazor 构建 Web 应用程序,您就会知道该框架已经取得了“长”的进步。最初是在 Blazor Server 和 Blazor WebAssembly 之间进行选择,现已发展成为统一、灵活的渲染模型,让您可以为应用程序中的每个组件选择正确的交互策略。
在 .NET 8 中,我们实现了根本性的转变——引入渲染模式和默认的静态服务器端渲染 (SSR)。现在,.NET 9 和 .NET 10 建立在该基础之上,并进行了改进,使开发人员体验更流畅,最终用户体验更快。
在这篇文章中,我想向您介绍 Blazor 交互性的全貌:渲染模式如何工作、流式 SSR 带来了什么、增强的导航和表单处理如何改变游戏,以及最新版本中的新增功能。如果您正在计划一个新的 Blazor 项目或考虑升级,那么当我开始深入研究这一切时,我希望我能拥有这本指南。
快速回顾:我们是如何走到这一步的
在 .NET 8 之前,您必须在项目级别致力于托管模型。 Blazor 服务器意味着一切都通过 SignalR 连接在服务器上运行。 Blazor WebAssembly 意味着一切都在浏览器中运行。每种方法都需要权衡,混合它们是痛苦的。
.NET 8 通过引入统一这两种模型的单个项目模板(Blazor Web App)改变了游戏规则。关键概念是渲染模式:您决定每个组件它应该如何渲染以及交互发生在哪里。默认设置为静态 SSR,这意味着组件在服务器上渲染并将纯 HTML 发送到浏览器 - 没有 SignalR,没有 WebAssembly,只有快速 HTML。
.NET 9 完善了这些概念,改善了开发人员体验并优化了性能。 .NET 10 通过更好的重新连接处理、跨渲染模式的持久组件状态以及对 Blazor 脚本本身交付方式的改进,进一步实现了这一点。
让我们把这一切分解一下。
.NET 9 中的渲染模式
现代 Blazor 的核心是渲染模式的概念。您应该了解四种模式:
1.静态SSR(默认)
创建新的 Blazor Web 应用程序时,组件默认在服务器上静态呈现。服务器处理 Razor 组件,生成 HTML,并将其发送到浏览器。没有持久连接,没有 WebAssembly 运行时——只有传统的请求/响应。
这非常适合内容丰富的页面、主要显示数据的仪表板或任何不需要实时交互的页面。
@page "/products"
<h1>Our Products</h1>
@foreach (var product in products)
{
<div class="product-card">
<h3>@product.Name</h3>
<p>@product.Description</p>
<span class="price">@product.Price.ToString("C")</span>
</div>
}
@code {
private List<Product> products = new();
protected override async Task OnInitializedAsync()
{
products = await ProductService.GetAllAsync();
}
}
该组件在服务器上呈现,生成 HTML,仅此而已。没有持续的连接。初始加载速度快,非常适合 SEO,服务器资源使用量最少。
2. 交互服务器当您需要实时交互(处理按钮点击、处理用户输入、动态更新 UI)时,您可以选择交互服务器模式。这会在浏览器和服务器之间建立 SignalR 连接,并通过该连接进行 UI 更新。
@page "/counter"
@rendermode InteractiveServer
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
@rendermode InteractiveServer 指令就足够了。该组件最初在服务器上呈现,然后建立 SignalR 连接来处理后续交互。您的 C# 事件处理程序在服务器上运行,并且 UI 差异将发送到浏览器。
何时使用它: 当您需要交互性时,您的组件需要访问服务器端资源(数据库、API、文件系统),并且您希望快速初始加载而无需等待 WebAssembly 下载。
3. 交互式 WebAssembly
如果您希望在不维护服务器连接的情况下进行交互,则交互式 WebAssembly 使用 .NET WebAssembly 运行时直接在浏览器中运行组件逻辑。
@page "/search"
@rendermode InteractiveWebAssembly
<h1>Product Search</h1>
<input @bind="searchTerm" @bind:event="oninput" placeholder="Search products..." />
@if (filteredProducts.Any())
{
<ul>
@foreach (var product in filteredProducts)
{
<li>@product.Name — @product.Price.ToString("C")</li>
}
</ul>
}
@code {
private string searchTerm = string.Empty;
private List<Product> allProducts = new();
private IEnumerable<Product> filteredProducts => allProducts
.Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync()
{
allProducts = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? new();
}
}
权衡:.NET 运行时和程序集需要初始下载成本。但一旦加载,该组件就完全在浏览器中运行,无需与服务器进行 UI 交互交互。
何时使用它: 对于延迟很重要的高度交互组件(例如:富文本编辑器、绘图工具、实时过滤),当您想要减少服务器负载时,或者当您构建渐进式 Web 应用程序 (PWA) 时。
4. 互动汽车
这是务实的中间立场,也是我最喜欢的功能之一。 Interactive Auto 首次加载时通过 SignalR 进行服务器端渲染,然后在后台静默下载 WebAssembly 运行时。在后续访问中,该组件在 WebAssembly 上运行。
@page "/dashboard"
@rendermode InteractiveAuto
<h1>Dashboard</h1>
<DashboardWidget Title="Sales" Value="@salesTotal" />
<DashboardWidget Title="Users" Value="@activeUsers" />
<button @onclick="RefreshData">Refresh</button>
@code {
private decimal salesTotal;
private int activeUsers;
protected override async Task OnInitializedAsync()
{
await RefreshData();
}
private async Task RefreshData()
{
var data = await DashboardService.GetSummaryAsync();
salesTotal = data.SalesTotal;
activeUsers = data.ActiveUsers;
}
}
这为您提供了两全其美的优势:快速首次渲染(无需等待 WASM 下载),并且最终组件在客户端运行。用户不会注意到这种转变。
何时使用它: 当您想要快速初始加载和最终客户端执行时。对于许多交互式组件来说,它是一个很好的默认选择。
流式 SSR:两全其美
流式 SSR 是听起来很简单但在感知性能方面产生巨大差异的功能之一。其想法是:服务器不会在发送任何 HTML 之前等待所有数据加载,而是立即发送页面 shell,然后在数据可用时流内容更新。
@page "/reports"
@attribute [StreamRendering]
<h1>Monthly Reports</h1>
@if (reports is null)
{
<div class="loading-spinner">
<p>Loading reports...</p>
</div>
}
else
{
<table class="table">
<thead>
<tr>
<th>Month</th>
<th>Revenue</th>
<th>Growth</th>
</tr>
</thead>
<tbody>
@foreach (var report in reports)
{
<tr>
<td>@report.Month</td>
<td>@report.Revenue.ToString("C")</td>
<td class="@(report.Growth >= 0 ? "text-success" : "text-danger")">
@report.Growth.ToString("P1")
</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<MonthlyReport>? reports;
protected override async Task OnInitializedAsync()
{
// This might take a couple of seconds
reports = await ReportService.GetMonthlyReportsAsync();
}
}
使用 [StreamRendering] 属性,会发生以下情况:
- 服务器立即使用加载微调器发送 HTML。
OnInitializedAsync运行并获取数据。- 当数据到达时,服务器将更新后的 HTML(表)传输到浏览器。
- 浏览器在不重新加载整页的情况下修补 DOM。
用户“立即”看到带有加载指示器的页面,然后内容就会填充。不需要 JavaScript 框架。没有 WebSocket 连接。只是巧妙地利用了 HTTP 流。何时使用它: 在渲染期间获取数据的任何静态 SSR 页面。产品列表页面、仪表板、报告——任何初始数据加载可能需要超过几百毫秒的地方。
何时不使用它: 如果数据加载速度极快(低于 100 毫秒),则流式传输开销不值得。此外,流式 SSR 无法为您提供持续的交互性 - 为此,您仍然需要交互式渲染模式。
增强的导航和表单处理
现代 Blazor 中微妙但强大的改进之一是增强的导航。默认情况下,Blazor 会拦截内部链接点击和表单提交,通过 fetch 获取新页面内容并修补 DOM,而不是执行完整的浏览器导航。
这意味着在静态 SSR 页面之间导航感觉就像 SPA — 没有整页闪烁,可以保留滚动位置,并且体验如丝般流畅。
它是如何运作的
加载 Blazor 脚本 (blazor.web.js) 时,它会自动拦截内部链接的点击。它不是传统的浏览器导航,而是:
- 向目标 URL 发出
fetch请求。 - 接收 HTML 响应。
- 将新内容合并到现有 DOM 中。 4.更新浏览器的URL和历史记录。
您无需执行任何操作即可启用此功能 - 默认情况下它是打开的。但你可以控制它:
<!-- Disable enhanced navigation for a specific link -->
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>
<!-- Force a full page reload for external-like behavior -->
<a href="/downloads/report.pdf" data-enhance-nav="false">Download Report</a>
增强的表单处理
表单得到相同的处理。当您在 Blazor 的表单处理中使用 EditForm 或标准 <form> 元素时,提交内容将通过 fetch 拦截和处理:
@page "/contact"
<EditForm Model="contactModel" OnValidSubmit="HandleSubmit" FormName="contact" Enhance>
<DataAnnotationsValidator />
<div class="mb-3">
<label for="name">Name</label>
<InputText id="name" @bind-Value="contactModel.Name" class="form-control" />
<ValidationMessage For="() => contactModel.Name" />
</div>
<div class="mb-3">
<label for="email">Email</label>
<InputText id="email" @bind-Value="contactModel.Email" class="form-control" />
<ValidationMessage For="() => contactModel.Email" />
</div>
<div class="mb-3">
<label for="message">Message</label>
<InputTextArea id="message" @bind-Value="contactModel.Message" class="form-control" />
<ValidationMessage For="() => contactModel.Message" />
</div>
<button type="submit" class="btn btn-primary">Send</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private ContactModel contactModel { get; set; } = new();
private async Task HandleSubmit()
{
await ContactService.SubmitAsync(contactModel);
contactModel = new ContactModel();
}
}
EditForm 上的 Enhance 属性告诉 Blazor 拦截表单提交。表单发送到服务器,服务器对其进行处理并重新呈现页面,然后将更新的 HTML 传回 - 所有这些都不需要完整的页面导航。它感觉是交互式的,但它完全是服务器渲染的。
请注意 [SupplyParameterFromForm] 属性 - 这是 Blazor 在静态 SSR 场景中将发布的表单数据绑定到模型的方式。它是传统表单柱和 Blazor 组件模型之间的桥梁。
每页面和每个组件的交互性
新模型最强大的方面之一是您可以在单个应用程序中混合渲染模式。您的产品列表页面可以是静态 SSR,您的购物车可以是交互式服务器,您的产品配置器可以是交互式 WebAssembly——所有这些都在同一个应用程序中。
在组件级别设置渲染模式
您可以使用 @rendermode 指令直接在组件上设置渲染模式:
@* This component is interactive via Server *@
@rendermode InteractiveServer
<h3>Live Chat</h3>
<!-- chat UI here -->
或者您可以在使用父组件时设置它:
@page "/product/{Id:int}"
<h1>@product?.Name</h1>
<p>@product?.Description</p>
<!-- This child component gets its own interactive render mode -->
<ProductConfigurator Product="product" @rendermode="InteractiveWebAssembly" />
<!-- This stays static -->
<ProductReviews ProductId="Id" />
@code {
[Parameter] public int Id { get; set; }
private Product? product;
protected override async Task OnInitializedAsync()
{
product = await ProductService.GetByIdAsync(Id);
}
}
在此示例中,页面本身是静态 SSR。 ProductConfigurator 组件在 WebAssembly 上运行,以实现丰富的客户端交互性。 ProductReviews 组件保持静态,因为它只是显示数据。
全局设置渲染模式
如果你希望所有页面默认都是交互式的,你可以在App.razor中的根级别设置渲染模式:
<Routes @rendermode="InteractiveServer" />
```这使得每个页面都通过服务器渲染进行交互。如果需要,单个组件仍然可以覆盖它。
### 需要记住的重要规则
混合渲染模式时需要记住一些限制:
- **子组件不能具有比其父组件“更具交互性”的渲染模式。** 如果父组件是静态的,则子组件可以是静态的或交互式的。但是,如果父级是 Interactive Server,则子级不能是 Interactive WebAssembly(它需要是 Server 或 Auto)。
- **交互式组件不能直接使用静态父级的作用域服务。** 如果组件在 WebAssembly 上运行,它无法直接访问服务器端 `DbContext` 实例 - 您将需要一个 API 层。
- **状态不会在渲染模式之间自动传输。** 如果组件在服务器上预渲染然后切换到 WebAssembly,则需要显式处理状态持久性 - 有关更多信息,请参阅 .NET 10 部分。
## .NET 10 中的新增功能
.NET 10 继续采用增量改进方法,重点关注可靠性和开发人员体验。以下是 Blazor 交互性的亮点:
### 改进重连体验
如果您在生产中使用过交互式服务器模式,您可能遇到过重新连接覆盖 - SignalR 连接断开并且用户看到“正在重新连接...”消息的那一刻。在 .NET 10 中,这种体验明显改善。
重新连接逻辑现在对于重试更加智能。它没有使用固定的重试间隔,而是使用适应情况的退避策略。重新连接期间的 UI 也更加精美,并且您有更好的钩子来自定义体验:
```razor
<!-- In your App.razor or layout -->
<div id="components-reconnect-modal">
<div class="reconnect-visible">
<p>Connection lost. Attempting to reconnect...</p>
<div class="spinner"></div>
</div>
<div class="reconnect-failed">
<p>Could not reconnect to the server.</p>
<button onclick="location.reload()">Reload</button>
</div>
<div class="reconnect-rejected">
<p>Your session has expired.</p>
<a href="/">Return to Home</a>
</div>
</div>
该框架现在还在瞬态故障上更积极地尝试重新连接,并且可以更可靠地恢复电路状态。这意味着您的用户将减少“请重新加载页面”的时刻。
跨渲染模式的持久组件状态
这是一件大事。在 .NET 9 中,当组件在服务器上预渲染然后转换到 WebAssembly(或在渲染模式之间切换)时,组件状态会丢失。该组件会有效地重新初始化,这可能会导致重复的数据获取和 UI 闪烁。
.NET 10 改进了 PersistentComponentState 服务,以便在渲染模式转换之间更加无缝地工作:
@page "/weather"
@rendermode InteractiveWebAssembly
@inject PersistentComponentState ApplicationState
<h1>Weather Forecast</h1>
@if (forecasts is null)
{
<p>Loading...</p>
}
else
{
@foreach (var forecast in forecasts)
{
<div class="forecast-card">
<h4>@forecast.Date.ToShortDateString()</h4>
<p>@forecast.Summary — @forecast.TemperatureC°C</p>
</div>
}
}
@code {
private List<WeatherForecast>? forecasts;
private PersistingComponentStateSubscription persistingSubscription;
protected override async Task OnInitializedAsync()
{
persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
if (!ApplicationState.TryTakeFromJson<List<WeatherForecast>>(
"weather-forecasts", out var restored))
{
// Data wasn't persisted from prerendering — fetch it
forecasts = await Http.GetFromJsonAsync<List<WeatherForecast>>("api/weather");
}
else
{
forecasts = restored;
}
}
private Task PersistData()
{
ApplicationState.PersistAsJson("weather-forecasts", forecasts);
return Task.CompletedTask;
}
public void Dispose()
{
persistingSubscription.Dispose();
}
}
随着 .NET 10 的改进,此模式的工作更加可靠。当组件在交互模式下初始化时,预渲染期间序列化的状态可以正确使用,从而避免双重数据获取和用户可能会看到的短暂闪烁。
Blazor 脚本作为静态 Web 资产
在以前的版本中,Blazor JavaScript 文件 (blazor.web.js) 由框架的内部端点提供。在 .NET 10 中,它作为静态 Web 资产提供。这听起来可能是一个很小的改变,但它有实际的好处:- 更好的缓存: 静态 Web 资源获得正确的缓存标头和指纹 URL,因此浏览器可以更有效地缓存它们。
- CDN 友好: 由于它是常规静态文件,CDN 可以从边缘位置缓存并提供服务。
- 压缩: 静态 Web 资源压缩会自动应用,从而减少线路上的脚本大小。
您无需更改引用它的方式 - 框架会自动处理更新的路径。
最佳实践:选择正确的渲染模式
在多个项目中使用所有这些模式之后,这是我的决策框架:
从静态 SSR 开始
将静态 SSR 设置为默认值。大多数应用程序中的大多数页面主要用于显示数据。产品页面、博客文章、用户个人资料、设置页面——这些不需要实时交互。静态 SSR 为您提供最佳性能、最低资源使用率和最简单的思维模型。
仅在需要的地方添加交互性
确定需要实时响应用户交互的特定组件。 “喜欢”按钮、聊天小部件、拖放界面——这些都需要交互性。但它们周围的页面可能没有。
使用交互式自动作为您的首选交互模式
当您确实需要交互性时,交互式自动通常是最佳默认选择。它为您提供快速的初始加载(服务器渲染)和最终的客户端执行(WebAssembly)。用户可以两全其美,而您只需编写一次代码。
为特定情况预留交互服务器
在以下情况下使用交互式服务器:
- 您的组件需要直接访问服务器资源(数据库、文件系统、内部 API)。
- WebAssembly 下载大小是一个问题,您不能使用自动。
- 出于安全原因(例如,处理敏感数据),您需要该组件始终在服务器上运行。
慷慨地使用流式 SSR
如果您的静态 SSR 页面获取任何数据,请添加 [StreamRendering]。成本是最低的,并且感知到的性能改进是显着的。用户看到的内容逐渐出现,而不是盯着空白页面。
小心处理状态转换
如果您使用 Interactive Auto 或将预渲染与 WebAssembly 混合,请始终使用 PersistentComponentState 以避免重复数据提取。您的用户会感谢您没有闪烁的内容。
牢记组件树
记住渲染模式层次结构规则。规划您的组件树,以便交互边界有意义。一种常见的模式是采用静态布局,并在需要的地方嵌入交互式“岛”:
@* Layout: Static SSR *@
<header>
<NavMenu />
<UserMenu @rendermode="InteractiveServer" /> @* Needs real-time auth state *@
</header>
<main>
@Body
</main>
<footer>
<ChatWidget @rendermode="InteractiveAuto" /> @* Rich interactivity *@
</footer>
结论
.NET 9 和 .NET 10 中的 Blazor 交互模型代表了一种成熟的、经过深思熟虑的构建 Web 应用程序的方法。为每个组件选择渲染模式的能力、无缝增强的导航、流式 SSR 以及重新连接和状态管理的持续改进使其成为各种应用程序的引人注目的选择。关键的见解是交互性是一个范围,而不是二元选择。您的大部分应用程序可以是静态的。有些部分需要服务器驱动的交互性。有些可能会受益于在浏览器中运行。 Blazor 现在允许您以最细粒度(单个组件)做出选择,而无需与框架冲突。
如果您要开始一个新项目,我的建议很简单:创建一个 Blazor Web 应用程序,从静态 SSR 开始,将 [StreamRendering] 添加到数据密集的页面,并仅在需要时将各个组件提升为交互模式。您最终将得到一个快速、高效且可扩展的应用程序。
祝您编码愉快,一如既往,如果您有疑问,请随时与我们联系!