第九章-单页应用程序和路由

单页应用程序

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.ClientShared 文件夹中的 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 而无需重新加载整个页面。

image

您可以在 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.cssCSS 文件添加到 Shared 文件夹中。这告诉错误布局将正文放置在浏览器窗口的中心。

清单 9-5 ErrorLayout样式

.error {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

现在将 App.razor 中的 LayoutViewLayout 属性替换为清单 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 属性更改为它们的反向对应物,如清单所示。

image

清单 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++;
    }
}

运行应用程序并在 HomeCounter 之间交替时观察布局变化。

大多数组件将使用相同的布局。 除了将相同的 @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 组件,如图所示。

image

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 所示。 单击按钮可显示导航菜单,再次单击可再次隐藏菜单。

image

剩下的标记包含导航菜单,它由 NavLink 组件组成。 让我们看看 NavLink 组件。
NavLink 组件是锚元素 <a/> 的特殊版本,用于创建导航链接,也称为超链接。 当浏览器的 URI 与 NavLinkhref 属性匹配时,它会将 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";
    }
}

在标记的底部,您添加一个锚点(并使用引导样式使其看起来像一个按钮)以返回菜单。 这是一个用锚点改变路线的例子。 当然,在现实生活中的应用程序中,您会展示比萨的成分、精美的图片和营养信息。 我把这个作为练习留给你.

posted @   F(x)_King  阅读(135)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示
主题色彩