Loading...
Loading...
Compare original and translation side by side
| Mode | Data access |
|---|---|
| None (Static SSR) | Server-side: inject services/ |
| Server | Server-side: inject services/ |
| WebAssembly | Browser-side: |
| Auto | Both server and browser. Always go through an API. |
| 模式 | 数据访问方式 |
|---|---|
| None (静态SSR) | 服务端:注入服务/ |
| Server | 服务端:注入服务/ |
| WebAssembly | 浏览器端:仅使用 |
| Auto | 同时支持服务端和浏览器端。需始终通过API访问数据。 |
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/"));.ClientProgram.csDbContext// 命名客户端 — 需要Microsoft.Extensions.Http NuGet包
builder.Services.AddHttpClient("CatalogAPI", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
});
// 类型化客户端
builder.Services.AddHttpClient<CatalogClient>(client =>
client.BaseAddress = new Uri("https://api.example.com/"));.ClientProgram.cs@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>@page "/products"
@inject CatalogClient Catalog
@if (products is null)
{
<p>加载中…</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>[StreamRendering]OnInitializedAsync@attribute [StreamRendering][StreamRendering]OnInitializedAsync@attribute [StreamRendering]OnInitializedAsync[PersistentState] private Product[]? products;
protected override async Task OnInitializedAsync()
{
products ??= await Catalog.GetProductsAsync();
}support-prerenderingOnInitializedAsync[PersistentState] private Product[]? products;
protected override async Task OnInitializedAsync()
{
products ??= await Catalog.GetProductsAsync();
}support-prerendering<ErrorBoundary><ErrorBoundary>
<ChildContent>
<ProductList />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">Something went wrong. Please refresh.</div>
</ErrorContent>
</ErrorBoundary>HttpRequestExceptionErrorBoundary<ErrorBoundary><ErrorBoundary>
<ChildContent>
<ProductList />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">出现错误,请刷新页面重试。</div>
</ErrorContent>
</ErrorBoundary>HttpRequestExceptionErrorBoundaryComponentBaseOperationCanceledExceptionErrorBoundaryComponentBaseOperationCanceledExceptionErrorBoundaryErrorBoundary// 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.";
}ErrorBoundarycatch (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.";
}ErrorBoundary// 仅捕获外部取消(超时)——其他异常全部传递给ErrorBoundary
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "分类{CategoryId}请求超时", CategoryId);
error = "请求超时,请重试。";
}ErrorBoundarycatch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "分类{CategoryId}请求超时", CategoryId);
error = "请求超时,请重试。";
}
catch (Exception ex)
{
Logger.LogError(ex, "加载分类{CategoryId}产品失败", CategoryId);
error = "无法加载产品,请重试。";
}exception.MessageILoggerCancellationTokenexception.MessageILoggerCancellationToken/products/1/products/2OnParametersSetAsync/products/1/products/2OnParametersSetAsync@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();
}
}
}loadedCategoryIdproductsisLoadingIAsyncDisposable@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">重试</button>
</div>
}
else if (products is null)
{
<p>加载中…</p>
}
else
{
@if (isLoading)
{
<p><em>刷新中…</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
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; // 仅ViewMode变更——无需重新加载
}
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; // 在await前本地捕获令牌
error = null;
isLoading = true;
try
{
var result = await ProductService.GetByCategoryAsync(CategoryId, cancellationToken);
products = result;
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "加载分类{CategoryId}超时", CategoryId);
error = "请求超时,请重试。";
}
finally
{
isLoading = false;
}
}
public async ValueTask DisposeAsync()
{
if (cts is not null)
{
await cts.CancelAsync();
cts.Dispose();
}
}
}loadedCategoryIdproductsisLoadingIAsyncDisposablevar 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();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();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.cspublic abstract class ProductServiceBase
{
public abstract Task<Product[]> GetAllAsync(CancellationToken ct = default);
}
// 服务端 — 直接访问数据库
public class ServerProductService(AppDbContext db) : ProductServiceBase
{
public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
await db.Products.ToArrayAsync(ct);
}
// 客户端 — 调用API
public class ClientProductService(HttpClient http) : ProductServiceBase
{
public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
await http.GetFromJsonAsync<Product[]>("api/products", ct) ?? [];
}Program.csOnInitializedAsyncOnParametersSetAsyncOnInitializedAsyncDbContextHttpClientexception.MessageOperationCanceledExceptionComponentBaseOnInitializedAsyncOnParametersSetAsyncOnInitializedAsyncDbContextHttpClientexception.MessageOperationCanceledExceptionComponentBase