一、Blazor路由机制与Router组件
Blazor的路由系统完全在客户端运行,不依赖服务器来匹配URL并返回对应页面------这正是现代单页应用(SPA)的核心特征。当用户点击链接时,Blazor拦截导航事件,在不刷新整个页面的前提下卸载当前组件并挂载新组件,整个页面保持流畅连续。
路由的起点是App.razor,这是应用的根组件,也是路由配置的核心:
csharp
@* Components/App.razor *@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>MyApp</title>
@* HeadOutlet 允许各页面组件动态修改 <head> 内容(如改变标题、SEO标签) *@
<HeadOutlet />
</head>
<body>
@* Routes 组件封装了路由核心逻辑 *@
<Routes />
</body>
</html>
csharp
@* Components/Routes.razor *@
@* Router 组件是路由系统的引擎,AppAssembly 指定要扫描 @page 指令的程序集 *@
<Router AppAssembly="typeof(App).Assembly">
<Found Context="routeData">
@* 找到匹配路由时,用 RouteView 渲染对应组件,DefaultLayout 指定默认布局 *@
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
@* FocusOnNavigate 在页面导航完成后自动聚焦指定选择器的元素,改善无障碍体验 *@
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
@* 没有匹配路由时渲染此处内容 *@
<PageTitle>页面未找到</PageTitle>
<LayoutView Layout="typeof(Layout.MainLayout)">
<h1>404 - 页面不存在</h1>
<p>您访问的页面不存在,<a href="/">返回首页</a>。</p>
</LayoutView>
</NotFound>
</Router>
Router组件在应用启动时扫描AppAssembly所指程序集,找出所有带@page指令的组件并建立路由表。每当URL发生变化(无论是用户点击链接、按浏览器后退键,还是通过代码导航),Router都会重新匹配当前URL,并在Found或NotFound分支中渲染相应内容。
如果应用跨多个程序集(如独立的Razor类库),需要通过AdditionalAssemblies参数告知Router扫描其他程序集:
csharp
<Router AppAssembly="typeof(App).Assembly"
AdditionalAssemblies="new[] { typeof(SomeLibrary.SomeComponent).Assembly }">
...
</Router>
二、路由参数、约束与可选参数
@page指令中的路由模板支持与ASP.NET Core MVC完全相同的参数语法,通过花括号定义路由段,并可附加约束和可选标记。
基础路由参数 直接在花括号内写参数名,参数名必须与组件@code区中标记了[Parameter]的属性名(不区分大小写)完全匹配:
csharp
@* /products/42 → Id = 42 *@
@page "/products/{Id:int}"
@code {
[Parameter]
public int Id { get; set; }
}
:int是路由约束 ,表示该路由段必须能转换为int类型,非整数URL(如/products/abc)不会匹配此路由。常用约束还有:guid(GUID格式)、:bool(布尔值)、:datetime(日期时间)、:decimal(小数)等。如果不加约束,参数类型默认被视为字符串。
可选参数 在参数名末尾加?,对应的C#属性应声明为可空类型(int?、string?):
csharp
@* /articles/2024 或 /articles 都能匹配 *@
@page "/articles/{Year:int?}"
@code {
[Parameter]
public int? Year { get; set; }
// 在 OnParametersSet 中处理可选参数的默认值
protected override void OnParametersSet()
{
Year ??= DateTime.Now.Year;
}
}
Catch-all参数 用*前缀捕获URL中任意数量的路径段,常用于构建树形结构的面包屑导航或文件浏览器:
csharp
@* /files/documents/2024/report.pdf → FilePath = "documents/2024/report.pdf" *@
@page "/files/{*FilePath}"
@code {
[Parameter]
public string FilePath { get; set; } = string.Empty;
}
同一个组件也可以通过多个@page指令注册多个路由:
csharp
@page "/products"
@page "/catalog"
@page "/shop"
三、NavigationManager:程序化导航
NavigationManager是Blazor提供的导航服务,允许在C#代码中读取当前URL、监听导航事件,以及在不点击任何链接的情况下以编程方式跳转页面。通过@inject注入后即可使用:
csharp
@inject NavigationManager NavManager
@code {
// 获取完整的当前URL,如 "https://localhost:5001/products/42"
private string currentUrl => NavManager.Uri;
// 获取基础URL,如 "https://localhost:5001/"
private string baseUrl => NavManager.BaseUri;
// 获取相对路径,如 "/products/42"
private string relativePath => NavManager.ToBaseRelativePath(NavManager.Uri);
private void GoToHome()
{
// 导航到指定URL(相对或绝对均可)
NavManager.NavigateTo("/");
}
private void GoToProductWithReplace()
{
// forceLoad: true 强制整页刷新(通常不需要)
// replace: true 替换浏览器历史记录中的当前条目(用户无法后退到此页面)
NavManager.NavigateTo("/products/1", replace: true);
}
// 登录成功后重定向到之前尝试访问的页面
private async Task HandleLogin()
{
var returnUrl = new Uri(NavManager.Uri).Query;
// ... 执行登录逻辑 ...
NavManager.NavigateTo(returnUrl.Length > 0 ? returnUrl : "/");
}
}
LocationChanged事件在每次导航成功后触发,常用于分析统计或在导航时执行特定操作(如滚动到顶部):
csharp
@inject NavigationManager NavManager
@implements IDisposable
@code {
protected override void OnInitialized()
{
NavManager.LocationChanged += OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
// e.Location 是新的完整URL
// e.IsNavigationIntercepted 表示是否是通过拦截锚点点击触发的导航
Console.WriteLine($"导航到: {e.Location}");
}
public void Dispose()
{
NavManager.LocationChanged -= OnLocationChanged;
}
}
NavLink组件是Blazor对HTML <a>标签的增强版,当其href与当前URL匹配时会自动添加ActiveClass属性指定的CSS类(默认为active),非常适合构建高亮当前页面的导航菜单:
csharp
@* NavLink 会在匹配当前路由时自动添加 "active" class *@
<NavLink href="/" Match="NavLinkMatch.All">首页</NavLink>
<NavLink href="/products" Match="NavLinkMatch.Prefix">产品中心</NavLink>
NavLinkMatch.All要求URL完全匹配才高亮;NavLinkMatch.Prefix只要URL以href开头就高亮,适合有子页面的导航项(如/products能匹配/products/42)。
四、路由守卫与授权控制
真实应用中,某些页面必须登录后才能访问。Blazor通过AuthorizeRouteView和[Authorize]特性提供声明式的路由级授权控制。
首先将Routes.razor中的RouteView替换为AuthorizeRouteView,并提供NotAuthorized内容,指定未认证时的降级UI:
csharp
@* Components/Routes.razor(带授权版本)*@
@using Microsoft.AspNetCore.Components.Authorization
<Router AppAssembly="typeof(App).Assembly">
<Found Context="routeData">
@* AuthorizeRouteView 在渲染页面前检查授权状态 *@
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
@* 未认证时,重定向到登录页,并将当前URL作为returnUrl参数传过去 *@
<RedirectToLogin />
</NotAuthorized>
<Authorizing>
@* 正在验证认证状态时显示(网络慢时可见) *@
<p>正在验证身份...</p>
</Authorizing>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(Layout.MainLayout)">
<h1>页面不存在</h1>
</LayoutView>
</NotFound>
</Router>
RedirectToLogin是一个简单的自定义组件,它在渲染时立即执行重定向:
csharp
@* Components/RedirectToLogin.razor *@
@inject NavigationManager NavManager
@code {
protected override void OnInitialized()
{
// 将当前URL编码后作为 returnUrl 参数,以便登录后能回到原页面
var returnUrl = Uri.EscapeDataString(NavManager.ToBaseRelativePath(NavManager.Uri));
NavManager.NavigateTo($"/login?returnUrl={returnUrl}");
}
}
在需要保护的页面组件上添加[Authorize]特性。如果用@attribute指令写在.razor文件中,等效于在类上标记特性:
csharp
@page "/admin/users"
@attribute [Authorize(Roles = "Admin")]
<h1>用户管理</h1>
@* 只有Admin角色的已登录用户才能看到此页面 *@
还可以在同一个页面内按细粒度控制UI元素的可见性,AuthorizeView组件提供了Authorized、NotAuthorized、Authorizing三个插槽:
razor
<AuthorizeView Roles="Admin,Manager">
<Authorized>
@* 已认证且具有 Admin 或 Manager 角色才显示 *@
<button class="btn btn-danger">删除记录</button>
</Authorized>
<NotAuthorized>
@* 权限不足时显示友好提示 *@
<span class="text-muted">(无权操作)</span>
</NotAuthorized>
</AuthorizeView>
认证状态本身由AuthenticationStateProvider提供,该服务的具体实现与认证方案(JWT、Cookie、Identity等)密切相关,我们将在第四章的认证授权部分做详细讲解。
五、总结
本章完整梳理了Blazor路由系统的运作机制:Router组件在运行时扫描程序集建立路由表,@page指令配合类型约束声明路由匹配规则,NavigationManager提供程序化导航与URL读取的能力,NavLink自动处理活跃状态高亮,AuthorizeRouteView与[Authorize]特性共同构建声明式的路由级授权体系。
至此,第二章的组件进阶内容已全部覆盖。在实际项目中,你一定会遇到某些功能必须借助JavaScript才能实现------浏览器原生API(如Clipboard、Notification、WebSocket)、第三方JavaScript库的图表和编辑器,或者对DOM的像素级操控。下一章,我们将深入Blazor与JavaScript互操作,学习如何从C#代码安全、高效地调用JavaScript函数,以及如何将JavaScript的回调结果传回C#组件。