Blazor 全栈 Web 开发
大约 9 分钟约 2647 字
Blazor 全栈 Web 开发
简介
Blazor 是 .NET 的 Web 前端框架,允许使用 C# 替代 JavaScript 构建交互式 Web UI。Blazor Server 通过 SignalR 在服务器端运行,Blazor WebAssembly 在浏览器中直接运行 .NET 代码。让 .NET 开发者可以全栈使用 C#。
特点
Blazor 模式
模式对比
| 特性 | Blazor Server | Blazor WebAssembly |
|---|---|---|
| 运行位置 | 服务器 | 浏览器 |
| 通信 | SignalR | 无需连接 |
| 初始加载 | 快 | 较慢(下载 .NET 运行时) |
| 离线 | 不支持 | 支持 |
| 服务器资源 | 占用高 | 占用低 |
| 延迟 | 受网络影响 | 本地执行 |
基本组件
Razor 组件
<!-- UserList.razor -->
@page "/users"
@inject IUserService UserService
@implements IDisposable
<h2>用户列表</h2>
<div class="mb-3">
<input type="text" @bind="keyword" @bind:event="oninput"
placeholder="搜索用户" class="form-control" />
</div>
@if (loading)
{
<p>加载中...</p>
}
else if (users.Any())
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var user in filteredUsers)
{
<tr>
<td>@user.Id</td>
<td>@user.Name</td>
<td>@user.Email</td>
<td>
<span class="badge @(user.IsActive ? "bg-success" : "bg-secondary")">
@(user.IsActive ? "活跃" : "禁用")
</span>
</td>
<td>
<button @onclick="() => EditUser(user.Id)" class="btn btn-sm btn-primary">编辑</button>
<button @onclick="() => DeleteUser(user.Id)" class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>暂无数据</p>
}
@code {
private List<UserDto> users = new();
private string keyword = "";
private bool loading = true;
private IEnumerable<UserDto> filteredUsers =>
string.IsNullOrEmpty(keyword)
? users
: users.Where(u => u.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|| u.Email.Contains(keyword, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync()
{
users = await UserService.GetAllAsync();
loading = false;
}
private async Task DeleteUser(int id)
{
await UserService.DeleteAsync(id);
users.RemoveAll(u => u.Id == id);
}
private void EditUser(int id)
{
Navigation.NavigateTo($"/users/{id}/edit");
}
[Inject]
private NavigationManager Navigation { get; set; } = default!;
}表单与验证
<!-- CreateUser.razor -->
@page "/users/create"
@inject IUserService UserService
@inject NavigationManager Navigation
<h2>创建用户</h2>
<EditForm Model="model" OnValidSubmit="CreateUser" FormName="CreateUser">
<DataAnnotationsValidator />
<div class="mb-3">
<label class="form-label">用户名</label>
<InputText @bind-Value="model.UserName" class="form-control" />
<ValidationMessage For="() => model.UserName" />
</div>
<div class="mb-3">
<label class="form-label">邮箱</label>
<InputText @bind-Value="model.Email" class="form-control" />
<ValidationMessage For="() => model.Email" />
</div>
<div class="mb-3">
<label class="form-label">手机号</label>
<InputText @bind-Value="model.Phone" class="form-control" />
<ValidationMessage For="() => model.Phone" />
</div>
<button type="submit" class="btn btn-primary">创建</button>
<a href="/users" class="btn btn-secondary">返回</a>
</EditForm>
@code {
private CreateUserRequest model = new();
private async Task CreateUser()
{
await UserService.CreateAsync(model);
Navigation.NavigateTo("/users");
}
}// 模型和验证
public class CreateUserRequest
{
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(50, ErrorMessage = "用户名最长50个字符")]
public string UserName { get; set; } = "";
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; } = "";
[RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手机号格式不正确")]
public string Phone { get; set; } = "";
}组件通信
参数与事件
<!-- 子组件:Pagination.razor -->
<div class="pagination">
<button @onclick="PrevPage" disabled="@(_page <= 1)">上一页</button>
<span>第 @_page / @TotalPages 页</span>
<button @onclick="NextPage" disabled="@(_page >= TotalPages)">下一页</button>
</div>
@code {
[Parameter]
public int TotalCount { get; set; }
[Parameter]
public int PageSize { get; set; } = 10;
[Parameter]
public EventCallback<int> PageChanged { get; set; }
private int _page = 1;
private int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
private async Task PrevPage()
{
if (_page > 1)
{
_page--;
await PageChanged.InvokeAsync(_page);
}
}
private async Task NextPage()
{
if (_page < TotalPages)
{
_page++;
await PageChanged.InvokeAsync(_page);
}
}
}<!-- 父组件使用 -->
<Pagination TotalCount="@totalCount" PageSize="10"
PageChanged="OnPageChanged" />
@code {
private int totalCount = 0;
private async Task OnPageChanged(int page)
{
await LoadData(page);
}
}调用 JavaScript
JS 互操作
/// <summary>
/// 从 C# 调用 JavaScript
/// </summary>
@inject IJSRuntime JS
@code {
private async Task ShowAlert()
{
await JS.InvokeVoidAsync("alert", "来自 Blazor 的消息");
}
private async Task<string> GetLocalStorage(string key)
{
return await JS.InvokeAsync<string>("localStorage.getItem", key);
}
private async Task SetLocalStorage(string key, string value)
{
await JS.InvokeVoidAsync("localStorage.setItem", key, value);
}
}// wwwroot/js/interop.js
window.chartHelper = {
drawChart: (canvasId, data) => {
const canvas = document.getElementById(canvasId);
// 绘制图表...
},
downloadFile: (fileName, content) => {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
}
};HTTP 调用
调用后端 API
状态管理
级联参数与状态容器
<!-- CascadingParameter — 跨层级传递状态 -->
<!-- App.razor 或 Layout 中提供状态 -->
<CascadingValue Value="@AppState" IsFixed="true">
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
</Router>
</CascadingValue>
@code {
public AppState AppState { get; set; } = new();
}/// <summary>
/// 全局状态容器
/// </summary>
public class AppState
{
public UserDto? CurrentUser { get; set; }
public event Action? OnUserChanged;
public void SetUser(UserDto user)
{
CurrentUser = user;
OnUserChanged?.Invoke();
}
}
// 子组件中接收
[ CascadingParameter]
public AppState AppState { get; set; } = default!;
protected override void OnInitialized()
{
AppState.OnUserChanged += HandleUserChanged;
}
public void Dispose()
{
AppState.OnUserChanged -= HandleUserChanged;
}
private void HandleUserChanged()
{
// 用户状态变化时刷新 UI
StateHasChanged();
}生命周期方法
组件生命周期
@page "/lifecycle-demo"
@implements IDisposable
<h3>组件生命周期演示</h3>
<p>初始化次数: @_initCount</p>
<p>渲染次数: @_renderCount</p>
@code {
private int _initCount;
private int _renderCount;
// 参数设置后调用(适合用参数初始化数据)
protected override async Task OnParametersSetAsync()
{
_initCount++;
await LoadDataAsync();
}
// 组件首次渲染后调用(适合 DOM 操作)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 只在首次渲染后执行
await JS.InvokeVoidAsync("initChart", "chart-container");
}
}
// shouldRender — 控制是否重新渲染(性能优化)
protected override bool ShouldRender()
{
// 避免不必要的渲染
return true;
}
private async Task LoadDataAsync()
{
await Task.Delay(100);
}
public void Dispose()
{
// 清理资源
}
}错误处理
ErrorBoundary 与全局异常
<!-- ErrorBoundary — 捕获子组件异常 -->
<ErrorBoundary @ref="_errorBoundary">
<ChildContent>
<DangerousComponent />
</ChildContent>
<ErrorContent Context="ex">
<div class="alert alert-danger">
<h5>组件发生错误</h5>
<p>@ex.Message</p>
<button @onclick="ResetError">重试</button>
</div>
</ErrorContent>
</ErrorBoundary>
@code {
private ErrorBoundary? _errorBoundary;
private void ResetError()
{
_errorBoundary?.Recover();
}
}<!-- 全局错误处理 — MainLayout.razor -->
<ErrorBoundary>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>您没有权限访问此页面。</p>
</NotAuthorized>
<Authorizing>
<p>正在验证权限...</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<PageTitle>未找到</PageTitle>
<p>请求的页面不存在。</p>
</NotFound>
</Router>
</ErrorBoundary>性能优化
虚拟化与延迟加载
<!-- Virtualize — 大列表虚拟滚动 -->
<Virtualize Items="@allUsers" Context="user" OverscanCount="10">
<div class="user-item">
<span>@user.Name</span>
<span>@user.Email</span>
</div>
</Virtualize>
@code {
private async Task<UserDto> LoadUsersAsync(int startIndex, int count)
{
// 按需加载
return await UserService.GetPagedAsync(startIndex, count);
}
}
<!-- Virtualize 异步加载 -->
<Virtualize ItemsProvider="@LoadUsersAsync" Context="user">
<div class="user-item">
<span>@user.Name</span>
</div>
</Virtualize><!-- 避免不必要的重渲染 -->
@* 使用 @bind:after 延迟绑定 *@
<input @bind="searchText" @bind:after="PerformSearch" @bind:event="oninput"
placeholder="搜索..." />
@* 使用 @key 稳定元素 *@
@foreach (var item in items)
{
<div @key="item.Id">
@item.Name
</div>
}
@code {
private string searchText = "";
private CancellationTokenSource? _cts;
private async Task PerformSearch()
{
_cts?.Cancel();
_cts = new CancellationTokenSource();
try
{
await Task.Delay(300, _cts.Token); // 防抖
await SearchAsync(searchText, _cts.Token);
}
catch (OperationCanceledException) { }
}
}Blazor WebAssembly 部署优化
减小包体积
<!-- 项目文件配置 -->
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<!-- 裁剪未使用的代码 -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Wasm Native AOT(.NET 8+,大幅提升性能) -->
<RunAOTCompilation>true</RunAOTCompilation>
<!-- 压缩 -->
<CompressionEnabled>true</CompressionEnabled>
<!-- Brotli 压缩 -->
<WasmEnableWebcil>true</WasmEnableWebcil>
</PropertyGroup>
<!-- 按需国际化 -->
<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="WasmSamples.Shared" />
</ItemGroup>
</Project>混合模式(.NET 8+)
Blazor Web App
<!-- .NET 8 混合模式:页面级选择 Server 或 WebAssembly -->
@* @rendermode="InteractiveServer" — 服务端交互 *@
@* @rendermode="InteractiveWebAssembly" — 客户端交互 *@
@* @rendermode="InteractiveAuto" — 自动选择(推荐)*@
@page "/counter"
@rendermode="InteractiveAuto"
<h1>计数器(自动模式)</h1>
<p>当前计数: @count</p>
<button @onclick="Increment">+1</button>
@code {
private int count;
private void Increment() => count++;
}// Program.cs — .NET 8 Blazor Web App 配置
var builder = WebApplication.CreateBuilder(args);
// 同时添加 Server 和 WebAssembly 支持
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
var app = builder.Build();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Client._Imports).Assembly);
app.Run();/// <summary>
/// Blazor 中的 HTTP 服务
/// </summary>
public class ApiClient
{
private readonly HttpClient _http;
public ApiClient(HttpClient http)
{
_http = http;
}
public async Task<List<UserDto>> GetUsersAsync()
{
return await _http.GetFromJsonAsync<List<UserDto>>("/api/users")
?? new List<UserDto>();
}
public async Task<UserDto?> GetUserAsync(int id)
{
var response = await _http.GetAsync($"/api/users/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<UserDto>();
}
public async Task<UserDto> CreateUserAsync(CreateUserRequest request)
{
var response = await _http.PostAsJsonAsync("/api/users", request);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<UserDto>())!;
}
}
// 注册
// builder.Services.AddScoped<ApiClient>();
// builder.Services.AddHttpClient<ApiClient>(c => c.BaseAddress = new Uri("https://api.example.com"));优点
缺点
总结
Blazor 适合 .NET 团队构建内部管理系统、后台面板等场景。Server 模式适合快速开发,WebAssembly 适合需要离线或低延迟的场景。如果是面向公众的网站,React/Vue 仍然是更好的选择。核心价值:C# 全栈开发,前后端代码共享。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《Blazor 全栈 Web 开发》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《Blazor 全栈 Web 开发》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Blazor 全栈 Web 开发》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Blazor 全栈 Web 开发》最大的收益和代价分别是什么?
