第九章-单页应用程序和路由
单页应用程序
SPA 是一种 Web 应用程序,它可以替换 UI 的某些部分,而无需重新加载整个页面。 SPA 使用 JavaScript 来实现对浏览器控制树的这种操作,也称为文档对象模型 (DOM),其中大多数由固定的 UI 和占位符元素组成,其中内容根据用户单击的位置被覆盖。 使用 SPA 的主要优点之一是您可以使 SPA 有状态。 这意味着您可以将应用程序加载的信息保存在内存中,就像构建桌面应用程序一样。 在本章中,您将看到一个使用 Blazor 构建的 SPA 示例。
布局组件#
让我们从 SPA 的固定部分开始。 每个 Web 应用程序都包含您可以在每个页面上找到的 UI 元素,例如页眉、页脚、版权、菜单等。将这些元素复制粘贴到每个页面将需要大量工作,并且需要更新每个页面,如果其中一个 这些要素需要改变。 开发人员不喜欢这样做,所以每个构建网站的框架都有一个解决方案。 例如,ASP.NET WebForms 使用母版页,而 ASP.NET MVC 具有布局页。 Blazor 还为此提供了一种称为布局组件的机制。
使用 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.razo
r 组件,如清单 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.razo
r(这也应该制作 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`` @inheri
t 并将其 @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
组件,如图所示。
Blazor 路由
单页应用程序使用路由来选择选择哪个组件来填充布局组件的 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
,则使用锚点(<a/>
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">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
如果您使用的是 Blazor Server,则可以在 _Host.cshtml 中找到基本标记。
当您在生产中部署时,您需要做的就是更新基本标记。例如,您可能将应用程序部署到 https://online.u2u.be/selfassessment。 在这种情况下,您需要将基本元素更新为 <base href="/selfassessment" />
。 那么为什么需要这样做呢? 如果您部署到 https://online.u2u.be/selfassement,则 Counter 组件的 URI 变为 https://online.u2u.be/selfassessment/counter。 路由将忽略基本 URI,因此它将按预期匹配计数器。 您只需要指定一次基本 URI,如清单 9-20 所示。
您还可以使用 NavigationManagerBaseUri
属性访问基本 URI(带有尾部斜杠)。 这对于将绝对 URI 传递给某些 JavaScript 库很有用。 我们将在下一章讨论 JavaScript 互操作性。
带有路由的延迟加载
Blazor 应用程序中的某些组件可能不经常使用。 但即便如此,Blazor 仍需要在运行应用程序之前将这些组件加载到浏览器中。 对于大型应用程序,这可能意味着您的应用程序将需要更长的时间来加载。 但是,使用 Blazor,我们可以在需要时加载组件。 这称为延迟加载。
延迟加载组件库#
延迟加载通过将不常用的组件移动到一个或多个组件库中,然后在您需要它们之前立即下载。 我们在第 3 章和第 4 章讨论了构建组件库。但是让我们从一个项目开始,将这些组件及其依赖项移动到库中,然后延迟加载它们。 在本书的下载中,您应该找到一种称为延迟加载的解决方案。 打开它。 这个项目应该看起来很熟悉。 您应该能够构建和运行此应用程序。 现在,为了示例,假设 Counter 和 FetchData 组件是我们想要延迟加载的组件。
让我们从 Counter 组件开始。 创建一个名为 LazyLoading.Library 的 Razor 类库项目。 将 Counter 组件移动到此库。 现在在客户端项目中添加对该库的项目引用,并将@using 指令添加到_Imports.razor(客户端项目中的那个)。
构建并运行您的解决方案。 单击计数器链接。 唔。 没有找到计数器。 为什么?
当 Router 组件被初始化时,它会从其 AppAssembly 参数中搜索程序集以查找具有 @page razor 指令的组件。 在我们将 Counter 组件移动到 razor 库之前,Counter 是该程序集的一部分。 但现在我们已将其移至剃刀库。 所以我们需要告诉 Router 组件在这个库中搜索可路由的组件。 我们可以通过设置路由器的 AdditionalAssemblies
参数轻松做到这一点。 打开 App.razor 并更新它,如清单 9-21 所示。 在这里,我们将 AdditionalAssemblies
参数设置为 List<Assembly>
,其中包含 Counter 组件的 Assembly。 现在应用程序应该显示 Counter 组件。
清单 9-21 使用附加程序集
@using System.Reflection
<Router AppAssembly="@typeof(Program).Assembly"
AdditionalAssemblies="@additionalAssemblies">
<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>
@code {
private List<Assembly> additionalAssemblies =
new List<Assembly>
{
typeof(Counter).Assembly
};
}
我们将 Counter
组件移到了 razor 库中,但是在加载应用程序时我们仍然会加载 Counter
组件。 是时候为 razor 库启用延迟加载了。
首先,我们会告诉运行时不要自动加载程序集,然后我们会在需要时加载它。
为延迟加载标记程序集#
使用编辑器打开客户端项目文件并添加 BlazorWebAssemblyLazyLoad
元素,如清单 9-22 所示。 这告诉运行时不要自动加载 LazyLoading.Library.dll
。
清单 9-22 开启延迟加载
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
...
</ItemGroup>
<ItemGroup>
<BlazorWebAssemblyLazyLoad
Include="LazyLoading.Library.dll" />
</ItemGroup>
</Project>
如果您尝试运行该应用程序,您将收到运行时错误:
Could not load file or assembly 'LazyLoading.Library, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null' or one of its dependencies.
动态加载程序集#
现在我们需要在需要时加载这个程序集。 我们什么时候加载这个程序集? 当我们导航到需要此程序集中的组件的组件时。 我们怎么知道我们正在导航? Router 组件对此有一个称为 OnNavigateAsync
的事件,我们将使用它来检测我们何时导航到使用延迟加载组件的组件。 然后我们将使用 LazyAssemblyLoader
下载程序集,以便它可以使用。
更新 App.razor
,如清单 9-23 所示。 首先,我们使用依赖注入获取 LazyAssemblyLoader
的实例。 然后我们使用 OnNavigate 方法实现 OnNavigateAsync
事件。 这个方法接收一个 NavigationContext
实例,如果我们导航到 Counter
组件,我们会检查 Path
。 如果是这样,我们为 Counter
组件 (LazyLoading.Library.dll) 加载程序集,并将其添加到 additionalAssemblies
集合中,以便路由器组件可以扫描它以查找路由模板。
清单 9-23 在需要时加载程序集
@using System.Reflection
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader assemblyLoader
<Router AppAssembly="@typeof(Program).Assembly"
AdditionalAssemblies="@additionalAssemblies"
OnNavigateAsync="OnNavigate">
<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>
@code {
private List<Assembly> additionalAssemblies =
new List<Assembly>
{
};
private async Task OnNavigate(NavigationContext context)
{
if( context.Path == "counter")
{
var assembliesToLoad = new List<string>
{
"LazyLoading.Library.dll"
};
var assemblies = await assemblyLoader.LoadAssembliesAsync
(assembliesToLoad);
additionalAssemblies.AddRange(assemblies);
}
}
}
在我们可以运行之前,我们需要配置依赖注入
builder.Services.AddScoped<LazyAssemblyLoader>();
构建并运行应用程序。 它应该会启动,当我们点击 Counter
时,浏览器会下载并渲染它。
如果我们的网络很慢怎么办? 也许我们想在程序集下载时显示一些加载 UI? 路由器有一个导航 RenderFragment
,它在加载时显示。 所以再次更新 App.razor
文件,如清单 9-24 所示,添加导航 UI。
清单 9-24 显示导航 UI
<Router AppAssembly="@typeof(Program).Assembly"
AdditionalAssemblies="@additionalAssemblies"
OnNavigateAsync="OnNavigate">
<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>
<Navigating>
Loading additional components...
</Navigating>
</Router>
延迟加载和依赖#
现在让我们尝试延迟加载 FetchData
组件。 此组件使用由 WeatherService
类(Blazor 项目中的那个)实现的 IWeatherService
实例。 我们将把两者都移到组件库中。
开始将 FetchData
组件和 WeatherService
类移动到组件库中。 由于 WeatherService
使用共享项目的 IWeatherService
,因此为共享项目添加对库项目的项目引用。
您的库项目现在应该可以编译了。
从 App.razor 更新 OnNavigate 方法以检查 FetchData URI,如清单 9-25 所示。
清单 9-25 FetchData 的 OnNavigate 方法
private async Task OnNavigate(NavigationContext context)
{
if (context.Path == "counter" || context.Path == "fetchdata")
{
var assembliesToLoad = new List<string>
{
"LazyLoading.Library.dll"
};
var assemblies = await assemblyLoader
.LoadAssembliesAsync(assembliesToLoad);
additionalAssemblies.AddRange(assemblies);
}
}
在 C# 中修复几个命名空间后,项目应该构建。 但是运行会失败。 为什么? 在 Program.cs 中,您正在从延迟加载的库中添加 WeatherService 类,但尚未加载(因为您告诉运行时不要加载它)。
也许我们可以推迟注册 WeatherService
? 抱歉,这行不通。 初始化后,依赖注入变得不可变,因此您以后无法添加依赖项。 当然,我们可以将 WeatherService
保留在 Blazor 客户端项目中,但让我们假设延迟加载它是值得的。 是时候介绍一点层了。 我们将使用工厂方法来创建依赖项,我们将使用依赖注入来注入工厂方法。 这将需要进行一些更改。
注意 工厂是一个类,它有一个方法可以创建某个类的实例,隐藏了创建过程。 例如,工厂可以创建一个实例,其中实例的类取决于某些业务规则。 当然,所有返回的实例都应该有一些共同的基类或接口。 实际上,依赖注入使用的IServiceProvider也是一个工厂,但是我们这里不能使用它,因为它不知道WeatherService的存在。 使用您最喜欢的搜索引擎并搜索“C# 中的工厂模式”以了解更多信息。
组件库和 Blazor 客户端应用程序都需要共享工厂接口,因此将 IWeatherServiceFactory
添加到 Shared 项目中,如清单 9-26 所示。
清单 9-26 IWeatherServiceFactory 接口
namespace LazyLoading.Shared
{
public interface IWeatherServiceFactory
{
IWeatherService Create();
}
}
更新 FetchData 组件以使用 IWeatherService 工厂创建 IWeatherService 实例,如清单 9-27 所示。
清单 9-27 更新 FetchData 组件
@page "/fetchdata"
@using LazyLoading.Shared
@inject IWeatherServiceFactory weatherServiceFactory
...
@code {
private IEnumerable<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
IWeatherService weatherService = weatherServiceFactory.Create();
forecasts = await weatherService.GetForecasts();
}
}
最后,我们将在客户端项目中实现 IWeatherServiceFactory 接口,如清单 9-28 所示,以创建实际的 WeatherService。 因为我们在使用工厂时只需要实现 WeatherService ,所以这将起作用,因为包含 WeatherService 的库将通过延迟加载来加载。 但是,WeatherService 有自己的依赖项,因此我们将在工厂中请求这些并将它们传递给实际的服务。 工厂是一个很小的类,当实际服务及其依赖项很大时,这种技术就变得很有趣。
清单 9-28 实现 IWeatherServiceFactory
using LazyLoading.Library.Services;
using LazyLoading.Shared;
using System.Net.Http;
namespace LazyLoading.Client
{
public class WeatherServiceFactory : IWeatherServiceFactory
{
private readonly HttpClient httpClient;
public WeatherServiceFactory(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public IWeatherService Create() => new WeatherService(httpClient);
}
}
将另一个页面添加到 PizzaPlace
让我们向 PizzaPlace 应用程序添加一个详细信息页面。 这将允许客户检查有关比萨饼的成分和营养信息。
当您使用路由在不同的 Blazor 组件之间导航时,您可能会遇到需要将信息从一个组件发送到另一个组件。实现此目的的一种方法是通过在 URI 中传递目标组件来设置一个参数。例如,您可以导航到 /pizzadetail/5 以告诉目标组件显示有关 id 为 5 的比萨饼的信息。然后目标组件可以使用服务加载有关比萨饼 #5 的信息,然后显示此信息。但在 Blazor 中,还有其他方法可以将信息从一个组件传递到另一个组件。如果两个组件共享一个共同的父组件,我们可以使用数据绑定。否则,我们可以使用一个 State 类(大多数开发人员称它为 State,但这只是一个约定,您可以随意调用它;State 很有意义),然后使用依赖注入为每个组件提供此类的相同实例.这个单一的 State 类包含组件需要的信息。我们之前在第 5 章中已经看到了这一点:这被称为单例模式。我们的 PizzaPlace 应用程序已经在使用 State 类,因此使用这种模式应该不会做太多工作。
首先打开 PizzaPlace 解决方案。 从 Pages 文件夹(在 PizzaPlace.Client 项目中)打开 Index 组件并查找私有 State 字段。 删除该字段(我已将其设为注释)并将其替换为 @inject 指令,如示例 9-29 所示。
清单 9-29 使用依赖注入获取状态单例实例
@page "/"
@inject IMenuService MenuService
@inject IOrderService orderService
@inject State State
...
@code {
// private State State { get; } = new State();
...
}
现在在 Program.cs 中配置依赖注入,将 State 实例作为单例注入,如清单 9-30 所示。
清单 9-30 为状态单例配置依赖注入
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Client.Services;
using PizzaPlace.Shared;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace PizzaPlace.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment
.BaseAddress)
});
builder.Services.AddTransient<IMenuService,
MenuService>();
builder.Services.AddTransient<IOrderService,
OrderService>();
builder.Services.AddSingleton<State>();
await builder.Build().RunAsync();
}
}
}
运行应用程序。 一切都应该仍然有效! 您所做的是使用单例模式将 State 单例注入到 Index 组件中。 让我们添加另一个使用相同 State 实例的组件。
您想使用新组件显示有关披萨的更多信息,但在此之前,您需要更新 State 类。 向 State 类添加一个名为 CurrentPizza 的新属性,如清单 9-31 所示。
清单 9-31 将 CurrentPizza 属性添加到状态类
using System.Linq;
namespace PizzaPlace.Shared
{
public class State
{
public Menu Menu { get; } = new Menu();
public ShoppingBasket Basket { get; } = new ShoppingBasket();
public UI UI { get; set; } = new UI();
public Pizza? CurrentPizza { get; set; }
public decimal TotalPrice
=> Basket.Orders.Sum(id => Menu.GetPizza(id)!.Price);
}
}
现在当有人点击菜单上的披萨时,它会显示披萨的信息。 通过将披萨名称包装在锚点中来更新 PizzaItem 组件,如示例 9-32 所示。 在示例 9-33 中的 PizzaItem 类中,我们添加了一个新的 ShowPizzaInformation 参数,如果它不为 null,我们将其包装在一个调用 ShowPizzaInformation 操作的锚点中。
清单 9-32 添加锚以显示披萨的信息
<div class="row">
<div class="col">
@if (ShowPizzaInformation is not null)
{
<a href=""
@onclick="@(() => ShowPizzaInformation?.Invoke(Pizza))">
@Pizza.Name
</a>
}
else
{
@Pizza.Name
}
</div>
<div class="col text-right">
@($"{Pizza.Price:0.00}")
</div>
<div class="col"></div>
<div class="col">
<img src="@SpicinessImage(Pizza.Spiciness)"
alt="@Pizza.Spiciness" />
</div>
<div class="col">
<button class="@ButtonClass"
@onclick="@(() => Selected.InvokeAsync(Pizza))">
@ButtonTitle
</button>
</div>
</div>
清单 9-33 添加 ShowPizzaInformation 参数
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
using System;
namespace PizzaPlace.Client.Pages
{
public partial class PizzaItem
{
[Parameter]
public Pizza Pizza { get; set; } = default!;
[Parameter]
public string ButtonTitle { get; set; } = default!;
[Parameter]
public string ButtonClass { get; set; } = default!;
[Parameter]
public EventCallback<Pizza> Selected { get; set; }
[Parameter]
public Action<Pizza>? ShowPizzaInformation { get; set; }
private string SpicinessImage(Spiciness spiciness)
=> $"images/{spiciness.ToString().ToLower()}.png";
}
}
更新 PizzaList 组件以设置 PizzaItem 组件的ShowPizzaInformation 参数如清单 9-34 和 9-35 所示。
当有人单击此链接时,它应该设置 State 实例的 CurrentPizza 属性。但是您无权访问 State 对象。 解决此问题的一种方法是在 PizzaItem 组件中注入 State 实例。 但是你不想让这个组件负担过重,所以你添加了一个 ShowPizzaInformation 回调委托来告诉包含 PizzaList 的组件你想显示更多关于披萨的信息。 单击比萨饼名称链接只会调用此回调,而不知道会发生什么。
您在此处应用了一种称为“哑元和智能组件(Dumb and Smart Components)”的模式。 哑组件是对应用程序的全局图一无所知的组件。 因为它对应用程序的其余部分一无所知,所以哑组件更容易重用。 智能组件知道应用程序的其他部分(例如使用哪个服务与数据库通信),并将使用哑组件来显示其信息。 在我们的示例中,PizzaList 和 PizzaItem 是哑组件,因为它们通过数据绑定接收所有数据,而 Index 组件是与服务对话的智能组件。
清单 9-34 向 PizzaList 组件添加 PizzaInformation 回调
<ItemList Items="@Items">
<Loading>
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</Loading>
<Header>
<h1>@Title</h1>
</Header>
<RowTemplate Context="pizza">
<PizzaItem Pizza="@pizza"
ButtonClass="@ButtonClass"
ButtonTitle="@ButtonTitle"
Selected="@Selected"
ShowPizzaInformation="@ShowPizzaInformation"/>
</RowTemplate>
</ItemList>
清单 9-35 添加 ShowPizzaInformation 回调参数
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
using System;
using System.Collections.Generic;
namespace PizzaPlace.Client.Pages
{
public partial class PizzaList
{
[Parameter]
public string Title { get; set; } = default!;
[Parameter]
public IEnumerable<Pizza> Items { get; set; } = default!;
[Parameter]
public string ButtonClass { get; set; } = default!;
[Parameter]
public string ButtonTitle { get; set; } = default!;
[Parameter]
public EventCallback<Pizza> Selected { get; set; }
[Parameter]
public Action<Pizza>? ShowPizzaInformation { get; set; }
}
}
您向 PizzaList 组件添加了一个 ShowPizzaInformation 回调,并且您只需将其传递给 PizzaItem 组件。 Index 组件将设置此回调,PizzaList 会将其传递给 PizzaItem 组件。
更新 Index 组件以设置 State 实例的 CurrentPizza 并导航到 PizzaInfo 组件,如清单 9-36 所示。 Index 组件告诉 PizzaList 组件在有人单击 PizzaItem 组件中的信息链接时调用 ShowPizzaInformation 方法。 ShowPizzaInformation 方法然后设置 State 的 CurrentPizza 属性(我们在 PizzaInfo 组件中需要它)并使用 NavigationManager 的 NavigateTo 方法导航到 /PizzaInfo 路由。
如果将 NavigateTo 作为回调的一部分调用,Blazor 将返回到原始路由。 这就是我使用后台任务的原因,因此 Blazor 将在回调之后导航。
清单 9-36 Index 组件导航到 PizzaInfo 组件
@page "/"
@inject IMenuService MenuService
@inject IOrderService orderService
@inject State State
@inject NavigationManager NavigationManager
@if (State.Menu.Pizzas.Any())
{
<!-- Menu -->
<PizzaList Title="Our Selection of Pizzas"
Items="@State.Menu.Pizzas"
ButtonTitle="Order"
ButtonClass="btn btn-success pl-4 pr-4"
Selected="@AddToBasket"
ShowPizzaInformation="@ShowPizzaInformation"/>
<!-- End menu -->
<!-- Shopping Basket -->
...
@code {
...
private void ShowPizzaInformation(Pizza selected)
{
this.State.CurrentPizza = selected;
Task.Run(() => this.NavigationManager.NavigateTo("/pizzainfo"));
}
}
右键单击 Pages 文件夹并添加一个名为 PizzaInfo 的新 razor 组件,如清单 9-37 和 9-38 所示(为了节省一些时间并保持简单,您可以复制大部分 PizzaItem 组件)。 PizzaInfo 组件显示有关州的 CurrentPizza 的信息。 这是有效的,因为您在这些组件之间共享相同的 State 实例。 Index 组件将在 State 中设置 CurrentPizza 属性,然后由 PizzaInfo 组件显示。 因为 State 的 CurrentPizza 属性可以为 null,所以我还在 PizzaInfo 组件中添加了一个辅助属性,该属性始终返回一个不可为 null 的 CurrentPizza(使用 null-forgiving 运算符)以避免编译器警告。
清单 9-37 添加 PizzaInfo 组件
@page "/PizzaInfo"
<h2>Pizza @CurrentPizza.Name Details</h2>
<div class="row">
<div class="col">
@CurrentPizza.Name
</div>
<div class="col">
@CurrentPizza.Price
</div>
<div class="col">
<img src="@SpicinessImage(CurrentPizza.Spiciness)"
alt="@CurrentPizza.Spiciness" />
</div>
</div>
<div class="row">
<div class="col">
<a class="btn btn-primary" href="/">Back to Menu</a>
</div>
</div>
清单 9-38 PizzaInfo 类
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
namespace PizzaPlace.Client.Pages
{
public partial class PizzaInfo
{
[Inject]
public State State { get; set; } = default!;
public Pizza CurrentPizza
=> State.CurrentPizza!;
private string SpicinessImage(Spiciness spiciness)
=> $"images/{spiciness.ToString().ToLower()}.png";
}
}
在标记的底部,您添加一个锚点(并使用引导样式使其看起来像一个按钮)以返回菜单。 这是一个用锚点改变路线的例子。 当然,在现实生活中的应用程序中,您会展示比萨的成分、精美的图片和营养信息。 我把这个作为练习留给你.
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· spring官宣接入deepseek,真的太香了~