SPA 是一种 Web 应用程序,它可以替换 UI 的某些部分,而无需重新加载整个页面。 SPA 使用 JavaScript 来实现对浏览器控制树的这种操作,也称为文档对象模型 (DOM),其中大多数由固定的 UI 和占位符元素组成,其中内容根据用户单击的位置被覆盖。 使用 SPA 的主要优点之一是您可以使 SPA 有状态。 这意味着您可以将应用程序加载的信息保存在内存中,就像构建桌面应用程序一样。 在本章中,您将看到一个使用 Blazor 构建的 SPA 示例。
让我们从 SPA 的固定部分开始。 每个 Web 应用程序都包含您可以在每个页面上找到的 UI 元素,例如页眉、页脚、版权、菜单等。将这些元素复制粘贴到每个页面将需要大量工作,并且需要更新每个页面,如果其中一个 这些要素需要改变。 开发人员不喜欢这样做,所以每个构建网站的框架都有一个解决方案。 例如,ASP.NET WebForms 使用母版页,而 ASP.NET MVC 具有布局页。 Blazor 还为此提供了一种称为布局组件的机制。
布局组件是 Blazor 组件。 任何你可以用常规组件做的事情,你都可以用布局组件做,比如依赖注入、数据绑定和嵌套其他组件。 唯一的区别是它们必须继承自 LayoutComponentBase 类。
LayoutComponentBase 类向 ComponentBase 添加了一个 Body 属性,如清单 9-1 所示。
清单 9-1 LayoutComponentBase 类(简体)
namespace Microsoft.AspNetCore.Components { public abstract class LayoutComponentBase : ComponentBase { [Parameter] public RenderFragment? Body { get; set; } } }
从清单 9-1 可以看出,LayoutComponentBase 类继承自 ComponentBase 类。 这就是为什么您可以做与普通组件相同的事情的原因。
让我们看一个布局组件的示例。 从本章提供的代码中打开 SinglePageApplications 解决方案。 现在,查看 SPA.Client 的 Shared 文件夹中的 MainLayout.razor 组件,如清单 9-2 所示。 由于布局组件由多个组件使用,因此将布局组件放在共享文件夹中是有意义的,尽管这样做没有技术要求。
清单 9-2 模板中的 MainLayout.razor
@inherits LayoutComponentBase <div class="page"> <div class="sidebar"> <NavMenu /> </div> <div class="main"> <div class="top-row px-4"> <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a> </div> <div class="content px-4"> @Body </div> </div> </div>
在第一行,MainLayout 组件声明它继承自 LayoutComponentBase。 然后你会看到一个侧边栏和主 <div>
元素,主元素数据绑定到继承的 Body 属性。 任何使用此布局组件的组件都将在 @Body 属性所在的位置结束,因此在 <div class="content px-4">
内部。
在下图中,您可以看到左侧的侧边栏(包含指向此单页应用程序不同组件的链接)和右侧的主要区域,其中 @Body 用黑色矩形突出显示(我添加了 如图)。 单击侧边栏中的 Home、Counter 或 Fetch Data 链接将用所选组件替换 Body 属性,更新 UI 而无需重新加载整个页面。
您可以在 MainLayout.razor.css 文件中找到此布局组件使用的 CSS 样式。
那么组件如何知道使用哪个布局组件呢? 组件可以为自己更改布局组件,应用程序可以设置默认布局组件,该组件将用于所有未明确设置其布局的组件。 让我们从应用程序开始。 打开 App.razor 文件,如清单 9-3 所示。 这里首先要注意的是 RouteView 组件,它有一个 Type 类型的 DefaultLayout 属性。 这是设置此应用程序的默认布局的位置。 默认情况下,此 RouteView 组件选择的任何组件都将使用 MainLayout。 如果找不到合适的组件来显示,App 组件使用 LayoutView 来显示错误消息。 同样,此 LayoutView 使用 MainLayout,但当然您可以将其更改为您喜欢的任何布局。
清单 9-3 App.razor 组件
<Router AppAssembly="@typeof(Program).Assembly"> <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>
在内部,RouteView 组件使用 LayoutView 组件来选择合适的布局组件。 LayoutView 允许您更改组件的任何部分的布局组件。
让我们创建一个简单的错误布局组件,它将水平居中显示错误。 首先将带有清单 9-4 中标记的名为 ErrorLayout 的新剃须刀组件添加到 Shared 文件夹。
清单 9-4 ErrorLayout组件
@inherits LayoutComponentBase <div class="error"> @Body </div>
现在将清单 9-5 中名为 ErrorLayout.razor.css 的 CSS 文件添加到 Shared 文件夹中。这告诉错误布局将正文放置在浏览器窗口的中心。
清单 9-5 ErrorLayout样式
.error { position: relative; display: flex; justify-content: center; align-items: center; height: 100vh; }
现在将 App.razor 中的 LayoutView 的 Layout 属性替换为清单 9-6。
清单 9-6 更新的 App.razor 文件
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(ErrorLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
运行 Blazor 应用程序并通过附加 /x 手动更改浏览器的 URL。由于此 URL 没有任何关联,因此将使用错误布局来显示错误,如图 9-2 所示。
每个组件都可以通过使用 @layout razor 指令声明布局组件的名称来选择要使用的布局。 例如,首先将 MainLayout.razor 文件复制到 MainLayoutRight.razor(这也应该制作 CSS 文件的副本)。 这将生成一个名为 MainLayoutRight 的新布局组件,从文件名推断(您可能需要重建项目以强制执行此操作)。 在该组件的 CSS 文件中,将两个 flex-direction 属性更改为它们的反向对应物,如清单所示。
清单 9-7 第二个布局组件
.page { position: relative; display: flex; flex-direction: column-reverse; } ... @media (min-width: 641px) { .page { flex-direction: row-reverse; } ... }
现在打开 Counter 组件并添加一个 @layout razor 指令,如清单 9-8 所示。
清单 9-8 使用 @layout 选择不同的布局
@page "/counter" @layout MainLayoutRight <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++; } }
运行应用程序并在 Home 和 Counter 之间交替时观察布局变化。
大多数组件将使用相同的布局。 除了将相同的 @layout razor 指令复制到每个页面之外,您还可以将 _Imports.razor 文件添加到与组件相同的文件夹中。 从 SPA.Client 项目中打开 Pages 文件夹并添加一个新的 _Imports.razor 文件。 将其内容替换为示例 9-9。
清单 9-9 _Imports.razor
@layout MainLayoutRight
此文件夹(或子文件夹)中未显式声明 @layout 组件的任何组件都将使用 MainLayoutRight 布局组件。
布局组件也可以嵌套。 您可以定义 MainLayout 以包含所有组件之间共享的所有 UI,然后定义嵌套布局以供这些组件的子集使用。 例如,将一个名为 NestedLayout.razor 的新 razor 视图添加到 Shared 文件夹,并将其内容替换为清单 9-10。
清单 9-10 一个简单的嵌套布局
@inherits LayoutComponentBase @layout MainLayout <div class="paper"> @Body </div>
要构建嵌套布局,请从 LayoutComponentBase @inherit 并将其 @layout 设置为另一个布局,例如 MainLayout。 我们的嵌套布局使用了一个纸类,所以在组件旁边添加一个 NestedLayout.razor.css 文件并添加清单 9-11。
清单 9-11 NestedLayout 组件的样式
.paper { background-image: url("images/paper.jpg"); padding: 1em; }
此样式使用 images 文件夹中的 paper.jpg 背景。
现在向 Pages 文件夹中的 _Imports.razer 文件添加一个布局指令,如清单 9-12 所示。
清单 9-12 嵌套布局
@layout NestedLayout
运行您的应用程序; 现在您在主布局内的嵌套布局内有了 Index 组件,如图所示。
单页应用程序使用路由来选择选择哪个组件来填充布局组件的 Body 属性。 路由是将浏览器的 URI 与路由模板集合匹配的过程,用于选择要在屏幕上显示的组件。 这就是为什么 Blazor SPA 中的每个组件都使用 @page 指令来定义路由模板以告诉路由器选择哪个组件。
从头开始创建 Blazor 解决方案时,路由器已安装,但让我们看看这是如何完成的。 打开 App.razor。 这个 App 组件只有一个组件,Router 组件,如清单 9-13 所示。
清单 9-13 包含路由器的 App 组件
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(ErrorLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
Router 组件是具有两个模板的模板化组件。 Found 模板用于已知路由,当 URI 不匹配任何已知路由时显示 NotFound。 您可以替换最后的内容以向用户显示一个漂亮的错误页面。
Found 模板使用 RouteView 组件,该组件将使用其布局(或默认布局)呈现所选组件。 当 Router 组件被实例化时,它将在其 AppAssembly 属性中搜索所有具有 RouteAttribute 的组件(@page razor 指令被编译为RouteAttribute)并选择与当前浏览器的 URI 匹配的组件。 例如 Counter 组件有 @page "/counter" razor 指令,当浏览器中的 URL 匹配 /counter 时,会在 MainLayout 组件中显示 Counter 组件。
查看清单 9-2 中的 MainLayout 组件。 在第四行,您将看到 NavMenu 组件。 该组件包含在组件之间导航的链接。 该组件自带模板; 随意使用另一个组件进行导航。 我们将在这里使用这个组件来探索一些概念。 打开 SPA.Client 项目并在 Shared 文件夹中查找 NavMenu 组件,如清单 9-14 所示。
清单 9-14 导航菜单组件
<div class="top-row pl-4 navbar navbar-dark"> <a class="navbar-brand" href="">SPA</a> <button class="navbar-toggler" @onclick="ToggleNavMenu"> <span class="navbar-toggler-icon"></span> </button> </div> <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> @code { private bool collapseNavMenu = true; private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } }
清单 9-14 的第一部分包含 Toggle 按钮,它允许您隐藏和显示导航菜单。 此按钮仅在宽度较窄的显示器(例如移动显示器)上可见。 如果您想查看它,运行您的应用程序并缩小浏览器宽度,直到在右上角看到汉堡包按钮,如图 9-4 所示。 单击按钮可显示导航菜单,再次单击可再次隐藏菜单。
剩下的标记包含导航菜单,它由 NavLink 组件组成。 让我们看看 NavLink 组件。
NavLink 组件是锚元素 <a/>
的特殊版本,用于创建导航链接,也称为超链接。 当浏览器的 URI 与 NavLink 的 href 属性匹配时,它会将 CSS 样式(如果您想自定义它,则为活动的 CSS 类)应用到自身,让您知道它是当前路由。 例如,查看示例 9-15。
清单 9-15 Counter Route 的 NavLink
<NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter </NavLink>
当浏览器的 URI 以 /counter 结尾(忽略查询字符串之类的内容)时,此 NavLink 将应用活动样式。 让我们看一下示例 9-16 中的另一个。
清单 9-16 默认路由的 NavLink
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink>
当浏览器的 URI 为空(站点的 URL 除外)时,示例 9-16 中的 NavLink 将处于活动状态。 但是这里有一个特殊情况。 通常,NavLink 组件只匹配 URI 的结尾。 例如,/counter 匹配示例 9-15 中的 NavLink。 但是对于空 URI,这将匹配所有内容! 这就是为什么在空 URI 的特殊情况下,您需要告诉 NavLink 匹配整个 URI。 您可以使用 Match 属性执行此操作,默认情况下该属性设置为 NavLinkMatch.Prefix。 如果要匹配整个 URI,请使用 NavLinkMatch.All,如清单 9-16 所示。
Blazor 的 Routing 组件检查浏览器的 URI 并搜索要匹配的组件的路由模板。 但是如何设置组件的路由模板呢? 打开清单 9-8 所示的 Counter 组件。 这个文件的顶部是 @page "/counter" razor 指令。 它定义了路由模板。 路由模板是与 URI 匹配的字符串,可以包含参数,然后您可以在组件中使用这些参数。
您可以通过在路由中传递参数来更改组件中显示的内容。 您可以传递产品的 id,使用 id 查找产品的详细信息,并使用它来显示产品的详细信息。 让我们看一个例子。 通过添加另一个将设置 CurrentCount 参数的路由模板,将 Counter 组件更改为如清单 9-17 所示。 这个清单说明了几件事。 首先,您可以有多个 @page razor 指令,因此 /counter 和 /counter/55 都将路由到 Counter 组件。 第二个@page 指令将从路由中设置CurrentCount 参数属性,并且参数名称在@page 指令中不区分大小写。 当然,参数需要用大括号括起来,这样路由器才能识别它。
清单 9-17 使用参数定义路由模板
@page "/counter" @page "/counter/{currentCount:int?}" @layout MainLayoutRight <h1>Counter</h1> <p>Current count: @CurrentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { [Parameter] public int CurrentCount { get; set; } private void IncrementCount() { CurrentCount++; } }
就像 ASP.NET MVC Core 中的路由一样,您可以使用路由约束来限制要匹配的参数类型。 例如,如果您要使用 /counter/Blazor URI,则路由模板将不匹配,因为参数不包含整数值,并且路由器不会找到任何要匹配的组件。
如果您不使用字符串类型的参数,约束甚至是强制性的;否则,路由器不会将参数转换为正确的类型。 您可以通过使用冒号附加约束来指定约束,例如,@page "/counter/{currentCount:int}"。 您还可以通过在约束后附加一个问号来使参数可选,如清单 9-17 所示。
表 9-1 中列出了其他路由约束。 这些中的每一个都映射到相应的 .NET 类型。
路由约束
bool datetime decimal double float guid int long
如果您将组件构建为纯 C# 组件,请将 RouteAttribute 应用到您的类,并将路由模板作为参数。 这就是 @page 指令被编译成的内容。
如何使用路由导航到另一个组件? 您有三个选择:使用标准锚元素、使用 NavLink 组件和使用代码。 让我们从普通的锚标签开始。
如果使用相对 href,则使用锚点( HTML 元素)很容易。 例如,在示例 9-17 的按钮下方添加示例 9-18。
清单 9-18 使用锚标记导航
<a class="btn btn-primary" href="/">Home</a>
此链接已使用 Bootstrap 4 设置为按钮。运行您的应用程序并导航到 Counter 组件。 点击 Home 按钮导航到路由模板匹配“/”的 Index 组件
NavLink 组件使用底层锚,因此其用法类似。 唯一的区别是 NavLink 组件在匹配路由时应用活动类。 通常,您只在 NavMenu 组件中使用 NavLink,但您可以随意使用它来代替锚点。
也可以在代码中导航,但您需要通过依赖注入创建 NavigationManager 类的实例。 此实例允许您检查页面的 URI,并具有有用的 NavigateTo 方法。 此方法接受一个字符串,该字符串将成为浏览器的新 URI。
让我们尝试一个例子。 修改 Counter 组件,使其类似于清单 9-19。
清单 9-19 使用导航管理器
@page "/counter" @page "/counter/{currentCount:int?}" @layout MainLayoutRight @inject NavigationManager navigationManager <h1>Counter</h1> <p>Current count: @CurrentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <a class="btn btn-primary" href="/">Home</a> <button class="btn btn-primary" @onclick="StartFrom50">Start from 50</button> @code { [Parameter] public int CurrentCount { get; set; } private void IncrementCount() { CurrentCount++; } private void StartFrom50() { navigationManager.NavigateTo("/counter/50"); } }
你用 @inject razor 指令告诉依赖注入给你一个 NavigationManager 的实例并将它放在 navigationManager 字段中。 NavigationManager 是 Blazor 通过依赖注入提供的现成类型之一。 然后添加一个在单击时调用 StartFrom50 方法的按钮。 此方法使用 NavigationManager 通过调用 NavigateTo 方法导航到另一个 URI。 运行您的应用程序并单击“从 50 开始”按钮。 您应该导航到 /counter/50。
导航时请不要使用绝对 URI。 为什么? 因为当您在 Internet 上部署应用程序时,基本 URI 会发生变化。 相反,Blazor 使用 <base/>
HTML 元素,所有相关 URI 都将与此 <base/>
标记组合。 <base/>
标签在哪里? 使用 Blazor WebAssembly,打开 Blazor 项目的 wwwroot 文件夹并打开 index.html,如清单 9-20 所示。
清单 9-20 index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>SPA</title> <base href="/" /> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> <link href="SPA.Client.styles.css" rel="stylesheet" /> </head> <body> <div id="app">Loading...</div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">