Blazor 路由及导航开发指南
翻译自 Waqas Anwar 2021年4月2日的文章 《A Developer’s Guide To Blazor Routing and Navigation》 1
检查传入的请求 URL 并将它们导航到对应的视图或页面是每个单页应用程序 (SPA) 框架的基本功能。Blazor Server 和 WebAssembly 应用程序也同样支持使用一些内置组件和服务进行路由。在本教程中,我将向您介绍在 Blazor 应用程序中实现路由所需了解的所有内容。
Blazor 应用程序中的路由配置
在开始为不同的 Blazor 组件/页面创建路由之前,我们需要了解如何将 Blazor Server 应用程序集成到 ASP.NET Core Endpoint 路由中。Blazor Server 应用程序通过 SignalR 连接与客户端进行通信,为了接受 Blazor 组件传入的连接,我们在 Startup.cs 文件的 Configure 方法中调用了 MapBlazorHub 方法,如下所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
默认配置将所有请求都转发到一个 Razor 页面,该页面扮演 Blazor Server 应用程序服务端主机的角色。按照惯例,该主页是 _Host.cshtml,它位于应用程序的 Pages 文件夹中。该主文件中指定的路由称之为应急路由,在路由匹配中具有极低的优先级,这意味着当没有其他路由匹配时,才会使用该路由。
Blazor 路由组件介绍
Router2 组件是 Blazor 中的内置组件之一,用在 Blazor 应用程序的 App 组件之中。该组件启用了 Blazor 应用程序中的路由,并提供与当前导航状态相对应的路由数据。它拦截传入的请求并呈现与请求 URL 相匹配的页面。
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
下表显示了 Router 组件的属性。
属性 | 说明 |
---|---|
AdditionalAssemblies | 获取或设置其他程序集的集合,这些程序集应在搜索可与 URI 匹配的组件时搜索。 |
AppAssembly | 获取或设置应在其中搜索与 URI 匹配的组件的程序集。 |
Found | 获取或设置当为请求的路由找到匹配项时要显示的内容。 |
Navigating | 获取或设置异步导航正在进行时显示的内容。 |
NotFound | 获取或设置当没有为请求的路由找到匹配项时要显示的内容。 |
OnNavigateAsync | 获取或设置在导航到新页之前应调用的处理程序。 |
当编译 Blazor 组件 (.razor) 时,它们生成的 C# 类会保存在 obj\Debug\net5.0\Razor\Pages 文件夹中。
如果您打开任意一个已编译的文件,将会注意到在编译之后,所有带有 @page 指令的组件都生成了一个带有 RouteAttribute 特性的类。
当应用程序启动时,会扫描通过 AppAssembly 属性指定的程序集,从所有指定了 RouteAttribute 特性的类中收集路由信息。
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
如果您创建了独立的组件类库,并希望应用程序从这些程序集中扫描和加载路由,那么您可以使用 AdditionalAssemblies 属性来接受一个 Assembly 对象集合。
下面是一个从定义在组件类库中的两个可路由组件(Component1 和 Component2)加载路由信息的示例。
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"
AdditionalAssemblies="new[] { typeof(Component1).Assembly, typeof(Component2).Assembly }">
</Router>
在运行时,RouteView 组件从 Router 接收 RouteData 以及任意路由参数,并使用组件中定义的布局渲染指定的组件。如果未定义布局,则使用 DefaultLayout 属性指定的布局。默认的布局通常是 Shared 文件夹中的 MainLayout 组件,不过您也可以创建并指定一个自定义布局。
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
Found 模板用于在找到匹配的路由时显示其内容,正如您在下图中所看到的那样,其中找到了一个匹配路由,并在浏览器中呈现了一个 Counter 页面。
NotFound 模板用于在没有找到匹配的路由时显示内容。默认情况下,NotFound 模板仅显示一条消息,如下面的截图所示。
我们还可以创建自定义错误的布局和页面,以显示自定义错误页面。让我们在 Shared 文件夹中创建一个新的名为 ErrorLayout.razor 的自定义布局。
ErrorLayout.razor
@inherits LayoutComponentBase
<main role="main" class="container">
<div class="text-center">
@Body
</div>
</main>
然后将 LayoutView 组件的 Layout 属性改为 ErrorLayout,并将 LayoutView 里的内容修改如下:
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(ErrorLayout)">
<h1 class="display-1">404</h1>
<h1 class="display-4">Not Found</h1>
<p class="lead">
Oops! Looks like this page doesn't exist.
</p>
</LayoutView>
</NotFound>
</Router>
现在,如果您在浏览器中运行应用程序,并尝试访问一个未在应用中任何位置指定过的 URL,那么您将会看到一个自定义的 404 错误页面,如下所示。
所有 Blazor 应用程序都应将 PreferExactMatches 特性显式地设置为 @true
,以便路由匹配更倾向于精确匹配,而不是通配符匹配。根据 Microsoft 官方文档,此特性从 .NET 6 开始将不可用,路由器将总是更倾向于精确匹配。
定义路由、参数和约束
在我们学习如何为 Blazor 组件定义路由之前,我们需要确保下面的 base
标签在每个页面都可用,以便正确地解析 URL。如果创建的是 Blazor Server 应用程序,那么您可以将此标签添加到 Pages/_Host.cshtml 文件的 head
部分,如果是 Blazor WebAssembly 应用程序,则可以将此标签添加到 wwwroot/index.html 文件中。
<base href="~/" />
要定义路由,我们可以使用 @page 指令,如下面的 Counter 组件示例所示。
@page "/counter"
<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++;
}
}
现在我们就可以使用 /counter URL 访问 Counter 组件了。
我们还可以使用多个 @page 指令定义多个路由模板,如下面例所示。
@page "/counter"
@page "/mycounter"
这意味着现在也可以使用 /mycounter URL 访问同一个 Counter 组件:
使用路由参数将数据从一个页面传递到另一个页面是十分常见的做法,Blazor 路由模板支持路由参数。路由参数名称不区分大小写,一旦我们定义了路由参数,路由器就会自动填充对应的具有相同名称的组件属性。例如,在下面的代码片段中,我们在组件中定义了一个路由参数 title,并创建了一个对应的属性 Title。此属性将自动使用路由参数文本的值填充。然后,我们在 h1
元素中显示 Title 属性作为页面的标题。
@page "/counter/{title}"
<h1>@Title</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
[Parameter]
public string Title { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
运行应用程序,并尝试在地址栏中 /counter/ 之后指定任意的字符串,您将看到路由参数的值会显示为页面标题。
我们还可以定义可选的路由参数,如下例所示,其中 title
是可选参数,因为在此参数名称后面带有问号 (?)。假如我们不提供此路由参数的值,该参数将在 OnInitialized 方法中使用默认值 Counter 进行初始化。
@page "/counter/{title?}"
<h1>@Title</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
[Parameter]
public string Title { get; set; }
protected override void OnInitialized()
{
Title = Title ?? "Counter";
}
private void IncrementCount()
{
currentCount++;
}
}
Blazor 还支持路由约束,在路由上强制类型匹配。在下面的代码片段中,我创建了一个 int
类型的路由参数 start
,这意味着现在我只能为此路由参数提供整数值。计数器现在将以路由参数中指定的值开始计数。
@page "/counter/{start:int}"
<h1>Counter</h1>
<p>Current count: @Start</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
[Parameter]
public int Start { get; set; }
private void IncrementCount()
{
Start++;
}
}
在浏览器中运行应用程序,并在 URL 中指定任一整数值,比如 /counter/4,您会看到计数器将以该起始值递增。
下表显示了 Blazor 路由约束支持的类型。
约束 | 示例 | 匹配项示例 |
---|---|---|
bool |
{active:bool} |
true ,FALSE |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
double |
{weight:double} |
1.234 , -1,001.01e8 |
float |
{weight:float} |
1.234 , -1,001.01e8 |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 , {CD2C1638-1638-72D5-1638-DEADBEEF1638} |
int |
{id:int} |
123456789 , -123456789 |
long |
{ticks:long} |
123456789 , -123456789 |
还可以定义多个路由参数,如下例所示,我们将 start
和 increment
定义为 int
类型的参数。
@page "/counter/{start:int}/{increment:int}"
<h1>Counter</h1>
<p>Current count: @Start</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
[Parameter]
public int Start { get; set; }
[Parameter]
public int Increment { get; set; }
private void IncrementCount()
{
Start+=Increment;
}
}
如下所示,运行应用程序并在 URL 地址中指定 start
和 increment
的值,您会注意到,当您每次点击 Click me 按钮时,计数器不仅会以数字 2
开始计数,而且会以 3
递增。
Blazor NavigationManager 服务概述
NavigationManager 服务允许我们在 C# 代码中管理 URI 和导航。NavigationManager 类具有以下常见的属性、方法和事件。
名称 | 类型 | 说明 |
---|---|---|
BaseUri | 属性 | 获取或设置当前的基 URI。BaseUri 始终表示为字符串形式的绝对 URI,以斜杠结尾。 通常,这与文档中 <base> 元素的 href 特性相对应。 |
Uri | 属性 | 获取或设置当前 URI。 Uri 始终以字符串形式表示为绝对 URI。 |
NavigateTo | 方法 | 导航到指定 URI。 |
ToAbsoluteUri | 方法 | 将相对 URI 转换为绝对 URI。 |
ToBaseRelativePath | 方法 | 给定基 URI (比如,前面的 BaseUri 的返回值),将绝对 URI 转换为相对于基 URI 前缀的 URI。 |
LocationChanged | 事件 | 当导航位置变化时触发的事件。 |
让我们来创建一个页面,查看一下以上属性和方法的一些实际行为。创建一个新的 Blazor 组件并使用 @inject
指令注入 NavigationManager 服务。 尝试在页面上打印出 Uri 和 BaseUri 属性,来查看一下它们返回的是什么类型的 URI。
@page "/navigationmanager"
@inject NavigationManager nvm
<h3>Navigation Manager</h3>
<br />
<p>@nvm.Uri</p>
<p>@nvm.BaseUri</p>
运行应用程序,您将在浏览器中看到类似以下内容的输出。Uri 属性显示当前页面的绝对 URI,而 BaseUri 属性显示当前的基 URI。
在页面上添加两个按钮 Home Page 和 Counter Page,并在 @code 代码块中添加它们的 onclick
事件处理方法。在事件处理方法中,我们可以在 C# 代码中使用 NavigateTo 方法将用户重定向到其它的 Blazor 组件。
@page "/navigationmanager"
@inject NavigationManager nvm
<h3>Navigation Manager</h3>
<br />
<p>@nvm.Uri</p>
<p>@nvm.BaseUri</p>
<button class="btn btn-primary" @onclick="GoToHome">
Home Page
</button>
<button class="btn btn-primary" @onclick="GoToCounter">
Counter Page
</button>
@code {
private void GoToHome()
{
nvm.NavigateTo("/");
}
private void GoToCounter()
{
nvm.NavigateTo("counter");
}
}
运行应用程序并试着点击这两个按钮,将按预期的那样,您可以导航到主页和计数器页面。
如果不想以编程方式处理导航,而想在 HTML 中生成超链接,则可以使用 Blazor NavLink 组件。 NavLink 组件类似于 HTML 中的 <a>
元素,具有一些很酷的功能。如果 NavLink 的 href 特性值与当前的 URL 相匹配,则会自动切换该元素的 active CSS 类(class)。这就使得我们可以在当前选中的链接上应用不同的样式。您可以在 Shared/NavMenu.razor 文件中看到这个组件的用法。
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
</ul>
</div>
NavLink 组件还有一个 Match 属性,可以设置为以下选项之一:
- NavLinkMatch.All:指定当 NavLink 与整个当前 URL 匹配时应处于活动状态。
- NavLinkMatch.Prefix(默认值):指定当 NavLink 与当前 URL 的任意前缀匹配时应处于活动状态。
Match 属性:获取或设置一个值,该值表示 URL 匹配行为。
总结
在本教程中,我尝试介绍 Blazor 应用程序中的多种路由功能,还介绍了开发者可用的与路由相关的一些组件和服务。我希望您现在能够更熟练地定义路由、参数和约束。如果您喜欢本教程,请与他人分享以传播知识。
相关阅读:
- Blazor Server 和 WebAssembly 应用程序入门指南
- Blazor 组件入门指南
- Blazor 数据绑定开发指南
- Blazor 事件处理开发指南
- Blazor 组件之间使用 EventCallback 进行通信
- Blazor 路由及导航开发指南
- Blazor 模板化组件开发指南
- Blazor Server 应用程序中进行 HTTP 请求
- Blazor WebAssembly 应用程序中进行 HTTP 请求
- Blazor 组件库开发指南
-
https://www.ezzylearning.net/tutorial/a-developers-guide-to-blazor-routing-and-navigation A Developer’s Guide To Blazor Routing and Navigation ↩
-
https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.components.routing.router ↩