ASP.NET 核心 Blazor 路由

注意

此版本不是本文的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本

本文介绍 Blazor 应用请求路由,并指导静态路由与交互式路由、ASP.NET 核心终结点路由集成、导航事件以及组件路由模板和约束 Razor 。

可以通过使用 Blazor 指令为应用中的每个可访问组件提供路由模板来实现 @page 中的路由。 编译具有 Razor 指令的 @page 文件时,将为生成的类提供指定路由模板的 RouteAttribute。 在运行时,路由器将使用 RouteAttribute 搜索组件类,并呈现具有与请求的 URL 匹配的路由模板的任何组件。

以下 HelloWorld 组件使用 /hello-world 的路由模板,可以通过相对 URL /hello-world 访问其呈现的网页。

HelloWorld.razor:

@page "/hello-world"

<h1>Hello World!</h1>

无论是否将组件作为链接添加到应用的 UI 导航,上述组件都会在浏览器中加载/hello-world

静态路由与交互式路由

本部分适用于 Blazor Web App。

如果启用预渲染,则 Blazor 路由器(Router 组件,<Router> 中的 Routes.razor)将在静态服务器端渲染(静态 SSR)期间对组件执行静态路由。 这种类型的路由称为“静态路由”

Routes 组件分配交互式呈现模式时,Blazor 路由器将在服务器上的静态 SSR 及静态路由后变为交互式。 这种类型的路由称为“交互式路由”

静态路由器使用终结点路由和 HTTP 请求路径来确定要呈现的组件。 当路由器变为交互式路由器时,它将使用文档的 URL(浏览器地址栏中的 URL)来确定要呈现的组件。 这意味着,如果文档的 URL 动态更改为另一个有效的内部 URL,交互式路由器可以动态更改呈现的组件,并且该过程中无需执行 HTTP 请求来提取新页面的内容。

交互式路由还会阻止预渲染,因为它不通过正常的页面请求从服务器获取新页面内容。 有关详细信息,请参阅 ASP.NET 核心 Blazor 预呈现状态持久性

ASP.NET Core 终结点路由集成

本部分适用于在线路上运行的 Blazor Web App。

Blazor Web App 已集成到 ASP.NET Core 终结点路由中。 ASP.NET Core 应用配置了可路由组件的终结点和根组件,以便在文件中呈现这些终结点MapRazorComponentsProgram。 默认根组件(加载的第一个组件)是 App 组件 (App.razor):

app.MapRazorComponents<App>();

本部分适用于 Blazor Server 通过线路运行的应用。

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

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

本部分适用于 Blazor Server 通过线路运行的应用。

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

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

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

有关配置 MapFallbackToPage 非根 URL 服务器托管的信息,请参阅 ASP.NET Core Blazor 应用基路径

路由模板

Router 组件允许路由到 Razor 组件,并且位于应用的 Routes 组件 (Components/Routes.razor)。

Router 组件允许路由到 Razor 组件。 Router 组件在 App 组件 (App.razor) 中使用。

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

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

在运行时,RouteView 组件:

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

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

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

BlazorRoute.razor:

@page "/blazor-route"
@page "/different-blazor-route"

<h1>Routing Example</h1>

<p>
    This page is reached at either <code>/blazor-route</code> or 
    <code>/different-blazor-route</code>.
</p>

重要

若要正确解析 URL,应用必须包含 <base> 标记(<head> 内容的位置),并在 href 属性中指定应用基路径。 有关详细信息,请参阅 ASP.NET Core Blazor 应用基路径

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

作为使用 @page 指令将路由模板指定为字符串文本的替代方法,可以使用 @attribute 指令指定基于常量的路由模板。

在以下示例中,组件中的 @page 指令替换为 @attribute 指令和 Constants.CounterRoute 中基于常量的路由模板,该模板在应用中的其他位置设置为“/counter”:

- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]

注意

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

将元素聚焦到导航上

在页面之间进行导航后,FocusOnNavigate 组件基于 CSS 选择器将 UI 焦点设置到元素。

<FocusOnNavigate RouteData="routeData" Selector="h1" />

Router 组件导航到新页面时,FocusOnNavigate 组件将焦点设置到页面的顶层标题 (<h1>)。 这是一种常见策略,可确保在使用屏幕阅读器时公布页面导航。

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

对于无法找到内容的请求时,可以将 Razor 组件分配给 Router 组件的 NotFoundPage 参数。 该参数与开发人员代码中调用的方法NavigationManager.NotFound协同工作,该方法会触发“未找到”响应。 NavigationManager.NotFound在下一篇文章ASP.NET Core Blazor导航中有所描述。

项目 Blazor 模板包括一个 NotFound.razor 页面。 每当调用此页NavigationManager.NotFound时,页面会自动渲染,从而可以处理缺失路由,确保一致的用户体验。

NotFound.razor:

@page "/not-found"
@layout MainLayout

<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

NotFound 组件分配给路由器 NotFoundPage 的参数。 NotFoundPage 支持可跨状态代码页面重新执行中间件使用的路由,包括非 Blazor 中间件。

在以下示例中,上述 NotFound 组件存在于应用的 Pages 文件夹中,并传递给 NotFoundPage 参数:

<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

有关详细信息,请参阅有关 ASP.NET Core Blazor 导航的下一篇文章。

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

Router 组件的 NotFound 参数设置自定义内容:

<Router ...>
    ...
    <NotFound>
        ...
    </NotFound>
</Router>

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

Blazor Web App不使用 NotFound 参数(<NotFound>...</NotFound> 标记),但支持该参数†为了在 .NET 8/9 中向后兼容,以避免在框架中发生中断性变更。 服务器端 ASP.NET 核心中间件管道处理服务器上的请求。 使用服务器端技术来处理错误的请求。

†此上下文中的支持意味着放置 <NotFound>...</NotFound> 标记不会导致异常,但使用该标记也没有效果。

有关详细信息,请参阅以下资源:

从多个程序集路由到组件

本部分适用于 Blazor Web App。

使用 Router 组件的 AdditionalAssemblies 参数和终结点约定生成器 AddAdditionalAssemblies 来发现其他程序集中的可路由组件。 以下小节说明了何时以及如何使用每个 API。

静态路由

若要从用于静态服务器端呈现(静态 SSR)的其他程序集中发现可路由组件,即使相应路由器过后会变成用以实现交互式呈现的交互式组件,也必须将程序集披露给 Blazor 框架。 使用在服务器项目的 AddAdditionalAssemblies 文件中链接到 MapRazorComponents 的其他程序集调用 Program 方法。

以下示例包括使用项目 BlazorSample.Client 文件的 _Imports.razor 项目程序集中的可路由组件:

app.MapRazorComponents<App>()
    .AddAdditionalAssemblies(typeof(BlazorSample.Client._Imports).Assembly);

注意

上述指南也适用于组件类库方案。 支持静态服务器端呈现(静态 SSR)的 ASP.NET Core Razor 类库 (RCL) 中提供了有关类库和静态 SSR 的其他重要指南。

交互式路由

交互式呈现模式可以分配给 Routes 组件 (Routes.razor),这会使 Blazor 路由器在服务器上的静态 SSR 和静态路由后变为交互式。 例如,<Routes @rendermode="InteractiveServer" /> 会将交互式服务器端呈现(交互式 SSR)分配给 Routes 组件。 Router 组件从 Routes 组件继承了交互式服务器端呈现(SSR)。 路由器将在服务器上的静态路由后变为交互式。

交互式路由的内部导航不涉及从服务器请求新页面内容。 因此,对于内部页面请求,不会进行预呈现。 有关详细信息,请参阅 ASP.NET 核心 Blazor 预呈现状态持久性

如果在服务器项目中定义了 Routes 组件,则 AdditionalAssemblies 组件的 Router 参数应包括 .Client 项目的程序集。 这样,路由器就可以在以交互方式呈现时正常工作。

在以下示例中,Routes 组件位于服务器项目中,_Imports.razor 项目的 BlazorSample.Client 文件指示要搜索可路由组件的程序集:

<Router
    AppAssembly="..."
    AdditionalAssemblies="[ typeof(BlazorSample.Client._Imports).Assembly ]">
    ...
</Router>

除了指定至 AppAssembly 的程序集外,还会扫描其他程序集。

注意

上述指南也适用于组件类库方案。

或者,可路由组件仅存在于应用了全局交互式 WebAssembly 或自动呈现的 .Client 项目中,Routes 组件则在 .Client 项目(而不是服务器项目)中进行定义。 在这种情况下,没有具有可路由组件的外部程序集,因此不需要为 AdditionalAssemblies 指定值。

本部分适用于 Blazor Server 应用。

使用 Router 组件的 AdditionalAssemblies 参数和终结点约定生成器 AddAdditionalAssemblies 来发现其他程序集中的可路由组件。

在以下示例中,Component1 是在名为ComponentLibrary中定义的可路由组件:

<Router
    AppAssembly="..."
    AdditionalAssemblies="new[] { typeof(ComponentLibrary.Component1).Assembly }">
    ...
</Router>

除了指定至 AppAssembly 的程序集外,还会扫描其他程序集。

路由参数

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

RouteParameter1.razor:

@page "/route-parameter-1/{text}"

<h1>Route Parameter Example 1</h1>

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

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

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

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

RouteParameter2.razor:

@page "/route-parameter-2/{text?}"

<h1>Route Parameter Example 2</h1>

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

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

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

使用 OnInitialized{Async} 生命周期方法而非 OnParametersSet{Async} 生命周期方法时,如果用户在同一组件内导航,则不会将 Text 属性默认分配给 fantastic。 例如,当用户从 /route-parameter-2/amazing 导航到 /route-parameter-2 时,就会出现这种情况。 随着组件实例持久保存并接受新参数,便不会再次调用 OnInitialized 方法。

注意

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

路由约束

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

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

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

User.razor:

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

<h1>User Example</h1>

<p>User Id: @Id</p>

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

注意

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

下表中显示的路由约束可用。 有关与不变文化匹配的路由约束,请参阅表下方的警告了解详细信息。

约束 示例 匹配项示例 固定条件
区域性
匹配
bool {active:bool} trueFALSE
datetime {dob:datetime} 2016-12-312016-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} 00001111-aaaa-2222-bbbb-3333cccc4444{00001111-aaaa-2222-bbbb-3333cccc4444}
int {id:int} 123456789-123456789
long {ticks:long} 123456789-123456789
nonfile {parameter:nonfile} 不是 BlazorSample.styles.css,不是 favicon.ico

警告

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

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

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; }
}

避免在路径参数中进行文件捕获

以下路由模板无意中捕获其可选路由参数 (Optional) 中的静态资产路径。 例如,将捕获应用的样式表 (.styles.css),这会中断应用的样式:

@page "/{optional?}"

...

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

若要将路由参数限制为捕获非文件路径,请使用路由模板中的 :nonfile 约束

@page "/{optional:nonfile?}"

使用包含点的 URL 进行路由

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

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

Example.razor:

@page "/example/{param?}"

<p>
    Param: @Param
</p>

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

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

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

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

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

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

若要允许托管的 ServerBlazor WebAssembly的 应用在 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 的末尾。

CatchAll.razor:

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

<h1>Catch All Parameters Example</h1>

<p>Add some URI segments to the route and request the page again.</p>

<p>
    PageRoute: @PageRoute
</p>

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

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

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

使用 OnNavigateAsync 处理异步导航事件

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

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

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        ...
    }
}
<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

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

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

在服务器上预呈现内容时,OnNavigateAsync 会执行两次

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

为了防止OnNavigateAsync 中的开发人员代码执行两次,Routes 组件可以存储 NavigationContext,以便在 OnAfterRender{Async} 生命周期方法中使用,其中可以检查 firstRender。 有关详细信息,请参阅使用 JavaScript 互操作预呈现

为了防止 OnNavigateAsync 中的开发人员代码执行两次,App 组件可以存储 NavigationContext,以供 OnAfterRender{Async} 使用,可以在其中检查 firstRender。 有关详细信息,请参阅使用 JavaScript 互操作预呈现

处理 OnNavigateAsync 中的取消

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

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

在以下示例中:

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

<Router AppAssembly="typeof(App).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 = new[] { 345, 789, 135, 689 };

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}
@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 = new[] { 345, 789, 135, 689 };

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

注意

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

用户与 <Navigating> 内容的交互

如果在导航期间出现明显延迟,例如在 Blazor WebAssembly 应用中延迟加载程序集时或在与 Blazor 服务器端应用进行缓慢的网络连接时出现明显延迟,Router 组件可以向用户指示正在发生页面转换。

在指定 Router 的组件顶部,为 @using 命名空间添加 Microsoft.AspNetCore.Components.Routing 指令:

@using Microsoft.AspNetCore.Components.Routing

Navigating 参数提供内容,以便在页面转换事件期间进行显示。

在路由器元素内容 (<Router>...</Router>) 中:

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

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