ASP.NET Core Blazor 路由

在本文中,学习如何管理请求路由以及如何使用 NavLink 组件在 Blazor 应用中创建导航链接。

路由模板

通过 Router 组件可在 Blazor 应用中路由到 Razor 组件。 Router 组件在 Blazor 应用的 App 组件中使用。

App.razor:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <p>Sorry, there's nothing at this address.</p>
    </NotFound>
</Router>

备注

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅 从 ASP.NET Core 3.1 迁移到 5.0

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <p>Sorry, there's nothing at this address.</p>
    </NotFound>
</Router>

编译带有 @page 指令的 Razor 组件 (.razor) 时,将为生成的组件类提供一个 RouteAttribute 来指定组件的路由模板。

当应用启动时,将扫描指定为 路由器的 AppAssembly 的程序集,来收集具有 RouteAttribute 的应用组件的路由信息。

在运行时,RouteView 组件:

  • Router 接收 RouteData 以及所有路由参数。
  • 使用指定的组件的布局来呈现该组件,包括任何后续嵌套布局。

对于没有使用 @layout 指令指定布局的组件,可选择使用布局类指定一个 DefaultLayout 参数。 框架的 Blazor 项目模板会指定 MainLayout 组件 (Shared/MainLayout.razor) 作为应用的默认布局。 有关布局的详细信息,请参阅 ASP.NET Core Blazor 布局

组件支持使用多个 @page 指令的多个路由模板。 以下示例组件会对 /BlazorRoute/DifferentBlazorRoute 的请求进行加载。

Pages/BlazorRoute.razor:

@page "/BlazorRoute"
@page "/DifferentBlazorRoute"

<h1>Blazor routing</h1>
@page "/BlazorRoute"
@page "/DifferentBlazorRoute"

<h1>Blazor routing</h1>

重要

若要正确解析 URL,应用必须在其 wwwroot/index.html 文件 (Blazor WebAssembly) 或 Pages/_Host.cshtml 文件 (Blazor Server) 中加入 <base> 标记,并在 href 属性中指定应用基路径。 有关详细信息,请参阅 托管和部署 ASP.NET Core Blazor

备注

Router 不与查询字符串值交互。 若要使用查询字符串,请参阅查询字符串和分析参数部分。

在找不到内容时提供自定义内容

如果找不到所请求路由的内容,则 Router 组件允许应用指定自定义内容。

App 组件中,在 Router 组件的 NotFound 模板中设置自定义内容。

App.razor:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <h1>Sorry</h1>
        <p>Sorry, there's nothing at this address.</p> b
    </NotFound>
</Router>

备注

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅 从 ASP.NET Core 3.1 迁移到 5.0

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <h1>Sorry</h1>
        <p>Sorry, there's nothing at this address.</p> b
    </NotFound>
</Router>

任意项都可用作 <NotFound> 标记的内容,例如其他交互式组件。 若要将默认布局应用于 NotFound 内容,请参阅 ASP.NET Core Blazor 布局

从多个程序集路由到组件

使用 AdditionalAssemblies 参数为 Router 组件指定搜索可路由组件时要考虑的其他程序集。 除了指定至 AppAssembly 的程序集外,还会扫描其他程序集。 在以下示例中,Component1 是在引用的组件类库中定义的可路由组件。 以下 AdditionalAssemblies 示例为 Component1 提供路由支持。

App.razor:

<Router
    AppAssembly="@typeof(Program).Assembly"
    AdditionalAssemblies="new[] { typeof(Component1).Assembly }">
    @* ... Router component elements ... *@
</Router>

备注

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅 从 ASP.NET Core 3.1 迁移到 5.0

路由参数

路由器使用路由参数以相同的名称填充相应的组件参数。 路由参数名不区分大小写。 在下面的示例中,text 参数将路由段的值赋给组件的 Text 属性。 对 /RouteParameter/amazing发出请求时,<h1> 标记内容呈现为 Blazor is amazing!

Pages/RouteParameter.razor:

@page "/RouteParameter/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }
}
@page "/RouteParameter/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }
}

支持可选参数。 在下面的示例中,text 可选参数将 route 段的值赋给组件的 Text 属性。 如果该段不存在,则将 Text 的值设置为 fantastic

Pages/RouteParameter.razor:

@page "/RouteParameter/{text?}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }

    protected override void OnInitialized()
    {
        Text = Text ?? "fantastic";
    }
}

不支持可选参数。 在下述示例中,应用了两个 @page 指令。 第一个指令允许导航到没有参数的组件。 第二个指令将 {text} 路由参数分配给组件的 Text 属性。

Pages/RouteParameter.razor:

@page "/RouteParameter"
@page "/RouteParameter/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }

    protected override void OnInitialized()
    {
        Text = Text ?? "fantastic";
    }
}

使用 OnParametersSet 而不是 OnInitialized{Async},以允许应用使用不同的可选参数值导航到同一组件。 根据上述示例,当用户应该能够从 /RouteParameter 导航到 /RouteParameter/amazing 或从 /RouteParameter/amazing 导航到 /RouteParameter 时使用 OnParametersSet

protected override void OnParametersSet()
{
    Text = Text ?? "fantastic";
}

备注

路由参数不适用于查询字符串值。 若要使用查询字符串,请参阅查询字符串和分析参数部分。

路由约束

路由约束强制在路由段和组件之间进行类型匹配。

在以下示例中,到 User 组件的路由仅在以下情况下匹配:

  • 请求 URL 中存在 Id 路由段。
  • Id 段是一个整数 (int) 类型。

Pages/User.razor:

@page "/user/{Id:int}"

<h1>User Id: @Id</h1>

@code {
    [Parameter]
    public int Id { get; set; }
}
@page "/user/{Id:int}"

<h1>User Id: @Id</h1>

@code {
    [Parameter]
    public int Id { get; set; }
}

备注

路由约束不适用于查询字符串值。 若要使用查询字符串,请参阅查询字符串和分析参数部分。

下表中显示的路由约束可用。 有关与固定区域性匹配的路由约束,请参阅表下方的警告了解详细信息。

约束 示例 匹配项示例 固定条件
区域性
匹配
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

警告

验证 URL 的路由约束并将转换为始终使用固定区域性的 CLR 类型(例如 intDateTime)。 这些约束假定 URL 不可本地化。

路由约束也适用于可选参数。 在下面的示例中,Id 是必需的,但 Option 是一个可选的布尔路由参数。

Pages/User.razor:

@page "/user/{Id:int}/{Option:bool?}"

<p>
    Id: @Id
</p>

<p>
    Option: @Option
</p>

@code {
    [Parameter]
    public int Id { get; set; }

    [Parameter]
    public bool Option { get; set; }
}

使用包含点的 URL 进行路由

对于托管的 Blazor WebAssembly 和 Blazor Server应用,服务器端默认路由模板假定如果请求 URL 的最后一段包含一个点 (.),则请求一个文件。 例如,URL https://localhost.com:5001/example/some.thing 由路由器解释为名为 some.thing 的文件的请求。 在没有额外配置的情况下,如果 some.thing 是指通过 @page 指令路由到一个组件,且 some.thing 是一个路由参数值,那么应用将返回“404 - 未找到”响应。 若要使用具有包含点的一个或多个参数的路由,则应用必须使用自定义模板配置该路由。

请考虑下面的 Example 组件,它可以从 URL 的最后一段接收路由参数。

Pages/Example.razor:

@page "/example/{param?}"

<p>
    Param: @Param
</p>

@code {
    [Parameter]
    public string Param { get; set; }
}
@page "/example"
@page "/example/{param}"

<p>
    Param: @Param
</p>

@code {
    [Parameter]
    public string Param { get; set; }
}

若要允许托管的 Blazor WebAssembly 解决方案的 Server 应用在 param 路由参数中使用一个点来路由请求,请添加一个回退文件路由模板,在该模板的 Startup.Configure 中包含该可选参数。

Startup.cs:

endpoints.MapFallbackToFile("/example/{param?}", "index.html");

若要配置 Blazor Server应用,使其在 param 路由参数中使用一个点来路由请求,请添加一个回退页面路由模板,该模板具有Startup.Configure 中的可选参数。

Startup.cs:

endpoints.MapFallbackToPage("/example/{param?}", "/_Host");

有关详细信息,请参阅 ASP.NET Core 中的路由

catch-all 路由参数

组件支持可跨多个文件夹边界捕获路径的 catch-all 路由参数。

Catch-all 路由参数是:

  • 以与路由段名称匹配的方式命名。 命名不区分大小写。
  • string 类型。 框架不提供自动强制转换。
  • 位于 URL 的末尾。

Pages/CatchAll.razor:

@page "/catch-all/{*pageRoute}"

@code {
    [Parameter]
    public string PageRoute { get; set; }
}

对于具有 /catch-all/{*pageRoute} 路由模板的 URL /catch-all/this/is/a/testPageRoute 的值设置为 this/is/a/test

对捕获路径的斜杠和段进行解码。 对于 /catch-all/{*pageRoute} 的路由模板,URL /catch-all/this/is/a%2Ftest%2A 会生成 this/is/a/test*

ASP.NET Core 5.0 或更高版本中支持 catch-all 路由参数。 有关详细信息,请选择本文的 5.0 版本。

URI 和导航状态帮助程序

在 C# 代码中使用 NavigationManager 来来管理 URI 和导航。 NavigationManager 提供下表所示的事件和方法。

成员 描述
Uri 获取当前绝对 URI。
BaseUri 获取可在相对 URI 路径之前添加用于生成绝对 URI 的基 URI(带有尾部反斜杠)。 通常,BaseUri 对应于 wwwroot/index.html (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 中文档的 <base> 元素上的 href 属性。
NavigateTo 导航到指定 URI。 如果 forceLoadtrue,则:
  • 客户端路由会被绕过。
  • 无论 URI 是否通常由客户端路由器处理,浏览器都必须从服务器加载新页面。
LocationChanged 导航位置更改时触发的事件。
ToAbsoluteUri 将相对 URI 转换为绝对 URI。
ToBaseRelativePath 给定基 URI(例如,之前由 BaseUri 返回的 URI),将绝对 URI 转换为相对于基 URI 前缀的 URI。

对于 LocationChanged 事件,LocationChangedEventArgs 提供了下述导航事件信息:

以下组件:

  • 使用 NavigateTo 选择按钮后,导航到应用的 Counter 组件 (Pages/Counter.razor)。
  • 通过订阅 NavigationManager.LocationChanged 来处理位置更改事件。
    • 在框架调用 Dispose 时,解除挂接 HandleLocationChanged 方法。 解除挂接该方法可允许组件进行垃圾回收。

    • 选择该按钮时,记录器实现会记录以下信息:

      BlazorSample.Pages.Navigate: Information: URL of new location: https://localhost:5001/counter

Pages/Navigate.razor:

@page "/navigate"
@using Microsoft.Extensions.Logging 
@implements IDisposable
@inject ILogger<Navigate> Logger
@inject NavigationManager NavigationManager

<h1>Navigate in component code example</h1>

<button class="btn btn-primary" @onclick="NavigateToCounterComponent">
    Navigate to the Counter component
</button>

@code {
    private void NavigateToCounterComponent()
    {
        NavigationManager.NavigateTo("counter");
    }

    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += HandleLocationChanged;
    }

    private void HandleLocationChanged(object sender, LocationChangedEventArgs e)
    {
        Logger.LogInformation("URL of new location: {Location}", e.Location);
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= HandleLocationChanged;
    }
}
@page "/navigate"
@using Microsoft.Extensions.Logging 
@implements IDisposable
@inject ILogger<Navigate> Logger
@inject NavigationManager NavigationManager

<h1>Navigate in component code example</h1>

<button class="btn btn-primary" @onclick="NavigateToCounterComponent">
    Navigate to the Counter component
</button>

@code {
    private void NavigateToCounterComponent()
    {
        NavigationManager.NavigateTo("counter");
    }

    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += HandleLocationChanged;
    }

    private void HandleLocationChanged(object sender, LocationChangedEventArgs e)
    {
        Logger.LogInformation("URL of new location: {Location}", e.Location);
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= HandleLocationChanged;
    }
}

要详细了解组件处置,请参阅 ASP.NET Core Razor 组件生命周期

查询字符串和分析参数

可从 NavigationManager.Uri 属性中获取请求的查询字符串:

@inject NavigationManager NavigationManager

...

var query = new Uri(NavigationManager.Uri).Query;

若要分析查询字符串的参数,一种方法是结合使用 URLSearchParamsJavaScript (JS) 互操作

export createQueryString = (string queryString) => new URLSearchParams(queryString);

有关 JavaScript 隔离与 JavaScript 模块的详细信息,请参阅 在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数

<script>
  window.createQueryString = (queryString) => {
    return new URLSearchParams(queryString);
  };
</script>

有关详细信息,请参阅 在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数

用户与 <Navigating> 内容的交互

Router 组件可向用户指示页面正在转换。

App 组件 (App.razor) 的顶部,为 Microsoft.AspNetCore.Components.Routing 命名空间添加 @using 指令:

@using Microsoft.AspNetCore.Components.Routing

向组件添加一个 <Navigating> 标记,其中包含在页面转换事件期间显示的标记。 有关详细信息,请参阅 Navigating(API 文档)。

App 组件 (App.razor) 的路由器元素内容 (<Router>...</Router>) 中:

<Navigating>
    <p>Loading the requested page&hellip;</p>
</Navigating>

有关使用 Navigating 属性的示例,请参阅 在 ASP.NET Core Blazor WebAssembly 中延迟加载程序集

使用 OnNavigateAsync 处理异步导航事件

Router 组件支持 OnNavigateAsync 功能。 当用户执行以下操作时,将调用 OnNavigateAsync 处理程序:

  • 通过直接在自己的浏览器中导航到某个路由,首次访问该路由。
  • 使用链接或 NavigationManager.NavigateTo 调用导航到一个新路由。

App 组件 (App.razor) 中:

<Router AppAssembly="@typeof(Program).Assembly" 
    OnNavigateAsync="@OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        ...
    }
}

备注

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅 从 ASP.NET Core 3.1 迁移到 5.0

有关使用 OnNavigateAsync 的示例,请参阅 在 ASP.NET Core Blazor WebAssembly 中延迟加载程序集

在 Blazor Server 应用或托管的 Blazor WebAssembly 应用中的服务器上预呈现内容时,OnNavigateAsync 会执行两次:

  • 第一次是在请求的终结点组件最初静态呈现时。
  • 第二次是浏览器呈现终结点组件时。

为了防止 OnNavigateAsync 中的开发人员代码执行两次,App 组件可以存储 NavigationContext,以供 OnAfterRender/OnAfterRenderAsync 使用,可以在其中检查 firstRender。 有关详细信息,请参阅 Blazor 生命周期一文中的检测应用何时预呈现

处理 OnNavigateAsync 中的取消

传递到 OnNavigateAsync 回调的 NavigationContext 对象包含的 CancellationToken 在发生新导航事件时进行设置。 设置此取消标记时,OnNavigateAsync 回调必须引发,以避免在过时的导航中继续运行 OnNavigateAsync 回调。

如果用户导航到某个终结点,但随后立即导航到新的终结点,则应用不应继续为第一个终结点运行 OnNavigateAsync 回叫。

在下面的 App 组件示例中:

  • 取消标记在对 PostAsJsonAsync 的调用中传递,用户离开 /about 终结点后它可取消 POST。
  • 如果用户离开 /store 终结点,则会在产品预提取操作期间设置取消操作。

App.razor:

@inject HttpClient Http
@inject ProductCatalog Products

<Router AppAssembly="@typeof(Program).Assembly" 
    OnNavigateAsync="@OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats = { Page = "/about" };
            await Http.PostAsJsonAsync("api/visited", stats, 
                context.CancellationToken);
        }
        else if (context.Path == "/store")
        {
            var productIds = [345, 789, 135, 689];

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}

备注

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅 从 ASP.NET Core 3.1 迁移到 5.0

备注

如果取消 NavigationContext 中的取消标记会导致意外的行为(例如,呈现上一次导航中的组件),则不会引发。

创建导航链接时,请使用 NavLink 组件代替 HTML 超链接元素 (<a>)。 NavLink 组件的行为方式类似于 <a> 元素,但它根据其 href 是否与当前 URL 匹配来切换 active CSS 类。 active 类可帮助用户了解所显示导航链接中的哪个页面是活动页面。 也可以选择将 CSS 类名分配到 NavLink.ActiveClass,以便在当前路由与 href 匹配时将自定义 CSS 类应用到呈现的链接。

以下 NavMenu 组件创建 Bootstrap 导航栏,该导航栏演示如何使用 NavLink 组件:

<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="component" Match="NavLinkMatch.Prefix">
                <span class="oi oi-plus" aria-hidden="true"></span> Link Text
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private string NavMenuCssClass;
    private void ToggleNavMenu() {}
}
<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="component" Match="NavLinkMatch.Prefix">
                <span class="oi oi-plus" aria-hidden="true"></span> Link Text
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private string NavMenuCssClass;
    private void ToggleNavMenu() {}
}

备注

NavMenu 组件 (NavMenu.razor) 将在 Blazor 项目模板生成的应用的 Shared 文件夹中提供。

有两个 NavLinkMatch 选项可分配给 <NavLink> 元素的 Match 属性:

在前面的示例中,Home NavLink href=""与主页 URL 匹配,并且仅在应用的默认基路径 URL(例如 https://localhost:5001/)处接收 active CSS 类。 当用户访问带有 component 前缀的任何 URL(例如,https://localhost:5001/componenthttps://localhost:5001/component/another-segment)时,第二个 NavLink 接收 active 类。

其他 NavLink 组件属性会传递到呈现的定位标记。 在以下示例中,NavLink 组件包括 target 属性:

<NavLink href="example-page" target="_blank">Example page</NavLink>

呈现以下 HTML 标记:

<a href="example-page" target="_blank">Example page</a>

警告

由于 Blazor 呈现子内容的方式,如果在 NavLink(子)组件的内容中使用递增循环变量,则在 for 循环内呈现 NavLink 组件需要本地索引变量:

@for (int c = 0; c < 10; c++)
{
    var current = c;
    <li ...>
        <NavLink ... href="@c">
            <span ...></span> @current
        </NavLink>
    </li>
}

子内容中使用循环变量的任何子组件(而不仅仅是 NavLink 组件)都要求必须在此方案中使用索引变量。

或者,将 foreach 循环用于 Enumerable.Range

@foreach(var c in Enumerable.Range(0,10))
{
    <li ...>
        <NavLink ... href="@c">
            <span ...></span> @c
        </NavLink>
    </li>
}

ASP.NET Core 终结点路由集成

本部分仅适用于 Blazor Server应用。

Blazor Server 已集成到 ASP.NET Core 终结点路由中。 ASP.NET Core 应用配置为接受 Startup.Configure 中带有 MapBlazorHub 的交互式组件的传入连接。

Startup.cs:

using Microsoft.AspNetCore.Builder;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}
using Microsoft.AspNetCore.Builder;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}

典型的配置是将所有请求路由到 Razor 页面,该页面充当 Blazor Server应用的服务器端部分的主机。 按照约定,主机页面通常在应用的 Pages 文件夹中被命名为 _Host.cshtml

主机文件中指定的路由称为 回退路由,因为它在路由匹配中以较低的优先级运行。 其他路由不匹配时,会使用回退路由。 这让应用能够使用其他控制器和页面,而不会干扰 Blazor Server应用中的组件路由。

若要了解如何为非根 URL 服务器托管配置 MapFallbackToPage,请参阅 托管和部署 ASP.NET Core Blazor