关于 fetch-and-send-data
面向 Blazor 开发者的技能指南,涵盖数据获取、提交和组件加载状态处理。按渲染模式区分数据访问方式,并说明取消、错误及参数变化时的处理模式。
主要功能
- 01按渲染模式选择数据访问方式
- 02配置命名或类型化 HttpClient
- 03处理静态 SSR 与预渲染
- 04使用 ErrorBoundary 管理错误
- 05按参数变化取消并重新加载
- 06通过 HTTP 提交数据
适用场景
- 01为 Blazor 页面加载外部 API 数据
- 02为静态 SSR 页面设置流式加载
- 03处理路由参数变化造成的重复请求
- 04提交表单并防止重复操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill fetch-and-send-data安装后在 Claude Code 中输入/fetch-and-send-data确认已加载。
详细介绍
获取和发送数据
步骤 1 — 阅读 AGENTS.md
检查 交互模式 和 范围:
| 模式 | 数据访问 |
|---|---|
| None(静态 SSR) | 服务端:注入服务/DbContext。使用 [StreamRendering] 提供加载体验。 |
| Server | 服务端:注入服务/DbContext。使用 ??= + [PersistentState] 防止预渲染重复执行。 |
| WebAssembly | 浏览器端:仅使用 HttpClient。无法直接访问服务端。 |
| Auto | 服务端和浏览器端均可。始终通过 API 访问。 |
步骤 2 — 注册 HttpClient
仅当从 Server 调用外部 API 时才需要;WebAssembly/Auto 则始终需要。访问自身数据库的服务端组件应直接注入 DbContext 或服务。
// Named client — requires Microsoft.Extensions.Http NuGet
builder.Services.AddHttpClient("CatalogAPI", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
});
// Typed client
builder.Services.AddHttpClient<CatalogClient>(client =>
client.BaseAddress = new Uri("https://api.example.com/"));
使用预渲染的 WebAssembly/Auto 项目,需要在服务端和 .Client 的 Program.cs 中都进行注册。
步骤 3 — 获取数据
简单加载
@page "/products"
@inject CatalogClient Catalog
@if (products is null)
{
<p>Loading…</p>
}
else
{
@foreach (var p in products)
{
<p>@p.Name — @p.Price.ToString("C")</p>
}
}
@code {
private Product[]? products;
protected override async Task OnInitializedAsync()
{
products = await Catalog.GetProductsAsync();
}
}
最简单的情况下无需处理错误——在父级或布局级别用 <ErrorBoundary> 包装组件,以捕获未处理的异常。
静态 SSR — StreamRendering
不使用 [StreamRendering] 时,用户在 OnInitializedAsync 完成前看不到任何内容:
@attribute [StreamRendering]
仅对静态 SSR 有效。对交互式组件没有影响。
预渲染保护
预渲染会调用两次 OnInitializedAsync。跳过重复调用:
[PersistentState] private Product[]? products;
protected override async Task OnInitializedAsync()
{
products ??= await Catalog.GetProductsAsync();
}
详情请参阅 support-prerendering 技能。
步骤 4 — 处理错误
默认错误处理策略是使用 <ErrorBoundary>。它能为所有组件提供一致的错误体验,无需在每个组件中编写捕获逻辑。在布局或父级中包装组件:
<ErrorBoundary>
<ChildContent>
<ProductList />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">Something went wrong. Please refresh.</div>
</ErrorContent>
</ErrorBoundary>
非取消异常(HttpRequestException 等)会自动传播到 ErrorBoundary,组件中无需编写 catch 块。
取消情况比较特殊
ComponentBase 会静默吞掉所有 OperationCanceledException,包括自行触发的取消(组件释放、参数更改)和外部取消(HttpClient 超时)。ErrorBoundary 永远不会收到这些异常。这意味着:
- 自行取消 → 静默忽略。行为正确,无需处理。
- 外部取消(超时)→ 同样会被静默吞掉。组件会卡在加载状态。通常可以接受,因为超时很少发生。
何时在组件内添加错误处理
仅当组件需要实现 ErrorBoundary 无法提供的行为时才添加 catch 块,通常是重试或超时专属消息。即便如此,也只捕获所需的异常:
// Catch only external cancellation (timeouts) — everything else flows to ErrorBoundary
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "Request timed out for category {CategoryId}", CategoryId);
error = "The request timed out. Please try again.";
}
如果组件还需要处理一般错误,并显示重试按钮,而不是交由 ErrorBoundary 处理:
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "Request timed out for category {CategoryId}", CategoryId);
error = "The request timed out. Please try again.";
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to load products for category {CategoryId}", CategoryId);
error = "Unable to load products. Please try again.";
}
规则
- 绝不显示
exception.Message——其中可能包含 PII、连接字符串或内部详情。请使用硬编码的用户友好消息。 - 始终通过
ILogger记录日志——真实异常会进入日志记录管道。 - 服务必须接受
CancellationToken——将其传递给每个异步调用,以便组件取消时停止工作。
步骤 5 — 根据参数重新加载
如果数据取决于会发生变化的路由或查询参数(例如,在 /products/1 和 /products/2 之间导航),请使用 OnParametersSetAsync,并通过保护条件跳过不影响数据的参数所触发的重新加载。
模式:取消并重新加载,同时覆盖显示旧数据
@page "/products/{CategoryId:int}"
@implements IAsyncDisposable
@inject ProductService ProductService
@inject ILogger<Products> Logger
@if (error is not null)
{
<div class="alert alert-danger">
<p>@error</p>
<button @onclick="LoadAsync">Retry</button>
</div>
}
else if (products is null)
{
<p>Loading…</p>
}
else
{
@if (isLoading)
{
<p><em>Refreshing…</em></p>
}
@foreach (var p in products)
{
<p>@p.Name — @p.Price.ToString("C")</p>
}
}
@code {
[Parameter] public int CategoryId { get; set; }
[SupplyParameterFromQuery] public string? ViewMode { get; set; } // UI-only
private CancellationTokenSource? cts;
private int? loadedCategoryId;
private List<Product>? products;
private bool isLoading;
private string? error;
protected override async Task OnParametersSetAsync()
{
if (CategoryId == loadedCategoryId)
{
return; // Only ViewMode changed — no reload
}
loadedCategoryId = CategoryId;
await LoadAsync();
}
private async Task LoadAsync()
{
if (cts is not null)
{
await cts.CancelAsync();
cts.Dispose();
}
cts = new CancellationTokenSource();
var cancellationToken = cts.Token; // Capture locally before await
error = null;
isLoading = true;
try
{
var result = await ProductService.GetByCategoryAsync(CategoryId, cancellationToken);
products = result;
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "Timed out loading category {CategoryId}", CategoryId);
error = "The request timed out. Please try again.";
}
finally
{
isLoading = false;
}
}
public async ValueTask DisposeAsync()
{
if (cts is not null)
{
await cts.CancelAsync();
cts.Dispose();
}
}
}
要点:
- 使用跟踪值进行保护:当只有 UI 参数变化时,
loadedCategoryId会跳过重新加载。 - 在 await 之前将令牌保存到局部变量——CTS 字段可能会因并发的参数更改而被替换。
- 后续加载时不要将
products设为 null——保留现有数据显示,并叠加isLoading状态。 IAsyncDisposable会在用户离开页面时取消待处理的工作。
步骤 6 — 发送数据
var response = await http.PostAsJsonAsync("products", newProduct);
response.EnsureSuccessStatusCode();
var response = await http.PutAsJsonAsync($"products/{id}", updated);
response.EnsureSuccessStatusCode();
var response = await http.DeleteAsync($"products/{id}");
response.EnsureSuccessStatusCode();
保存期间禁用提交按钮,以防止重复请求。显示保存指示器。
步骤 7 — 为启用预渲染的 Auto 或 WebAssembly 模式抽象服务
当组件同时在服务器和浏览器中运行时(Auto 模式,或启用预渲染的 WebAssembly),请使用抽象基类封装数据访问:
public abstract class ProductServiceBase
{
public abstract Task<Product[]> GetAllAsync(CancellationToken ct = default);
}
// Server — direct database access
public class ServerProductService(AppDbContext db) : ProductServiceBase
{
public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
await db.Products.ToArrayAsync(ct);
}
// Client — calls API
public class ClientProductService(HttpClient http) : ProductServiceBase
{
public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
await http.GetFromJsonAsync<Product[]>("api/products", ct) ?? [];
}
在每个项目的 Program.cs 中注册相应的实现。组件注入抽象基类。
注意事项
- 不要在构造函数中调用 API —— 使用
OnInitializedAsync。 - 除非数据依赖于会发生变化的参数,否则不要使用
OnParametersSetAsync。 初次加载时使用OnInitializedAsync。 - 不要在 WebAssembly/Auto 组件中注入
DbContext—— 浏览器中没有数据库。 - 不要通过
HttpClient调用自己的服务端 —— 直接注入服务。 - 不要向用户显示
exception.Message—— 存在泄露个人身份信息(PII)的风险。记录日志,并显示通用消息。 - 不要捕获用于自行取消的
OperationCanceledException——ComponentBase会处理它。