Blazor-Web-开发第三版-全-

Blazor Web 开发第三版(全)

原文:zh.annas-archive.org/md5/ce3dcde358e5ad6aa0a4c17a1e10f0f7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

到目前为止,创建交互式网页意味着使用 JavaScript。但有了用于创建 .NET 网络应用的 Blazor 框架,开发者可以轻松地使用 C# 构建交互式和丰富的网络应用。本书将引导你通过开始 Blazor 之旅时最常遇到的场景。

首先,你将发现如何利用 Blazor 的力量,并了解你可以使用服务器端渲染SSR)、Blazor 服务器、Blazor WebAssembly 和 Blazor 混合应用做什么。本书将通过展示所有元素如何实际协同工作,帮助你克服开发者面临的一些常见障碍。随着你的进步,你将学习如何创建 Blazor 项目,了解 Razor 语法的工作原理,如何验证表单,以及如何创建自己的组件。然后,本书将向你介绍 Blazor 网络开发中的关键概念,你可以立即将其付诸实践。

在阅读完这本 Blazor 书籍之后,你将获得创建和部署生产就绪的 Blazor 应用程序的信心。

本书面向对象

本书面向希望探索 Blazor 以学习如何构建动态网络用户界面的网络开发人员和软件开发人员。本书假设读者熟悉 C# 编程和网络开发概念。

本书涵盖内容

第一章你好,Blazor,将教你 Blazor 服务器和 Blazor WebAssembly 之间的区别,以及新的静态和流式 SSR。你将了解这项技术的工作原理以及 Blazor 的简要历史。了解托管模型的结构和区别对于理解这项技术至关重要。

第二章创建你的第一个 Blazor 应用,帮助你了解如何安装和设置你的开发环境。你将创建你的第一个 Blazor 应用,并了解项目模板的结构。

第三章管理状态 - 第一部分,教你如何创建一个存储你的数据(博客文章、分类、标签和评论)的仓库。

第四章理解基本 Blazor 组件,更深入地探讨了组件、生命周期事件、添加参数以及组件间参数共享。你还将在本章中创建可重用组件。

第五章创建高级 Blazor 组件,更深入地探讨了组件,增加了子组件、级联参数和值等功能,并介绍了如何使用操作和回调。

第六章使用验证构建表单,探讨了表单、如何验证表单以及如何构建自己的验证机制。本章将涵盖处理表单时最常见的使用场景,例如文件上传、文本、数字,以及在键盘上键入时触发代码。

第七章创建 API,探讨了使用 Minimal API 创建 API。当使用 Blazor WebAssembly 时,我们需要一个 API 来获取数据。

第八章认证和授权,探讨了如何将认证和授权添加到 Blazor 中,并确保导航,如重定向到登录页面,能按预期工作。

第九章共享代码和资源,教你如何在项目之间共享代码。在本章中,我们继续构建一个可以打包为 NuGet 包并与他人共享的共享库。

第十章JavaScript 互操作,探讨了在使用 Blazor 时如何利用 JavaScript 库,并从 C#调用 JavaScript。你还将检查 JavaScript 如何在我们的 Blazor 应用程序中调用 C#函数。

第十一章管理状态 – 第二部分,探讨了管理状态(持久化数据)的不同方式,例如使用 LocalStorage 或仅通过依赖注入在内存中保留数据。你还将使用 SignalR 实现博客文章的实时更新。

第十二章调试代码,教你如何调试应用程序并添加扩展日志来找出应用程序的问题。你不仅会查看传统的调试,还会直接在网页浏览器中调试 C#代码。

第十三章测试,探讨了自动化测试,以确保你的组件按预期工作(并且继续这样做)。没有内置的测试 Blazor 应用程序的方法,但有一个名为 bUnit 的优秀社区项目。

第十四章部署到生产环境,将带你了解在运行 Blazor 时需要考虑的不同事项。

第十五章从现有网站迁移或与之结合,将展示如何将 Blazor 集成到现有网站中,并将 JavaScript 框架如 Angular 或 React 与 Blazor 结合使用。

第十六章深入 WebAssembly,涵盖了 Blazor WebAssembly 的特定内容。

第十七章检查源生成器,涵盖了 Blazor 如何大量依赖源生成器。在本章中,你将了解它们的工作原理以及与 Blazor 的关系。

第十八章访问.NET MAUI,探讨了第三种托管模型,Blazor 混合。使用.NET MAUI,你可以利用本书中学到的知识构建 iOS、Android、macOS、Tizen 和 Windows 应用程序。

第十九章下一步该做什么,是一个简短的章节,包含行动号召、一些你可以使用的资源以及一个结尾。

为了充分利用本书

我建议阅读前几章,以确保你对 Blazor 的基本概念有足够的了解。我们创建的项目适用于实际应用,但省略了一些部分,例如适当的错误处理。然而,你应该能够很好地掌握 Blazor 的构建块。

本书侧重于使用 Visual Studio 2022;尽管如此,你可以自由地使用任何你熟悉且支持 Blazor 的版本。

本书涵盖的软件 操作系统要求
Visual Studio 2022, .NET8 Windows 10 或更高版本,macOS,Linux

如果你使用的是本书的数字版,我们建议你亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助你避免与代码复制和粘贴相关的任何潜在错误。

我非常希望你在阅读这本书或一般 Blazor 开发过程中分享你的进度。在@EngstromJimmy上向我发推文。

我希望你在阅读这本书的过程中能和我写作时一样享受乐趣。

下载示例代码文件

书籍的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781835465912

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“EditForm将创建一个EditContext实例作为级联值,以便所有放入EditForm中的组件都能访问相同的EditContext。”

代码块设置如下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
} 

任何命令行输入或输出都如下所示:

dotnet new blazorserver -o BlazorServerSideApp
cd Data 

粗体:表示新术语、重要单词或你在屏幕上看到的单词,例如在菜单或对话框中,这些单词在文本中也以这种方式出现。例如:“从搜索结果中选择Blazor Server App并按下一步。”

警告或重要注意事项看起来像这样。

技巧和窍门看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈:请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果你对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。

勘误:尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这一点。请访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果你在互联网上以任何形式遇到我们作品的非法副本,我们将不胜感激,如果你能向我们提供位置地址或网站名称。请通过copyright@packtpub.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《使用 Blazor 进行 Web 开发》,我们非常乐意听到您的想法!请点击此处直接转到此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在每购买一本 Packt 图书,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何地点、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此结束,您还可以获得独家折扣、时事通讯和每日邮箱中的优质免费内容。

按照以下简单步骤获取好处:

  1. 扫描下面的二维码或访问以下链接:

packt.link/free-ebook/9781835465912

  1. 提交您的购买证明。

  2. 就这些!我们将直接将您的免费 PDF 和其他好处发送到您的电子邮件。

第一章:你好,Blazor

感谢您选择阅读《使用 Blazor 进行 Web 开发》。本书旨在让您尽可能快速、顺畅地开始,逐章进行,无需在开始使用 Blazor 之前通读全书。

本书将从引导你了解你在开始 Blazor 之旅时最常遇到的常见场景开始,稍后还会深入探讨一些更高级的场景。本书旨在向你展示 Blazor 是什么——Blazor Server、Blazor WebAssembly、Blazor Hybrid,以及在此基础上,新的服务器端渲染SSR)——以及它如何在实际中工作,帮助你避免陷阱。

这是本书的第三版;自第一版以来发生了许多变化。.NET 6 和.NET 7 已经发布,对于第二版,我更新了内容以反映这些变化和我们所获得的新功能。

本版已更新,包括.NET 8 的所有新内容,让我告诉你,那可是一大堆。

我在世界各地做 Blazor 演示,经常有人问我一些常见的问题。不过,我不会过多地深入细节,它们通常与 Blazor WebAssembly 的下载大小或与 Blazor Server 的持续连接有关。在.NET 8 中,我们可以利用一种新模式,SSR,它可以一举解决所有这些问题。好吧,也许不是所有问题,但我们正在朝着解决这些问题的方向前进。一个普遍的看法是 Blazor 就是 WebAssembly,但 WebAssembly 只是运行 Blazor 的一种方式。许多关于 Blazor 的书籍、研讨会和博客文章都高度关注 WebAssembly。

本书将涵盖 Blazor WebAssembly、Blazor Server、Blazor Hybrid 和 SSR。运行 Blazor 的不同方式之间有一些差异;我们将随着内容的展开指出这些差异。

本章将探讨 Blazor 的起源,是什么技术使得 Blazor 成为可能,以及运行 Blazor 的不同方式。我们还将讨论哪种类型(Blazor WebAssembly、Blazor Server 或 Blazor Hybrid)最适合您。

在本章中,我们将涵盖以下主题:

  • 为什么选择 Blazor?

  • Blazor 之前

  • 介绍 WebAssembly

  • 介绍.NET 8

  • 介绍 Blazor

技术要求

建议你在开始之前对.NET 有一些了解,因为本书的目标是针对希望利用其技能制作交互式 Web 应用的.NET 开发者。然而,如果你是.NET 世界的初学者,你完全有可能学到一些.NET 技巧。

为什么选择 Blazor?

不久前,有人在 Facebook 上随机问我是否使用 Blazor。

我说:“是的,是的,我在做”。

他接着发表了一长串评论,告诉我 Blazor 永远不会打败 Angular、React 或 Vue。

我经常看到这样的评论,重要的是要理解,打败其他单页应用程序SPA)框架从未是我们的目标。这并不是《最后的武士》,可以有多个。

学习网页开发以前相当困难。我们不仅需要了解 ASP.NET 用于服务器,还需要学习像 React、Angular 或 Vue 这样的 SPA 框架。

但这还没有结束。我们还需要学习 npm、Bower 和 Parcel,以及 JavaScript 或 TypeScript。

我们需要理解转译并将其纳入我们的开发流程中。这当然只是冰山一角;根据技术不同,我们需要探索其他兔子洞。

Blazor 是.NET 开发者编写交互式网页应用程序的一个优秀选择,无需学习(或跟上)我们刚才提到的所有内容。我们可以利用我们现有的 C#知识和我们使用的包,并在服务器和客户端之间共享代码。

我通常说,“Blazor 消除了我对网页开发的所有厌恶。”我想这句话应该是,“Blazor可以消除我对网页开发的所有厌恶。”使用 Blazor,仍然可以进行 JavaScript 互操作,并在 Blazor 中使用 JavaScript 框架或其他 SPA 框架,但我们不必这样做。

Blazor 为我打开了一扇门,让我在拥有现有 C#知识的基础上,能够感受到高效和自信,为我的用户创造出色的用户体验。

在 Blazor 之前

你可能不会为了阅读关于JavaScript的书而选择这本书,但记住我们来自 Blazor 之前的时代是有帮助的。我记得那个时代——黑暗的时代。Blazor 中使用的许多概念与许多 JavaScript 框架中使用的概念并不遥远,所以我会从简要概述我们从哪里来开始。

作为开发者,我们可以为许多不同的平台进行开发,包括桌面、移动、游戏、云(或服务器端)、人工智能,甚至物联网。所有这些平台都有很多不同的语言可供选择,但当然还有一个额外的平台:运行在浏览器内的应用程序。

我已经是一名网页开发者很长时间了,我见证了代码从服务器迁移到在浏览器中运行。这改变了我们开发应用程序的方式。例如 Angular、React、Aurelia 和 Vue 等框架已经将网页从重新加载整个页面转变为动态更新小部分内容。这种的动态更新方法使得页面加载更快,因为感知的加载时间已经降低(不一定是整个页面的加载)。

但对于许多开发者来说,这是一套全新的技能集——也就是说,从服务器(如果你在读这本书,很可能是 C#)切换到用 JavaScript 开发的客户端。数据对象在服务器端用 C#编写,然后序列化为 JSON,通过 API 发送,然后在客户端用 JavaScript 反序列化为另一个对象。

JavaScript 在不同浏览器中的工作方式曾经不同,jQuery 通过提供一个通用的 API 来解决这个问题,该 API 被转换成浏览器可以理解的东西。现在,不同浏览器之间的差异要小得多,这在许多情况下已经使 jQuery 变得过时。

JavaScript 与其他语言略有不同,因为它不是面向对象或强类型的,例如。2010 年,Anders Hejlsberg(以其作为 C#、Delphi 和 Turbo Pascal 的原始语言设计者而闻名)开始着手开发 TypeScript。这种面向对象的语言可以编译/转译成 JavaScript。

您可以使用 TypeScript 与 Angular、React、Aurelia 和 Vue 一起使用,但最终,运行实际代码的是 JavaScript。简单来说,要使用 JavaScript/TypeScript 创建交互式 Web 应用程序,您需要在不同语言之间切换,并选择并跟上不同的框架。

在这本书中,我们将以另一种方式来看待这个问题。尽管我们将讨论 JavaScript,但我们的主要关注点将是使用 C# 开发交互式 Web 应用程序。

现在,我们了解了一些关于 JavaScript 的历史。由于 WebAssembly 的出现,JavaScript 已不再是唯一可以在浏览器中运行的编程语言,我们将在下一节中介绍 WebAssembly。

介绍 WebAssembly

在本节中,我们将探讨 WebAssembly 的工作原理。运行 Blazor 的一种方式是通过使用 WebAssembly,但到目前为止,让我们先关注 WebAssembly 是什么。

WebAssembly 是一种二进制指令格式,它是编译的,因此更小。它设计用于原生速度,这意味着在速度方面,它比 JavaScript 更接近 C++。当加载 JavaScript 时,JavaScript 文件(或内联 JavaScript)会被下载、解析、优化和 JIT 编译;对于 WebAssembly,这些步骤中的大多数都不需要。

WebAssembly 具有非常严格的安全模型,可以保护用户免受有缺陷或恶意代码的侵害。它在一个沙盒中运行,并且不能不通过适当的 API 就逃离沙盒。假设您想与 WebAssembly 外部通信,例如,通过更改 文档对象模型DOM)或从网络上下载文件。在这种情况下,您将需要使用 JavaScript 互操作来完成这项工作(稍后我们会详细介绍;不要担心——Blazor 会为我们解决这个问题)。

让我们看看一些代码,以便更熟悉 WebAssembly。

在本节中,我们将创建一个应用程序,该应用程序将两个数字相加并返回结果,使用的是 C(坦白说,这是我能接受的 C 的水平)。

我们可以将 C 编译成 WebAssembly,但这需要安装一些工具,所以我们不会一直这样做。这里的目的是让我们对 WebAssembly 在底层的工作方式有一个感觉。考虑以下代码:

int main() {
  return 1+2;
} 

这的结果将是数字 3

WebAssembly 是一种栈式机器语言,这意味着它使用栈来执行其操作。

考虑以下代码:

1+2 

大多数编译器都会优化代码并返回 3

但让我们假设所有指令都应该被执行。这是 WebAssembly 会这样做的方式:

  1. 它将从将 1 压入栈中(指令:i32.const 1)开始,然后是 2 压入栈中(指令:i32.const 2)。此时,栈中包含 12

  2. 然后,我们必须执行 add 指令(i32.add),这将弹出(获取)栈顶的两个值(12),将它们相加,并将新值推入栈中(3)。

这个演示表明我们可以从 C 代码构建 WebAssembly。尽管我们不需要达到这个水平来理解 WebAssembly(Blazor 为我们处理所有这些),但我们在本书的后面部分(第十六章,深入 WebAssembly)将使用编译成 WebAssembly 的 C 代码和其他库。

其他语言

通常情况下,只有底层语言可以被编译成 WebAssembly(例如 C 或 Rust)。然而,有许多语言可以在 WebAssembly 上运行。以下是一些这些语言的优秀集合:github.com/appcypher/awesome-wasm-langs

WebAssembly 的性能非常出色(接近原生速度)——如此出色的性能以至于游戏引擎已经采用了这项技术,正是出于这个原因。Unity 以及 Unreal Engine 都可以被编译成 WebAssembly。

这里有一些在 WebAssembly 上运行的游戏的例子:

这是一个不同 WebAssembly 项目的优秀列表:github.com/mbasso/awesome-wasm

本节简要介绍了 WebAssembly 的工作原理;在大多数情况下,你不需要了解更多。我们将在本章的后面部分深入探讨 Blazor 如何使用这项技术。

要编写 Blazor 应用程序,我们可以利用 .NET 8 的力量,我们将在下一节中探讨。

介绍 .NET 8

.NET 是微软开发的一个平台,用于构建不同类型的应用程序,包括 Web、移动和桌面应用程序。多年来,.NET 团队一直在努力为我们开发者简化一切。他们一直在使一切变得更简单、更小、跨平台和开源——更不用说更容易利用你现有的 .NET 开发知识了。

.NET Core 是向更统一 .NET 的一大步。它允许微软重新构想整个 .NET 平台,以全新的方式构建它,并使其能够在更多平台上运行。

有三种不同类型的 .NET 运行时:

  • .NET Framework(完整 .NET)

  • .NET Core

  • Mono/Xamarin

不同的运行时有不同的功能和性能。这也意味着创建一个 .NET Core 应用程序(例如)需要安装不同的工具和框架。

.NET 5 是我们迈向单一 .NET 的旅程的开始。有了这个统一的工具链,创建、运行等体验在所有不同的项目类型之间变得相同。“框架”和“Core”已被从名称中删除。.NET 5 仍然以我们熟悉的方式模块化,所以我们不必担心将所有不同的 .NET 版本合并会导致 .NET 变得臃肿。

感谢 .NET 平台,你将能够使用 C# 和相同的工具链访问我们在本章开头提到的所有平台(Web、桌面、移动、游戏、云(或服务器端)、AI,甚至 IoT)。

Blazor 已经存在一段时间了。在 .NET Core 3 中,Blazor Server 的第一个版本发布,而在 2020 年的 Microsoft Build 上,微软发布了 Blazor WebAssembly。

在 .NET 5 中,我们为 Blazor 获得了许多新组件——例如预渲染和 CSS 隔离等。不用担心;我们将在整本书中逐一介绍这些内容。

在 .NET 6 中,我们获得了更多的功能,如热重载、本地 JavaScript、新组件等等,所有这些内容我们将在整本书中探讨。

在 .NET 7 中,我们为 Blazor 开发者提供了更多的增强功能。包括性能改进和 get/set/after 修饰符等。

2023 年 11 月,微软发布了 .NET 8,随之而来的是一切的改变。在开发过程中,这种新的 Blazor 应用程序开发方式被称为“Blazor United”,现在他们已经将其更新为简单的 Blazor。这是创建 Blazor 应用程序的新方法,它是一种很棒的方法。但让我们也为后面的章节留一些内容。

.NET 8 带来了性能改进、原生 Define、更好的源生成器等等。它也是一个 LTS(长期支持)版本。

从增强功能和数量来看,我只能得出结论,微软相信 Blazor,我也相信。

现在你已经了解了周围的一些技术,在下一节中,我们将介绍这本书的主角:Blazor。

介绍 Blazor

Blazor 是一个开源的 Web UI 框架。在同一句话中包含这么多术语,简单来说,就是你可以使用 HTML、CSS 和 C# 创建交互式 Web 应用程序,Blazor 提供了完整的支持,包括绑定、事件、表单和验证、依赖注入、调试等等,我们将在这本书中探讨这些内容。

2017 年,Steve Sanderson(因创建 Knockout JavaScript 框架而闻名,并在微软的 ASP.NET 团队工作)在开发者大会 NDC Oslo 上即将进行一场名为 Web Apps can’t really do that, can they? 的演讲。

但 Steve 想要展示一个酷炫的演示,所以他想着,“是否可以在 WebAssembly 中运行 C#?”,他在 GitHub 上找到一个名为 Dot Net Anywhere 的旧停用项目,该项目是用 C 编写的,并使用工具(与我们刚刚做的类似)将 C 代码编译成 WebAssembly。

他让一个简单的控制台应用程序在浏览器中运行。这对大多数人来说可能是一个精彩的演示,但 Steve 想要更进一步。他想着,“是否可以在其上创建一个简单的 Web 框架?”,并继续探索是否也可以让工具工作。

当他进行会议时,他有一个可以创建新项目、创建带有出色工具支持的待办事项列表并在浏览器中运行项目的有效示例。

Damian Edwards(.NET 团队)和 David Fowler(.NET 团队)也参加了 NDC 会议。Steve 向他们展示了即将演示的内容,他们形容说,他们的头都炸了,下巴都掉了。

就这样,Blazor 的原型诞生了。

Blazor 这个名字来自 BrowserRazor(这是用于组合代码和 HTML 的技术)的结合。添加一个 L 使名字听起来更好,但除此之外,它没有真正的意义或缩写。

Blazor 有几种不同的版本,包括 Blazor Server、Blazor WebAssembly、Blazor Hybrid(使用 .NET MAUI)和服务器端渲染。

不同的版本有一些优点和缺点,所有这些我将在接下来的章节中介绍。

Blazor Server

Blazor Server 使用 SignalR 在客户端和服务器之间进行通信,如下所示:

图 1.1:Blazor Server 概述

SignalR 是一个开源的实时通信库,它将在客户端和服务器之间建立连接。SignalR 可以使用许多不同的数据传输方式,并自动根据您的服务器和客户端能力选择最佳的传输协议。SignalR 总是会尝试使用 WebSockets,这是一种内置在 HTML5 中的传输协议。如果 WebSockets 未启用,它将优雅地回退到另一个协议。

Blazor 是用可重用的 UI 元素构建的,称为 组件(关于组件的更多内容请参阅 第四章理解基本 Blazor 组件)。每个组件都包含 C# 代码和标记。组件可以包含其他组件。您可以使用 Razor 语法混合标记和 C# 代码,或者如果您愿意,可以在 C# 中完成所有操作。组件可以通过用户交互(按按钮)或触发器(如计时器)进行更新。

组件被渲染成渲染树,这是 DOM 的二进制表示,包含对象状态以及任何属性或值。渲染树将跟踪与上一个渲染树的任何变化,然后仅通过 SignalR 以二进制格式发送更改的内容来更新 DOM。

JavaScript 将在客户端接收更改并相应地更新页面。如果我们将其与传统 ASP.NET 进行比较,我们只渲染组件本身,而不是整个页面,并且我们只发送实际更改到 DOM,而不是整个页面。

Blazor 服务器版有一些优点:

  • 它包含足够的代码来确保连接被下载到客户端,因此网站占用空间小,这使得网站启动非常快。

  • 由于所有内容都在服务器上渲染,Blazor 服务器版对搜索引擎优化(SEO)更友好。

  • 由于我们是在服务器上运行,应用程序可以充分利用服务器的功能。

  • 该网站将在不支持 WebAssembly 的旧版网络浏览器上运行。

  • 代码在服务器上运行并停留在服务器上;无法反编译代码。

  • 由于代码是在您的服务器(或云中)上执行的,您可以直接调用组织内的服务和数据库。

当然,Blazor 服务器版也有一些缺点:

  • 由于渲染是在服务器上完成的,您需要始终连接到服务器。如果您有糟糕的互联网连接,网站可能无法工作。与非 Blazor 服务器网站相比,最大的区别是,非 Blazor 服务器网站可以发送页面并断开连接,直到请求另一个页面。在 Blazor 中,该连接(SignalR)必须始终连接(轻微断开是可以接受的)。

  • 由于它需要连接,因此没有离线/PWA渐进式网络应用)模式。

  • 每次点击或页面更新都必须进行往返服务器,这可能会导致更高的延迟。重要的是要记住,Blazor 服务器只会发送更改后的数据。我个人没有经历过任何缓慢的响应时间。

  • 由于我们必须与服务器建立连接,服务器的负载增加,这使得扩展变得困难。为了解决这个问题,您可以使用 Azure SignalR 集线器来处理持续连接,并让您的服务器专注于内容交付。

  • 每个连接都会在服务器的内存中存储信息,这增加了内存使用,并使负载均衡更加困难。

  • 要运行 Blazor 服务器,您必须在启用了 ASP.NET Core 的服务器上托管它。

在我的工作场所,我们已经有了一个大型网站,因此我们决定为我们的项目使用 Blazor 服务器。我们有一个客户门户和一个内部 CRM 工具,我们的方法是一次转换一个组件,将其转换为 Blazor 组件。

我们很快意识到,在大多数情况下,重新制作组件在 Blazor 中比继续使用 ASP.NET MVC 并添加功能要快。随着转换,最终用户的用户体验UX)甚至变得更好。

页面加载速度更快。我们可以根据需要重新加载页面的一部分,而不是整个页面,等等。

我们发现 Blazor 引入了一个新问题:页面变得快了。我们的用户不明白数据是否已保存,因为什么都没发生;事情确实发生了,但发生得太快,以至于用户注意不到。突然间,我们不得不更多地考虑用户体验和如何通知用户发生了变化。这当然是 Blazor 的一个非常积极的副作用。

Blazor 服务器不是运行 Blazor 的唯一方式——你还可以使用 WebAssembly 在客户端(网络浏览器)上运行它。

Blazor WebAssembly

另有一个选择:你可以在 WebAssembly 中运行 Blazor,而不是在服务器上运行它。

Mono 运行时是一个工具,它允许你在各种操作系统上运行用 C#和其他.NET 语言编写的程序,而不仅仅是 Windows。

微软已经将 Mono 运行时(用 C 编写)编译成 WebAssembly。

Blazor 的 WebAssembly 版本与服务器版本非常相似,如下面的图所示。我们已经将所有内容从服务器上移除,现在它正在我们的网络浏览器中运行:

图片

图 1.2:Blazor WebAssembly 概述

仍然会创建渲染树,而不是在服务器上运行 Razor 页面,现在它们正在我们的网络浏览器中运行。由于 WebAssembly 没有直接的 DOM 访问,所以 Blazor 使用直接的 JavaScript 互操作来更新 DOM。

编译成 WebAssembly 的 Mono 运行时称为dotnet.wasm。页面包含一小段 JavaScript,确保加载dotnet.wasm。然后,它将下载blazor.boot.json,这是一个包含应用程序运行所需的所有文件的 JSON 文件,以及应用程序的入口点。

如果我们查看在 Visual Studio 中启动新 Blazor 项目时创建的默认示例网站,Blazor.boot.json文件包含 63 个需要下载的依赖项。所有依赖项都会被下载,然后应用程序启动。

正如我们之前提到的,dotnet.wasm是编译成 WebAssembly 的 Mono 运行时。它运行.NET DLLs——你编写的和.NET Framework(运行你的应用程序所需的)——在你的浏览器中。

当我第一次听说这件事时,我有点不舒服。整个.NET 运行时都在我的浏览器中运行?!但过了一会儿,我意识到这是多么令人惊叹。你可以在你的网络浏览器中运行任何.NET Standard DLLs。

在下一章中,我们将探讨当 WebAssembly 应用程序启动时,代码的确切执行顺序以及发生了什么。

当然,Blazor WebAssembly 有一些优势:

  • 由于代码在浏览器中运行,创建PWA很容易。

  • 它不需要连接到服务器。Blazor WebAssembly 将离线工作。

  • 由于我们不在服务器上运行任何东西,我们可以使用任何后端服务器或文件共享(不需要后端有.NET 兼容的服务器)。

  • 没有往返意味着你可以更快地更新屏幕(这就是为什么有些游戏引擎使用 WebAssembly)。

Blazor WebAssembly 也有一些缺点:

  • 即使与其他大型网站相比,Blazor WebAssembly 的足迹也很大,并且需要下载大量文件。

  • 要访问任何站内资源,你需要创建一个 Web API 来访问它们。你不能直接访问数据库。

  • 代码在浏览器中运行,这意味着它可以被反编译。所有应用开发者都习惯了这一点,但这对网络开发者来说可能并不那么常见。

我想测试一下 WebAssembly!当我七岁的时候,我得到了我的第一台电脑,一台 Sinclair ZX Spectrum。我记得我坐下来编写了以下内容:

10 PRINT "Jimmy"
20 GOTO 10 

那是我的代码;我让电脑反复在屏幕上写我的名字!

那就是我决定我想成为一名开发者,让电脑做事情的时候。

成为开发者后,我想重温我的童年,并决定我想构建一个 ZX Spectrum 模拟器。在许多方面,模拟器已经成为了我在遇到新技术时的测试项目,而不是一个简单的Hello World。我曾在 Gadgeteer、Xbox One 甚至 HoloLens(仅举几个平台/设备)上运行它。

但我的模拟器能在 Blazor 中运行吗?

我只花了几个小时就通过利用已经构建的.NET Standard DLL 使模拟器与 Blazor WebAssembly 兼容;我只需要编写特定于这个实现的代码,比如键盘和图形。这也是 Blazor(无论是服务器端还是 WebAssembly)如此强大的原因之一:它可以运行已经制作好的库。你不仅可以利用你的 C#知识,还可以利用庞大的生态系统和.NET 社区。

你可以在这里找到模拟器:zxbox.com。这是我最喜欢的工作项目之一,因为我一直在找到优化和改进模拟器的方法。

以前,构建交互式 Web 应用程序只能使用 JavaScript。现在,我们知道我们可以使用 Blazor WebAssembly 和 Blazor Server,但这两个新选项中哪一个才是最好的?

Blazor WebAssembly 与 Blazor Server 的比较

我们应该选择哪一个?答案始终是,这取决于。你已经看到了两者的优缺点。

如果你有一个想要迁移到 Blazor 的现有网站,我建议选择服务器端;一旦迁移完成,你就可以做出新的决定,是否也要使用 WebAssembly。这样,迁移网站的部分就很容易,而且使用 Blazor Server 的调试体验更好。

假设你的网站运行在移动浏览器或另一个不可靠的互联网连接上。在这种情况下,你可能会考虑使用具有离线功能的(PWA)场景和 Blazor WebAssembly,因为 Blazor Server 需要一个持续的连接。

WebAssembly 的启动时间有点慢,但有一些方法可以将两种托管模型结合起来,以获得两者的最佳效果。我们将在 第十六章深入 WebAssembly 中介绍这一点。

在这个问题上,没有一劳永逸的解决方案,但请了解其优缺点,并看看它们如何影响您的项目和用例。

在 .NET 8 中,我们有更多机会混合和匹配不同的技术,因此这个问题变得不那么相关,因为我们可以选择让一个组件运行 Blazor 服务器,另一个运行 Blazor WebAssembly(关于这一点将在本章后面详细说明)。

我们可以在服务器端和客户端运行 Blazor,但桌面和移动应用怎么办呢?

Blazor 混合/.NET MAUI

.NET MAUI 是一个跨平台的应用程序框架。这个名字来源于 .NET Multi-platform App UI,是 Xamarin 的下一个版本。我们可以使用传统的 XAML 代码创建我们的跨平台应用程序,就像在 Xamarin 中一样。然而,.NET MAUI 也针对桌面操作系统,这将使我们能够在 Windows 和 macOS 上运行我们的 Blazor 应用程序。

.NET MAUI 有自己的模板,使我们能够在 .NET MAUI 应用程序中使用 Blazor WebView 运行 Blazor。这被称为 Blazor 混合。Blazor 混合的工作方式与其他托管模型(Blazor 服务器和 Blazor WebAssembly)类似。它有一个渲染树,并更新 Blazor WebView,这是 .NET MAUI 中的一个浏览器组件。这可能有点过于简化,但我们有一个关于 Blazor 混合的整个章节(第十八章访问 .NET MAUI)。使用 Blazor 混合,我们还可以访问原生 API(不仅仅是 Web API),使我们的应用达到另一个层次。

我们将在 第十八章访问 .NET MAUI 中查看 .NET MAUI。

有时候我们不需要交互式组件,我们只需要渲染一些内容然后完成。在 .NET 8 中,我们有了新的方法来做这件事。

服务器端渲染 (SSR)

服务器端渲染是 Blazor 中的新成员。它使得可以使用 Razor 语法构建服务器端渲染的网页,就像 MVC 或 Razor Pages 一样。这被称为静态服务器端渲染。它有一些额外的功能,即使在整个页面重新加载的情况下,也能保持滚动位置,这被称为增强表单导航。这将只渲染静态页面,没有交互性(有一些例外)。还有一种称为流式渲染的方式,可以更快地加载页面。这种模式称为流式服务器端渲染。在长时间运行的任务中,流式渲染会首先发送它拥有的 HTML,然后在长时间运行的任务完成后更新 DOM,给它带来更互动的感觉。

但有时我们想要交互性,选择 Blazor 服务器或 Blazor WebAssembly 可能有点困难。但如果我告诉你我们不再需要选择呢?我们可以混合使用。

之前被称为 Blazor United 的功能

当微软第一次提到这个功能时,它被称为“Blazor United”,但现在它只是 Blazor 的一部分,而不是一个额外功能。我仍然想提到这个名字,因为社区仍在使用它,而且你很可能已经听说过它,想知道为什么我没有提到它。

这是一个非常酷的功能:我们可以选择和决定哪些组件将使用 SSR 运行,哪些组件将使用 Blazor Server、Blazor WebAssembly 或(希望你能坐下来听这个)两者的混合。以前,我们不得不在这两者(Blazor Server 或 Blazor WebAssembly)之间选择其一,但现在我们可以结合这些技术,取其精华。我们现在可以告诉每个组件我们希望它如何渲染,并且我们可以在整个网站上混合搭配。新的“自动”功能意味着当我们的用户第一次访问网站时,它将运行 Blazor Server。这是为了快速建立连接,尽可能快地将数据传送给用户。在后台,WebAssembly 版本将被下载并缓存,以便下次他们访问网站时,它将使用缓存的 Blazor WebAssembly 版本。如果 WebAssembly 版本可以在 100 毫秒内下载并启动,它将只加载 WebAssembly 版本。如果需要更长的时间,它将启动 Blazor Server 并在后台下载。这是我们加快 Blazor 网站下载速度的一种方法。我们可以结合所有这些技术,在服务器端使用静态服务器端渲染预先渲染内容,使用 Blazor Server(使用 SignalR)使网站交互,然后切换到 Blazor WebAssembly 而不花费“长时间”下载。

摘要

在本章中,我们讨论了 Blazor 的创建及其底层技术,例如 SignalR 和 WebAssembly。你还了解了渲染树以及 DOM 如何更新,以便你了解 Blazor 在底层是如何工作的。

我们概述了你可以与 Blazor 一起使用的不同技术,例如服务器端(Blazor Server)、客户端(WebAssembly)、桌面和移动(Blazor Hybrid)。这个概述应该帮助你决定为你的下一个项目选择哪种技术。

我们讨论了为什么 Blazor 是 .NET 开发者的一个不错的选择。

我们探讨了 SSR 和(据我所知).NET 8 为 Blazor 提供的最重要的功能,即之前被称为 Blazor United 的功能。

在接下来的章节中,我将带你了解各种场景,以便你掌握从升级旧/现有网站到创建新的服务器端网站,再到创建新的 WebAssembly 网站的全部知识。

在下一章中,我们将通过配置我们的开发环境并创建和检查我们的第一个 Blazor 应用程序来“动手实践”。

进一步阅读

作为 .NET 开发者,你可能对 Uno Platform (platform.uno/) 感兴趣,它使得在 XAML 中创建 UI 并将其部署到许多不同的平台成为可能,包括 WebAssembly。

如果你想了解 ZX Spectrum 模拟器的构建过程,可以在此处下载源代码:github.com/EngstromJimmy/ZXSpectrum.

加入我们的 Discord 社区

加入我们的 Discord 空间,与作者和其他读者进行讨论:

packt.link/WebDevBlazor3e

第二章:创建您的第一个 Blazor 应用程序

在本章中,我们将设置我们的开发环境,以便我们可以开始开发 Blazor 应用程序。我们将创建我们的第一个 Blazor 应用程序,并了解项目结构。

到本章结束时,您将拥有一个可工作的开发环境,并创建了一个可以运行混合流式服务器端渲染、Blazor 服务器和 Blazor WebAssembly 的 Blazor 应用程序。

在本章中,我们将涵盖以下内容:

  • 设置您的开发环境

  • 创建我们的第一个 Blazor 应用程序

  • 使用命令行

  • 确定项目结构

技术要求

我们将创建一个新的项目(一个博客引擎),并在整本书中继续对该项目进行工作。

您可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter02 找到本章结果的源代码。

设置您的开发环境

在本书中,我们将重点关注 Windows 开发,并且任何截图都将来自 Visual Studio(除非另有说明)。但由于 .NET 8 是跨平台的,我们将介绍如何在 Windows、macOS 和 Linux 上设置您的开发环境。

所有平台的链接可以在这里找到:visualstudio.microsoft.com/.

我们可以从网页上下载 Visual Studio 或 Visual Studio Code。

Windows

在 Windows 上,我们有很多不同的选项来开发 Blazor 应用程序。Visual Studio 2022 是我们可以使用的最强大的工具。

有三个不同的版本,如下所示:

  • 社区 2022

  • 专业 2022

  • 企业 2022

简而言之,社区版是免费的,而其他版本则需要付费。社区版确实有一些限制,我们可以在以下链接中比较不同的版本:visualstudio.microsoft.com/vs/compare/.

对于本书,我们可以使用这些版本中的任何一种。请按照以下步骤操作:

  1. visualstudio.microsoft.com/vs/ 下载 Visual Studio 2022。选择适合您的版本。

  2. 安装 Visual Studio,并在安装过程中确保选择 ASP.NET 和 Web 开发,如图 图 2.1 所示:

计算机的截图  自动生成的描述

图 2.1:Windows 上 Visual Studio 2022 的安装

  1. 在右侧是一个将安装的所有组件的列表。请检查 .NET WebAssembly Build Tools。列表中可能还有 .NET6 或 .NET 7 版本,但我们想要不带版本号的版本。

我们还可以使用 Visual Studio Code 在 Windows 上开发 Blazor,但我们将不会讨论 Windows 的安装过程。

macOS 和 Linux(或 Windows)

Visual Studio Code 是跨平台的,这意味着我们可以在 Linux、macOS 或 Windows 上使用它。

不同版本可在 code.visualstudio.com/Download 获取。

安装完成后,我们还需要添加两个扩展:

  1. 打开 Visual Studio Code 并通过按 Shift + command + X(在 macOS 上)或 Ctrl+ Shift+ X(在 Linux 和 Windows 上)打开扩展面板。

  2. 搜索 Visual Studio Code 的 C# 开发工具包 并点击 安装。您可能需要一个 Microsoft 账户来安装 C# 开发工具包。

  3. 搜索 JavaScript 调试器(夜间版) 并点击 安装

还有其他跨平台的 IDE,例如 JetBrains Rider,有些人更喜欢使用。

现在一切都已经设置好了,让我们创建我们的第一个应用。

创建我们的第一个 Blazor 应用程序

在整本书中,我们将创建一个博客引擎。您不需要学习很多业务逻辑;该应用易于理解,但将涉及您在构建 Blazor 应用时将面临的技术和领域。

我有机会与 Steve Sanderson(Blazor 的创造者)和 Dan Roth(ASP.NET 的项目经理)讨论该项目。我们得出的结论是,这将展示 Blazor 最重要的功能。

该项目将允许访客阅读博客文章并发表评论。它还将有一个管理网站,我们可以在这里撰写博客文章,这将受到密码保护。

我们将创建一个利用 Blazor 服务器、Blazor WebAssembly 和流式服务器端渲染的应用。

重要提示

从现在起,本指南将使用 Visual Studio 2022,但其他平台也有类似创建项目的方法。

探索模板

在 .NET 8 中,微软减少了我们可访问的模板数量。我们将在 第四章理解基本 Blazor 组件 中进一步探讨它们。这一章将为您提供一个快速概述。

在 .NET 7 中,根据我们是否需要示例代码,我们有不同的模板,但在 .NET 8 中我们只有两个。我们还有一个 Blazor 混合模板 (.NET MAUI),但我们将会在 第十八章访问 .NET MAUI 中回到它。

Blazor Web 应用

Blazor Web 应用 模板为我们提供了一个 Blazor 应用。一旦我们选择了这个模板,我们就可以选择我们希望应用如何运行。我们可以使用示例代码或无代码配置我们的应用。我们可以选择是否支持交互式组件以及我们想要的交互类型。我们还可以选择是否为每个组件或整个应用指定渲染模式。因此,一开始,我们不需要选择是否要选择 Blazor 服务器或 Blazor WebAssembly;我们可以混合搭配。

如果我们添加示例页面,我们将获得一些组件来查看 Blazor 应用看起来像什么,以及一些基本的设置和菜单结构。它还包含添加 Bootstrap、隔离 CSS 以及类似内容的代码(见 第九章共享代码和资源)。

这是本书中将使用的模板,以便更好地理解事物是如何组合在一起的。

Blazor WebAssembly 独立应用

Blazor WebAssembly Standalone App模板(正如其名所示)为我们提供了一个 Blazor WebAssembly 独立应用程序。在这里,我们可以选择是否要包含示例页面。它包含一些组件,以展示 Blazor 应用程序的外观,以及一些基本的设置和菜单结构。它还包含添加 Bootstrap、独立 CSS 等代码(见第九章共享代码和资源)。那么为什么我们有这个模板呢?嗯,Blazor Web App 在某种程度上依赖于服务器渲染技术。如果你想要能够从文件共享、GitHub Pages 或 Azure Static Web Apps(仅举几个例子)运行你的应用程序,这个模板就是为你准备的。

创建 Blazor Web 应用程序

首先,我们将创建一个 Blazor 服务器应用程序并对其进行操作:

  1. 启动 Visual Studio 2022,你将看到以下屏幕:

计算机屏幕截图  自动生成描述

图 2.2:Visual Studio 启动屏幕

  1. 点击创建新项目,在搜索栏中输入blazor

  2. 你将获得不同模板的列表——这是.NET 7 和.NET 8 模板的混合。现在我们需要选择项目的模板。从搜索结果中选择Blazor Web App并点击下一步

计算机屏幕截图  自动生成描述

图 2.3:Visual Studio 创建新项目屏幕

  1. 现在命名项目(这是任何项目中最难的部分,但别担心,我已经完成了这个步骤!)!将应用程序命名为BlazorWebApp。将解决方案名称更改为MyBlog并点击下一步

计算机屏幕截图  自动生成描述

图 2.4:Visual Studio 配置新项目屏幕

  1. 接下来,选择我们应该创建哪种类型的 Blazor 应用程序。从下拉菜单中选择.NET 8.0(长期支持)。将身份验证类型设置为。勾选配置以启用 HTTPS。将交互式渲染模式设置为自动(服务器和 Webassembly)并将交互性位置设置为每页/组件

  2. 选择包含示例页面。取消选择不使用顶级语句。取消选择加入.NET Aspire 编排并点击创建

计算机屏幕截图  自动生成描述

图 2.5:Visual Studio 创建新 Blazor 应用程序屏幕

  1. 现在通过按Ctrl + F5(我们也可以在调试 | 不调试启动菜单下找到它)来运行应用程序。

恭喜!你刚刚创建了你第一个 Blazor Web 应用程序。网站应该看起来像图 2.6所示:

计算机屏幕截图  自动生成描述

图 2.6:一个新的 Blazor Web App

探索一下网站,导航到计数器天气以了解加载时间,并查看示例应用程序的功能。

不同的页面有不同的运行方式。计数器页面是交互式的,并使用自动渲染模式。这意味着如果加载 WebAssembly 版本需要超过 100 毫秒,网页将首先启动一个 SignalR 连接,以便快速加载组件。在后台,WebAssembly 版本将被下载并缓存。下次用户访问我们的网站时,它将使用缓存的版本并快速加载。

浏览器下载页面、一些 CSS 和 blazor.web.js,这是负责设置网站的文件。在某些情况下,它是一个返回服务器的 SignalR 连接,在某些情况下,它启动 WebAssembly。

计数器页面配置为 RenderMode:Auto,这意味着它将首先使用 Blazor Server 进行渲染,然后任何后续请求都将使用 WebAssembly 进行更新。一旦切换到计数器页面,就会在后台下载大量文件。

在这种情况下,当页面被下载时,将触发必要的 JavaScript 文件的下载。然后,下载 blazor.boot.json图 2.13 展示了 blazor.boot.json 部分内容的示例:

文本的特写  自动生成的描述

图 2.7:blazor.boot.json 文件的部分内容

blazor.boot.json 中最重要的内容是入口程序集,这是浏览器应该开始执行 DLL 的名称。它还包括应用程序运行所需的所有框架 DLL。现在,我们的应用程序知道它需要启动什么。在 .NET 的早期版本中,文件是 DLL 文件。在 .NET 8 中,我们下载 .wasmfiles

默认情况下,Blazor 将使用 Webcil,这是一种有效载荷格式。Webcil 将打包 DLLs。一些提供商已经关闭了对 DLL 文件的支持,并且防病毒程序与 DLL 文件存在问题,因此这是一个很棒的更新。

JavaScript 将下载 blazor.boot.json 中提到的所有资源:这是一个混合体,包括您的代码编译成的 .NET Standard DLL、Microsoft .NET Framework 代码以及您可能使用的任何社区或第三方 DLL。然后,JavaScript 下载 dotnet.native.wasm,这是编译成 WebAssembly 的 Mono 运行时,它将启动您的应用程序。

现在,我们已经有了项目的基线。在这本书的整个过程中,我们将使用 Visual Studio,但还有其他方法可以运行您的 Blazor 网站,例如使用命令行。命令行是一个超级强大的工具,在下一节中,我们将探讨如何使用命令行设置项目。

使用命令行

在 .NET 5 中,我们获得了一个名为 dotnet.exe 的超级强大工具。之前使用过 .NET Core 的开发者已经熟悉这个工具了,但在 .NET 5 中,它不再仅限于 .NET Core 开发者使用。

它可以完成 Visual Studio 可以做的许多事情,例如创建项目、添加和创建 NuGet 包,等等。在下面的示例中,我们将使用 dotnet 命令创建一个 Blazor Server 和一个 Blazor WebAssembly 项目。

使用命令行创建项目

以下步骤是为了展示使用命令行的强大功能。我们将在本书的后续内容中不会使用此项目,所以如果你不想尝试,可以跳过这一部分。CLI 是跨平台的,因此这也可以在 Linux 和 macOS 上使用。

要使用 Blazor 应用程序创建解决方案,你可以执行以下操作:

dotnet new blazor -o BlazorWebApp 

我们在本书中不会深入探讨 CLI,但要知道它是一种创建项目、添加包以及更多操作的好方法。

注意:.NET CLI

理念是,你应该能够从命令行完成所有操作。如果你更喜欢使用命令行,你应该查看 .NET CLI;你可以在以下链接中了解更多关于 .NET CLI 的信息:docs.microsoft.com/en-us/dotnet/core/tools/

让我们回到 Blazor 模板,它为我们添加了许多文件。在下一节中,我们将查看 Visual Studio 为我们生成的内容。

确定项目结构

Visual Studio 将生成两个项目:Blazor App,这是服务器项目,以及 BlazorWebApp.Client,这是我们放置 WebAssembly 组件的地方。

现在,是时候查看不同的文件以及它们在不同项目中的可能差异了。在我们逐一查看时,请查看我们刚刚创建的两个项目中的代码(在 创建我们的第一个 Blazor 应用 部分)。

Program.cs (BlazorWebApp 项目)

Program.cs 是第一个被调用的类。因此,让我们从它开始看起。

Program.cs 文件看起来是这样的:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Counter).Assembly);
app.Run(); 

在 .NET 6 中,Microsoft 删除了 Startup.cs 文件,并将所有启动代码放入 Program.cs。它还使用了顶级语句,这使得代码略微不那么臃肿。

在这里有一些值得提到的事情。Program 类从添加我们应用程序中需要的所有依赖项开始。在这种情况下,我们添加了 RazorComponent,这使得我们能够运行 Razor 组件。然后,我们添加了 InteractiveServerComponents,这使我们能够访问运行 Blazor 服务器所需的所有对象。由于我们选择了自动渲染模式,我们还通过添加 InteractiveWebAssemblyComponents 获得了对 Blazor WebAssembly 的访问。

它还配置了 HTTP 严格传输安全HSTS),强制应用程序使用 HTTPS,并确保用户不会使用任何不受信任的资源或证书。我们还确保网站重定向到 HTTPS 以保护网站。

UseStaticFiles 允许下载静态文件,如 CSS 或图像。

UseAntiforgery 是一个方法,它将反伪造中间件添加到应用程序管道中,提供一层针对 跨站请求伪造CSRFXSRF)攻击的安全防护。

这种类型的攻击发生在恶意网络应用程序影响客户端浏览器与信任该浏览器的网络应用程序之间的交互时,通常会导致未经用户同意执行不希望的操作。

不同的 Use* 方法将请求代理添加到请求管道或中间件管道。每个请求代理(ExceptionHandlerHttpRedirectionStaticFiles 等)都按照它们在 Program.cs 中添加的顺序从上到下依次调用,然后再从下到上。

这就是为什么异常处理器是第一个被添加的原因。

如果在后续的任何请求代理中发生异常,异常处理器仍然能够处理它(因为请求会通过管道返回),如图 2.15 所示:

图 2.15 – 请求中间件管道

图 2.8:请求中间件管道

如果这些请求代理中的任何一个在处理静态文件等情况下处理请求,则不需要涉及路由,其余的请求代理将不会被调用。有时,添加请求代理的正确顺序是至关重要的;例如,我们希望在管道中尽早运行身份验证,以确保用户无法访问他们不应访问的内容。

备注:

如果你想深入了解,这里有一些更多信息:docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0

在类末尾,我们将 Razor 组件映射到 App 组件。我们添加不同的渲染模式和额外的程序集 – 在这种情况下,BlazorWebApp.Client 项目,这是我们的 WebAssembly 项目。

Program.cs (BlazorWebApp.Client)

位于 Blazor WebAssembly 项目的 program.cs 文件中不包含很多东西。

它看起来像这样:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
await builder.Build().RunAsync(); 

它只是设置主机构建器并使用默认配置。

App (BlazorWebApp)

接下来发生的事情是 App 组件运行。

它看起来像这样:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="BlazorWebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="img/blazor.web.js"></script>
</body>
</html> 

让我们逐一分析,看看我们能学到什么。它从 htmldoctype 和一个 head 标签开始。head 标签包含元标签和 样式表CSS)。base 标签是为了让应用程序找到适当的文件。如果我们,例如,在子文件夹(如 GitHub Pages)中托管我们的应用程序,我们需要修改 base 标签以反映这一点。HeadOutlet 组件用于我们在代码中添加诸如页面标题等内容(我们将在 第五章创建高级 Blazor 组件中回到这一点)。

Routes 组件是处理所有路由的组件,我们将在下一节中对其进行探讨。最后但同样重要的是,我们还有使所有这些成为可能的 JavaScript。

路由

Routes 组件是处理所有路由的组件。它看起来像这样:

<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router> 

此文件处理路由,查找要显示的合适组件(基于 @page 指令)。如果找不到路由,它会显示错误消息。在 第八章身份验证和授权中,我们将在此文件实现身份验证时对其进行更改。

Routes组件还包括一个默认布局。我们可以为每个组件覆盖布局,但通常,你将为你的网站有一个布局页面。在这种情况下,默认布局被称为MainLayout

FocusOnNavigate将在我们导航/加载新的组件/路由后,将焦点设置在特定的元素上——在本例中,是H1

MainLayout

MainLayout,我们可以在共享文件夹中找到,包含所有组件作为页面查看时的默认布局。MainLayout包含几个div标签,一个用于侧边栏,一个用于主要内容:

@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
            @Body
        </article>
</main>
</div>
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
<a class="dismiss">X</a>
</div> 

你在这份文档中需要的只有@inherits LayoutComponentBase@Body;其余的都是 Bootstrap。

Bootstrap 是开发响应式和移动优先网站中最受欢迎的 CSS 框架之一。

我们可以在App.Razor文件中找到对 Bootstrap 的引用。

它是由 Twitter/X 创建的,用于 Twitter/X。你可以在getbootstrap.com/了解更多关于 Bootstrap 的信息。

@inherits指令从LayoutComponentBase继承,其中包含使用布局所需的所有代码。@Body是组件将被渲染的位置(当作为页面查看时)。

它还包含 Blazor 的默认错误 UI。

在布局的顶部,你可以看到<NavMenu>,一个 Razor 组件。它位于Components/Layout文件夹中,看起来像这样:

<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorWebApp</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
            </NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
</div>
</nav>
</div> 

它包含左侧菜单,是一个标准的 Bootstrap 菜单。它还具有三个菜单项和汉堡菜单的逻辑(如果是在手机上查看)。这种导航菜单通常是用 JavaScript 完成的,但这个是用 CSS 和 C#完成的。

你会发现另一个组件,NavLink,它是框架内建的。它将渲染一个锚点标签,但也会检查当前路由。如果你当前位于与导航链接相同的路由/URL 上,它将自动向标签添加一个名为active的 CSS 类。

我们将遇到一些内置的组件,这些组件将帮助我们前进。模板中也有一些页面,但现在我们将暂时忽略它们,并在下一章中介绍组件时再进行讨论。

CSS

Components/Layout文件夹中,有两个 CSS 文件:NavMenu.razor.cssMainLayout.razor.css

这些文件是仅影响特定组件(名称的第一部分)的 CSS 样式。我们将在第九章中返回一个称为隔离 CSS 的概念,共享代码和资源

摘要

在本章中,我们成功搭建了开发环境,并创建了我们的第一个 Blazor 应用程序。我们学习了类、组件和布局的调用顺序,这使得跟踪代码变得更加容易。

在下一章中,我们将从 Blazor 中暂时休息一下,看看如何管理状态,并设置一个存储我们的博客文章的仓库。

第三章:管理状态 – 第一部分

在本章中,我们将开始探讨状态管理。本章的续篇是第十一章管理状态 – 第二部分

管理状态或持久化数据的方法有很多。一旦我们离开组件,状态就会消失。如果我们从示例页面点击计数器按钮,看到计数器计数增加然后导航离开,我们就不知道需要点击计数器按钮多少次,并不得不从头开始。您无法想象我多年来点击那个计数器按钮的次数。这是一个简单而强大的 Blazor 演示,也是 Steve 在 2017 年原始演示的一部分。

为了快速入门,我已经将本章分为两部分。在本章中,我们将专注于数据访问,而在第二部分中,我们将回到更多状态管理。由于本书专注于 Blazor,因此我们不会探索如何连接到数据库,而是创建简单的 JSON 存储。

在第一版中,我们使用 Entity Framework 连接到数据库,但有些人不习惯使用 Entity Framework,他们很快就陷入了困境。使用 Entity Framework 本身就是一本书,所以我选择不将其包含在本书中,以消除任何额外的复杂性。

在 GitHub 上的 repo 中,您可以找到更多将数据存储在数据库中的示例,例如RavenDBMSSQL

我们将使用一个称为仓储模式的通用模式。

我们还将创建一个 API 来访问 JSON 存储库中的数据。

到本章结束时,您将学会如何创建 JSON 存储库和 API。

我们将涵盖以下主要主题:

  • 创建数据项目

  • 将 API 添加到 Blazor

技术要求

确保您已经遵循了前面的章节,或者使用 GitHub 上的Chapter02文件夹作为起点。

您可以在github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter03找到本章结果的源代码。

创建数据项目

存储数据的方式有很多:文档数据库、关系数据库和文件,仅举几例。为了避免本书的复杂性,我们将使用最简单的方式为我们的项目创建博客文章,将它们存储在文件夹中的 JSON 中。

要保存我们的博客文章,我们将使用存储在文件夹中的 JSON 文件,为此,我们需要创建一个新的项目。

创建新项目

我们可以从 Visual Studio 内部创建一个新的项目(说实话,我会这样做),但为了了解.NET CLI,让我们从命令行操作。

要创建一个新的项目,请按照以下步骤操作:

  1. 打开 PowerShell 提示符。

  2. 导航到MyBlog文件夹。

  3. 通过输入以下命令创建一个类库(classlib):

    dotnet new classlib -o Data 
    

    dotnet工具现在应该已经创建了一个名为Data的文件夹。

  4. 我们还需要创建一个项目,以便我们可以放置我们的模型:

    dotnet new classlib -o Data.Models 
    
  5. 通过运行以下命令将新项目添加到我们的解决方案中:

    dotnet sln add Data
    dotnet sln add Data.Models 
    

它将在当前文件夹中查找任何解决方案。

我们将项目命名为 DataData.Models,这样它们的用途将很容易理解,并且很容易找到。

默认项目包含一个 class1.cs 文件 - 随意删除该文件。

下一步是创建数据类来存储我们的信息。

创建数据类

现在我们需要为我们的博客文章创建一个类。为此,我们将回到 Visual Studio:

  1. 在 Visual Studio 中打开 MyBlog 解决方案(如果尚未打开)。

    我们现在应该在解决方案中有一个名为 Data 的新项目。我们可能会收到一个弹出窗口询问我们是否想要重新加载解决方案;如果是,请点击 重新加载

  2. 右键单击 Data.Models 项目,然后选择 添加 | 。将类命名为 BlogPost.cs 并点击 添加

  3. 右键单击 Data.Models 项目,然后选择 添加 | 。将类命名为 Category.cs 并点击 添加

  4. 右键单击 Data.Models 项目,然后选择 添加 | 。将类命名为 Tag.cs 并点击 添加

  5. 右键单击 Data.Models 项目,然后选择 添加 | 。将类命名为 Comment.cs 并点击 添加

  6. 打开 Category.cs 并将内容替换为以下代码:

    namespace Data.Models;
    public class Category
    {
    	public string? Id { get; set; }
    	public string Name { get; set; } = string.Empty;
    } 
    

    Category 类包含 IdName 属性。Id 属性是字符串类型可能看起来有些奇怪,但这是因为我们将支持多种数据存储类型,包括 MSSQL、RavenDB 和 JSON。字符串是一个支持所有这些类型的优秀数据类型。Id 属性也可以为空,因此如果我们创建一个新的类别,我们将以空值作为 Id 发送。

  7. 打开 Tag.cs 并将内容替换为以下代码:

    namespace Data.Models;
    public class Tag
    {
        public string? Id { get; set; }
        public string Name { get; set; } = string.Empty;
    } 
    

    Tag 类包含一个 IdName

  8. 打开 Comment.cs 并将内容替换为以下代码:

    namespace Data.Models;
    public class Comment
    {
        public string? Id { get; set; }
        public required string BlogPostId { get; set; }
        public DateTime Date { get; set; }
        public string Text { get; set; } = string.Empty;
        public string Name { get; set; } = string.Empty;
    } 
    
  9. comment 类可以是 Blogpost 类的一部分,但为了使用相同的类对不同数据库类型进行操作,我们将评论作为一个单独的实体引用博客文章。

  10. 打开 BlogPost.cs 并将内容替换为以下代码:

    namespace Data.Models;
    public class BlogPost
    {
        public string? Id { get; set; }
        public string Title { get; set; } = string.Empty;
        public string Text { get; set; } = string.Empty;
        public DateTime PublishDate { get; set; }
        public Category? Category { get; set; }
        public List<Tag> Tags { get; set; } = new();
    } 
    

在这个类中,我们定义了博客文章的内容。我们需要一个 Id 来标识博客文章,一个标题,一些文本(文章),以及发布日期。我们还在类中有一个 Category 属性,它是 Category 类型。在这种情况下,一篇博客文章只能有一个类别,一篇博客文章可以包含零个或多个标签。我们使用 List<Tag> 定义 Tag 属性。

我们现在已经创建了一些我们将使用的类。我已将这些类的复杂性保持在最低,因为我们在这里是为了学习 Blazor。

接下来,我们将创建一种存储和检索博客文章信息的方法。

创建接口

在本节中,我们将创建一个 API。

我们将创建一个具有直接数据库访问的 API 和一个将通过 Web API 获取数据的 API。

第七章创建 API 中,我们将回到创建 Web API。为什么我们要创建两个 API?

我们不是创建两个 API;我们正在创建一个具有直接数据库访问的服务和一个通过网络进行操作然后使用直接数据库访问的客户端。但我们将使用相同的接口来处理这两种情况,使得在服务器上使用一个,在客户端使用另一个成为可能。

在实际应用中,以统一的方式访问所有数据会更合理,而不是使用两种方式。但重点是展示混合匹配并选择适合你场景的正确方式。

我们将从具有直接数据库访问的 API 开始:

  1. 右键点击Data.Models项目,选择添加 | 新建文件夹,并将其命名为Interfaces

  2. 右键点击Interfaces文件夹,选择添加 |

  3. 在不同的模板列表中,选择接口,并将其命名为IBlogApi.cs

  4. 打开IBlogApi.cs,并将其内容替换为以下内容:

    namespace Data.Models.Interfaces;
    public interface IBlogApi
    {
        Task<int> GetBlogPostCountAsync();
        Task<List<BlogPost>> GetBlogPostsAsync(int numberofposts, int startindex);
        Task<List<Category>> GetCategoriesAsync();
        Task<List<Tag>> GetTagsAsync();
        Task<List<Comment>> GetCommentsAsync(string blogPostId);
        Task<BlogPost?> GetBlogPostAsync(string id);
        Task<Category?> GetCategoryAsync(string id);
        Task<Tag?> GetTagAsync(string id);
        Task<BlogPost?> SaveBlogPostAsync(BlogPost item);
        Task<Category?> SaveCategoryAsync(Category item);
        Task<Tag?> SaveTagAsync(Tag item);
        Task<Comment?> SaveCommentAsync(Comment item);
        Task DeleteBlogPostAsync(string id);
        Task DeleteCategoryAsync(string id);
        Task DeleteTagAsync(string id);
        Task DeleteCommentAsync(string id);
    } 
    
  5. 好吧,关于这个IBlogApi的事情是这样的。它基本上是我们处理所有博客内容的速查表,比如帖子、评论、标签和分类。需要获取一些帖子或者将其从存在中删除?这个接口就是你的首选。它主要是为了让我们在编写博客时生活更轻松,保持事情整洁和直接。

现在,我们有一个 API 接口,其中包含了我们需要列出博客帖子、标签和分类,以及保存(创建/更新)和删除它们的方法。接下来,让我们实现这个接口。

实现接口

想法是创建一个类,将我们的博客帖子、标签、评论和分类作为 JSON 文件存储在我们的文件系统中。我们将从实现直接访问实现开始。当我们直接从数据库访问信息而不是通过 Web API 时,我们可以使用这个直接访问实现。当我们在服务器上运行我们的组件并访问数据库时,我们的 Web API 也将使用它来访问数据库,但我们将回到第七章,创建 API。

要实现直接数据库访问实现的接口,请按照以下步骤操作:

  1. 首先,为了能够访问我们的数据模型,我们需要在我们的Data模型中添加一个引用。展开Data项目,右键点击依赖项节点。选择添加项目引用,并勾选Data.Models项目。点击确定

  2. 再次右键点击依赖项节点,但选择管理 NuGet 包。在浏览选项卡中,搜索Microsoft.Extensions.Options并点击安装

  3. 我们需要一个类来保存我们的设置。

    Data项目中,添加一个名为BlogApiJsonDirectAccessSetting.cs的新类,并将其内容替换为:

    namespace Data;
    public class BlogApiJsonDirectAccessSetting
    {
        public string BlogPostsFolder { get; set; } = string.Empty;
        public string CategoriesFolder { get; set; } = string.Empty;
        public string TagsFolder { get; set; } = string.Empty;
        public string CommentsFolder { get; set; } = string.Empty;
        public string DataPath { get; set; } = string.Empty;
    } 
    
  4. 这是一个保存我们的设置以及我们将用于存储 JSON 文件的文件夹的类。IOptions在配置依赖项期间在program中配置,并注入到所有请求特定类型的类中。

  5. 接下来,我们需要为我们的 API 创建一个类。右键单击 Data 项目,选择 添加 | ,并将类命名为 BlogApiJsonDirectAccess.cs

  6. 打开 BlogApiJsonDirectAccess.cs 文件,并将代码替换为以下内容:

    using Data.Models.Interfaces;
    using Microsoft.Extensions.Options;
    using System.Text.Json;
    using Data.Models;
    namespace Data;
    public class BlogApiJsonDirectAccess: IBlogApi
    {
    } 
    

    这是我们的 JSON 直接访问类的开始。它引用了 IBlogAPI,我们将实现接口想要的所有方法。

    错误列表应该包含许多错误,因为我们还没有实现这些方法。我们正在继承自 IBlogApi,因此我们知道要公开哪些方法。

  7. 为了能够读取设置,我们也添加了一种注入 IOptions 的方式。通过这种方式获取设置,我们不需要添加任何代码——它可以从数据库、设置文件,甚至可以硬编码。

    这是我最喜欢的获取设置的方式,因为代码的这一部分本身并不知道如何实现——相反,我们通过依赖注入添加所有配置。

    将以下代码添加到 BlogApiJsonDirectAccess 类中:

     BlogApiJsonDirectAccessSetting _settings;
        public BlogApiJsonDirectAccess( IOptions<BlogApiJsonDirectAccessSetting> option)
        {
            _settings = option.Value;
            ManageDataPaths();
        }
        private void ManageDataPaths()
        {
            CreateDirectoryIfNotExists(_settings.DataPath);
            CreateDirectoryIfNotExists($@"{_settings.DataPath}\{_settings.BlogPostsFolder}");
            CreateDirectoryIfNotExists($@"{_settings.DataPath}\{_settings.CategoriesFolder}");
            CreateDirectoryIfNotExists($@"{_settings.DataPath}\{_settings.TagsFolder}");
            CreateDirectoryIfNotExists($@"{_settings.DataPath}\{_settings.CommentsFolder}");
        }
        private static void CreateDirectoryIfNotExists(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
        } 
    

    我们获取注入的设置并确保我们有正确的数据文件夹结构。

  8. 现在,是时候实现 API 了,但首先,我们需要一些辅助方法来从我们的文件系统中加载数据。为此,我们将以下代码添加到我们的类中:

    private async Task<List<T>> LoadAsync<T>(string folder)
    {
        var list = new List<T>();
        foreach (var f in Directory.GetFiles($@"{_settings.DataPath}\{folder}"))
        {
            var json = await File.ReadAllTextAsync(f);
            var blogPost = JsonSerializer.Deserialize<T>(json);
            if (blogPost is not null)
            {
                list.Add(blogPost);
            }
        }
        return list;
    } 
    
  9. LoadAsync 方法是一个泛型方法,允许我们使用相同的方法加载博客文章、标签、评论和类别。它将在我们请求时从文件系统中加载数据。这是一个放置一些缓存逻辑的好地方,但如果我们使用数据库(我们总是会询问数据库),实现将更接近这个样子。

  10. 接下来,我们将添加几个帮助方法来操作数据,即 SaveAsyncDelete。添加以下方法:

    private async Task SaveAsync<T>(string folder, string filename, T item)
    {
        var filepath = $@"{_settings.DataPath}\{folder}\{filename}.json";
        await File.WriteAllTextAsync(filepath, JsonSerializer.Serialize<T>(item));
    }
    private Task DeleteAsync(string folder, string filename)
     {
         var filepath = $@"{_settings.DataPath}\{folder}\{filename}.json";
         if (File.Exists(filepath))
         {
             File.Delete(filepath);
         }
         return Task.CompletedTask;
     } 
    

    这些方法也是泛型的,以便尽可能多地共享代码并避免为每种类型的类(BlogPostCategoryCommentTag)重复代码。

  11. 接下来,是时候通过添加获取博客文章的方法来实现 API 了。添加以下代码:

    public async Task<int> GetBlogPostCountAsync()
     {
         var list = await LoadAsync<BlogPost>(_settings.BlogPostsFolder);
         return list.Count;
     }
     public async Task<List<BlogPost>> GetBlogPostsAsync(int numberofposts, int startindex)
     {
         var list = await LoadAsync<BlogPost>(_settings.BlogPostsFolder);
         return list.Skip(startindex).Take(numberofposts).ToList();
     } 
    public async Task<BlogPost?> GetBlogPostAsync(string id)
        {
            var list = await LoadAsync<BlogPost>(_settings.BlogPostsFolder);
            return list.FirstOrDefault(bp => bp.Id == id);
        } 
    

    GetBlogPostsAsync 方法接受一些参数,我们将在稍后用于分页。它将从我们的 JSON 存储中获取博客文章,并返回我们请求的文章,跳过并取回正确的数量以供分页使用。

    我们还有一个返回当前博客文章计数的函数,我们将用它来进行分页。最后但同样重要的是,我们有 GetBlogPostAsync 用于从我们的 JSON 存储中获取单个博客文章。

  12. 现在,我们需要为类别添加相同的方法。为此,添加以下代码:

     public async Task<List<Category>> GetCategoriesAsync()
        {
            return await LoadAsync<Category>(_settings.CategoriesFolder);
        }
        public async Task<Category?> GetCategoryAsync(string id)
        {
            var list = await LoadAsync<Category>(_settings.CategoriesFolder);
            return list.FirstOrDefault(c => c.Id == id);
        } 
    
  13. Category 方法没有分页支持。否则,它们应该看起来很熟悉,因为它们几乎与博客文章方法相同。

  14. 现在,是时候为标签做同样的事情了。添加以下代码:

     public async Task<List<Tag>> GetTagsAsync()
        {
            return await LoadAsync<Tag>(_settings.TagsFolder);
        }
        public async Task<Tag?> GetTagAsync(string id)
        {
            var list = await LoadAsync<Tag>(_settings.TagsFolder);
            return list.FirstOrDefault(t => t.Id == id);
        } 
    

    如我们所见,tag 代码基本上是类别代码的副本。

    我们还需要一种方法来检索博客文章的评论。我们不会创建一个检索单个 comment 的方法;我们总是获取特定文章的所有评论。添加以下方法:

     public async Task<List<Comment>> GetCommentsAsync(string blogPostId)
        {
            var list = await LoadAsync<Comment>(_settings.
    CommentsFolder);
            return list.Where(t => t.BlogPostId == blogPostId).ToList();
        } 
    

    此方法将获取博客文章的所有评论。

  15. 我们还需要几个用于保存数据的方法,所以接下来,我们将添加保存博客文章、分类、评论和标签的方法。

    添加以下代码:

    public async Task<BlogPost?> SaveBlogPostAsync(BlogPost item)
    {
        item.Id ??= Guid.NewGuid().ToString();
        await SaveAsync(_settings.BlogPostsFolder, item.Id, item);
        return item;
    }
    public async Task<Category?> SaveCategoryAsync(Category item)
    {
        item.Id ??= Guid.NewGuid().ToString();
        await SaveAsync(_settings.CategoriesFolder, item.Id, item);
        return item;
    }
    public async Task<Tag?> SaveTagAsync(Tag item)
    {
        item.Id ??= Guid.NewGuid().ToString();
        await SaveAsync(_settings.TagsFolder, item.Id, item);
        return item;
    }
    public async Task<Comment?> SaveCommentAsync(Comment item)
    {
        item.Id ??= Guid.NewGuid().ToString();
        await SaveAsync(_settings.CommentsFolder, item.Id, item);
        return item;
    } 
    

    我们首先要做的是检查项目的 id 是否为空。如果是,我们创建一个新的 Guid。这是新项目的 id。这将是存储在文件系统上的 JSON 文件的名称。

  16. 现在我们有了保存和获取项目的方法。但有时事情并不按计划进行,我们需要一种方法来删除我们创建的项目。接下来,我们将添加一些删除方法。添加以下代码:

     public async Task DeleteBlogPostAsync(string id)
        {
            await DeleteAsync(_settings.BlogPostsFolder, id);
    
            var comments = await GetCommentsAsync(id);
            foreach (var comment in comments)
            {
                if (comment.Id != null)
                {
                    await DeleteAsync(_settings.CommentsFolder, comment.Id);
                }
            }
        }
        public async Task DeleteCategoryAsync(string id)
        {
            await DeleteAsync(_settings.CategoriesFolder, id);
        }
        public async Task DeleteTagAsync(string id)
        {
            await DeleteAsync(_settings.TagsFolder, id);
        }
        public async Task DeleteCommentAsync(string id)
        {
            await DeleteAsync(_settings.CommentsFolder, id);
        } 
    

我们刚刚添加的代码调用了 DeleteAsync 方法,该方法删除了博客文章、标签、分类等。

我们的 JSON 存储已完成!

最后,文件系统上将有四个文件夹,一个用于博客文章,一个用于分类,一个用于评论,一个用于标签。

下一步是将 Blazor 项目添加并配置为使用我们新的存储。

将 API 添加到 Blazor

现在我们有了一种访问存储在文件系统上的 JSON 文件的方法。在 GitHub 上的仓库中,你可以找到更多使用 RavenDB 或 SQL Server 存储我们数据的方法,但请注意保持重点(Blazor)。

现在是时候将 API 添加到我们的 Blazor 服务器项目中:

  1. BlazorWebApp 项目中,添加对 Data 项目的项目引用。打开 Program.cs 并添加以下命名空间:

    using Data;
    using Data.Models.Interfaces; 
    
  2. .AddInteractiveWebAssemblyComponents(); 之后添加以下代码:

    builder.Services.AddOptions<BlogApiJsonDirectAccessSetting>().Configure(options =>
    {
        options.DataPath = @"..\..\..\Data\";
        options.BlogPostsFolder = "Blogposts";
        options.TagsFolder = "Tags";
        options.CategoriesFolder = "Categories";
        options.CommentsFolder = "Comments";
    });
    builder.Services.AddScoped<IBlogApi, BlogApiJsonDirectAccess>(); 
    
IOptions<BlogApiJsonDirectAccessSetting>, the dependency injection will return an object populated with the information we have supplied above. This is an excellent place to load configuration from our .NET configuration, a key vault, or a database.

我们还说明,当我们请求 IBlogAPI 时,我们将从我们的依赖注入中获取 BlogApiJsonDirectAccess 的实例。我们将在 第四章理解基本 Blazor 组件 中返回依赖注入。

现在,我们可以使用我们的 API 来访问 Blazor 项目中的数据库。

摘要

本章教会了我们如何创建一个简单的 JSON 存储库来存储我们的数据。我们还了解到,如果您想查看其他选项,GitHub 仓库中还有其他替代方案。

我们还创建了一个接口来访问数据,我们将在本书的后面部分使用它。

在下一章中,我们将学习组件,特别是 Blazor 模板中的内置组件。我们还将使用本章中创建的 API 和存储库创建我们的第一个组件。

第四章:理解基本 Blazor 组件

在本章中,我们将查看 Blazor 模板中提供的组件,并开始构建我们自己的组件。了解用于创建 Blazor 网站的不同的技术将有助于我们开始构建自己的组件。

Blazor 使用组件来完成大多数事情,因此我们将在这本书的整个过程中使用本章的知识。

我们将从这个章节的理论开始,并以创建一个组件来展示一些博客文章结束,这些博客文章使用我们在 第三章管理状态 – 第一部分 中创建的 API。

在本章中,我们将涵盖以下主题:

  • 探索组件

  • 学习 Razor 语法

  • 理解依赖注入

  • 更改渲染模式

  • 确定代码放置的位置

  • 生命周期事件

  • 参数

  • 编写我们的第一个组件

技术要求

确保你已经跟进了前面的章节,或者使用 Chapter03 文件夹作为起点。

你可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter04 找到本章结果的源代码。

探索组件

在 Blazor 中,一个 component 是一个包含小而独立的函数(代码和标记)的 .razor 文件,或者它可以作为一个页面使用。组件还可以托管其他组件。本章将向我们展示组件是如何工作的以及如何使用它们。

我们有三种不同的方式可以创建一个组件:

  • 使用 Razor 语法,代码和 HTML 在同一个文件中共享

  • 将代码后文件与 .razor 文件一起使用

  • 仅使用代码后文件

在本章中,我们将探讨不同的选项。首先,我们将探讨我们用于创建项目的模板中的组件;这些组件都使用了第一个选项,即 .razor 文件,其中我们在同一个文件中混合了代码和 HTML。

模板中的组件如下:

  • 计数器

  • 天气

计数器

counter 页面显示了一个按钮和一个计数器;如果我们点击按钮,计数器会增加。现在我们将把这个页面拆分开来,使其更容易理解。它位于 BlazorWebApp.Client 项目中的 Pages 文件夹内。

页面顶部是 @page 指令,这使得我们可以直接路由到组件,正如我们在这段代码中看到的那样:

@page "/counter" 

如果我们启动 BlazorWebApp 项目,并将 /counter 添加到 URL 的末尾,我们可以直接通过其路由访问组件。我们还可以使路由接受参数,但我们将稍后回到这一点。

在下面,我们有渲染模式:

@rendermode InteractiveAuto 

这是我们可以在特定组件上设置渲染模式的方法。这意味着当我们使用这个组件时,它将首先使用 Blazor 服务器(带有 SignalR)渲染页面,并在后台下载 WebAssembly 版本,以便下次我们加载页面时,它将运行 WebAssembly 版本。

接下来,让我们探索一下代码。要向页面添加代码,我们使用 @code 语句,并在该语句中可以添加普通的 C# 代码,如下所示:

@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
} 

在前面的代码块中,我们有一个设置为 0 的私有 currentCount 变量。然后,我们有一个名为 IncrementCount() 的方法,该方法将 currentCount 变量增加 1

我们通过使用 @ 符号来显示当前值。在 Razor 中,@ 符号表示是时候编写一些代码了:

<p role="status">Current count: @currentCount</p> 

如我们所见,Razor 非常智能,因为它能理解代码何时停止,标记何时继续,因此不需要添加额外的内容来从代码过渡到标记(更多内容将在下一节中介绍)。

如前例所示,我们在 HTML 标签和 @currentCount 之间混合,Razor 能够理解这种区别。接下来,我们有一个按钮,它是改变值的触发器:

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> 

这是一个带有 Bootstrap 类(使其看起来更美观)的 HTML 按钮。@onclick 将按钮的 onclick 事件绑定到 IncrementCount() 方法。如果我们不使用 @ 符号来使用 onclick,它将引用 JavaScript 事件而不会工作。

因此,当我们点击按钮时,它将调用 IncrementCount() 方法(在 图 4.1 中表示为 1),该方法增加变量(在 图 4.1 中表示为 2),由于变量发生了变化,UI 将自动更新(在 图 4.1 中表示为 3),如图所示:

图 4.1 – 计数组件的流程

图 4.1:计数组件的流程

counter 组件是在 BlazorWebApp.Client 项目中实现的,这是一个 WebAssembly 项目。在那个项目中,我们应该放置所有我们想要作为 WebAssembly 运行的组件。然后 BlazorWebApp 项目引用 BlazorWebApp.Client 项目,这样它就能找到所有组件,并且如果我们想的话,可以将其作为 Blazor 服务器组件运行。

天气

我们接下来要查看的组件是 Weather 组件。它位于 Components/Pages/Weather.razor 文件夹中。

Weather 组件介绍了新的流式渲染功能。文件最初看起来是这样的:

@page "/weather"
@attribute [StreamRendering(true)] 

就像 Counter 组件一样,我们首先定义一个路由。这个页面上没有渲染模式属性。该组件将使用 服务器端渲染SSR)进行渲染。这是所有组件的默认行为,除非指定,就像 Counter 组件一样。

当我们开始项目时,我们将 交互位置 设置为 每页/组件。这意味着当我们想要交互时,我们需要指定这一点。但是,有了 [StreamRendering(true)] 属性,我们将获得一种交互感。页面首先加载,显示加载文本。然后,使用相同的请求,我们获取其余的数据,就像一个 好吧,流。所以,我们将获得快速加载,无需等待数据,无需使用 WebAssembly 或 SignalR 添加交互性,但仍有一些加载进度。我们将在稍后的章节中进一步探讨这一点。

Weather 组件的 HTML 部分看起来像这样:

<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
            }
        </tbody>
</table>
} 

如果我们没有任何预报,它将显示“加载中...”,一旦我们有了数据,它将渲染一个显示数据的表格。

生成一些模拟数据的代码部分看起来像这样:

@code {
    private WeatherForecast[]? forecasts;
    protected override async Task OnInitializedAsync()
    {
        // Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToArray();
    }
    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
} 

当页面初始化时,WeatherForecast 数组会被填充随机数据。

这让我想起有人(作为玩笑)向 Dan Roth 在 GitHub 上的一个仓库提交了一个错误报告,报告称“天气预报不可靠。”

对话继续,“在我的伦敦之行中,天气预报功能完全准确,但在加利福尼亚却证明是误导性的。计数和整体紫色很棒。”Dan Roth 回复道:“感谢您的反馈!我会联系 .NET 核心框架团队,确保 System.Random 能更好地考虑加利福尼亚的天气模式。”

这是我喜欢 .NET 社区的原因之一。

如我们所见,通过使用 Razor 语法,我们可以无缝地将代码与 HTML 混合。代码会检查是否有任何数据——如果有,它将渲染表格;如果没有,它将显示一个加载信息。一旦我们有了数据,组件将自动更新。我们对 HTML 有完全的控制权,Blazor 不会向生成的 HTML 中添加任何内容。

有一些组件库可以使这个过程变得简单一些,我们将在下一章,第五章创建高级 Blazor 组件中探讨。

现在我们已经知道了示例模板是如何实现的,是时候深入探讨 Razor 语法了。

学习 Razor 语法

我喜欢 Razor 语法的其中一个原因是它很容易混合代码和 HTML 标签。由于代码靠近标记,我认为它更容易跟随和理解。语法非常流畅;Razor 解析器理解代码何时停止,标记何时开始,这意味着我们不需要过多地考虑它。它也不是一门新语言;相反,我们可以利用我们现有的 C# 和 HTML 知识来创建我们的组件。本节将包含大量理论,帮助我们理解 Razor 语法。

要从 HTML 转换到代码(C#),我们使用 @ 符号。我们可以用几种方法将代码添加到我们的文件中,我们将在接下来的章节中探讨这些方法:

  • Razor 代码块

  • 隐式 Razor 表达式

  • 显式 Razor 表达式

  • 表达式编码

  • 指令

Razor 代码块

我们已经看到了一些代码块。一个代码块看起来像这样:

@code {
    //your code here
} 

如果我们愿意,我们可以省略code关键字,如下所示:

@{
    //your code here
} 

在这些花括号内,我们可以像这样混合 HTML 和代码:

@{
    void RenderName(string name)
    {
        <p>Name: <strong>@name</strong></p>
    }
    RenderName("Steve Sanderson");
    RenderName("Daniel Roth");
} 

注意RenderName()方法如何从代码过渡到段落标签,然后再回到代码;这是一个隐式过渡。

如果我们想要输出没有 HTML 标签的文本,我们可以使用text标签而不是段落标签,如下面的示例所示:

<text>Name: <strong>@name</strong></text> 

这将渲染与之前代码相同的结果,但没有段落标签,并且text标签不会被渲染。

隐式 Razor 表达式

隐式 Razor 表达式是指我们在 HTML 标签内添加代码。

我们已经在Weather示例中看到了这个:

<td>@forecast.Summary</td> 

我们从一个<td>标签开始,然后使用@符号切换到 C#,再使用结束标签切换回 HTML。我们可以将await关键字与方法调用一起使用,但除此之外,隐式 Razor 表达式不能包含任何空格。

由于<>会被解释为 HTML,因此我们不能使用隐式表达式调用泛型方法。因此,为了解决这个问题,我们可以使用显式表达式。

显式 Razor 表达式

如果我们想在代码中使用空格,我们可以使用显式 Razor 表达式。用@符号后跟括号()编写代码。所以,它看起来像这样:@()

在这个示例中,我们从当前日期减去7天:

<td>@(DateTime.Now - TimeSpan.FromDays(7))</td> 

我们还可以使用显式 Razor 表达式来连接文本;例如,我们可以像这样连接文本和代码:

<td>Temp@(forecast.TemperatureC)</td> 

输出结果将是 <td>Temp42</td>

使用显式表达式,我们可以通过使用此语法轻松调用泛型方法:

<td>@(MyGenericMethod<string>())</td> 

Razor 引擎知道我们是否在使用代码。它还确保在输出到浏览器时将字符串编码为 HTML,这被称为表达式编码

表达式编码

如果我们将 HTML 作为字符串,它默认会被转义。以下代码为例:

@("<span>Hello World</span>") 

渲染的 HTML 将看起来像这样:

&lt;span&gt;Hello World&lt;/span&gt; 

要从字符串输出实际的 HTML(我们将在第五章创建高级 Blazor 组件中这样做),可以使用以下语法:

@((MarkupString)"<span>Hello World</span>") 

使用MarkupString,输出将是 HTML,显示 HTML 标签span。在某些情况下,一行代码不够;然后,我们可以使用代码块。

指令

有许多指令会改变组件的解析方式或启用功能。这些是跟随@符号的保留关键字。我们将介绍最常见和最有用的几个。

我发现将布局和代码放在同一个.razor文件中非常方便。

注意,我们可以使用代码隐藏来编写我们的代码,以便在代码和布局之间获得更多的分离。在本章的后面部分,我们将探讨如何使用代码隐藏而不是 Razor 语法来完成所有操作。现在,以下示例将展示如何使用 Razor 语法和代码隐藏来完成相同的指令。

添加属性

要向我们的页面添加属性,我们可以使用attribute指令:

@attribute [Authorize] 

如果我们使用的是代码后端文件,我们将使用以下语法:

[Authorize] public partial class SomeClass {} 

添加接口

要实现接口(在这种情况下为IDisposable),我们会使用以下代码:

@implements IDisposable 

然后,我们将在@code{}部分实现接口所需的方法。

在代码后端场景中执行相同操作时,我们会在类名后添加接口,如下例所示:

public partial class SomeClass : IDisposable {} 

继承

要继承另一个类,我们应该使用以下代码:

@inherits TypeNameOfClassToInheritFrom 

在代码后端场景中执行相同操作时,我们会在类名后添加我们想要继承的类:

public class SomeClass : TypeNameOfClassToInheritFrom {} 

泛型

我们可以将我们的组件定义为泛型组件。

泛型允许我们定义数据类型,因此组件可以与任何数据类型一起工作。

要将组件定义为泛型组件,我们添加@typeparam指令;然后,我们可以在组件的代码中使用该类型,如下所示:

@typeparam TItem
@code
{
      [Parameter]
      public List<TItem> Data { get; set; }
} 

泛型在创建可重用组件时非常强大;这将使我们的组件对不同数据类型可重用。我们将在第六章使用验证构建表单中回到泛型。

更改布局

如果我们想要为页面设置特定的布局(不是在Routes.razor文件中指定的默认布局),我们可以使用@layout指令:

@layout AnotherLayout 

这样,我们的组件将使用指定的布局(这仅适用于具有@page指令的组件)。

设置命名空间

默认情况下,组件的命名空间将是我们的项目默认命名空间加上文件夹结构。如果我们想让我们的组件位于特定的命名空间中,我们可以使用以下方法:

@namespace Another.NameSpace 

设置路由

我们已经提到了@page指令。如果我们想让我们的组件可以通过 URL 直接访问,我们可以使用@page指令:

@page "/theurl" 

URL 可以包含参数、子文件夹等等,我们将在本章后面回到这一点。

添加使用语句

要向我们的组件添加命名空间,我们可以使用@using指令:

@using System.IO 

如果我们在多个组件中使用相同的命名空间,那么我们可以将它们添加到_Imports.razor文件中。这样,它们将可用在我们创建的所有组件中。

如果你想进一步了解指令,你可以在这里找到更多信息:learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-8.0#directives

现在我们对 Razor 语法的运作方式有了更多的了解。不用担心;我们将有足够的时间来练习它。本节还有一个我没有涵盖的指令,那就是inject。我们首先需要了解依赖注入(DI)是什么以及它是如何工作的,这将在下一节中展示。

理解依赖注入

依赖注入(DI)是一种软件模式和实现控制反转(IoC)的技术。

IoC 是一个通用术语,意味着我们可以表明类需要一个类实例,而不是让我们的类实例化一个对象。我们可以说我们的类想要一个特定的类或一个特定的接口。

类的创建在其他地方,IoC 决定它将创建哪个类。

当涉及到 DI 时,如果对象(类实例)通过构造函数、参数或服务查找传递,它是一种 IoC 的形式。

如果你想要深入了解 .NET 中的 DI,这是一个很好的资源:learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection

在 Blazor 中,我们可以通过提供一种实例化对象的方法来配置 DI;这是一个我们应该使用的核心架构模式。我们已经看到一些关于它的引用,例如在 Program.cs 中:

builder.Services.AddScoped<IBlogApi, BlogApiJsonDirectAccess>(); 

在这里,我们说如果任何类需要 IBlogApi,应用程序应该实例化一个 BlogApiJsonDirectAccess 类型的对象。在这种情况下,我们使用了一个接口;相反,我们可以说:

builder.Services.AddScoped<BlogApiJsonDirectAccess>(); 

在这种情况下,当我们请求一个 BlogApiJsonDirectAccess 对象时,它将返回该类型的对象。如果我们所构建的东西只有一个实现,就没有必要为它创建一个接口。在上一章,第三章管理状态 – 第一部分。我们创建了一个 IBlogApi 接口,它返回一个 BlogApiJsonDirectAccess 的实例。当我们实现 WebAssembly 版本时,DI 将返回另一个类。

使用 DI 有许多优点。我们的依赖关系是松散耦合的,所以我们不需要在我们的类中实例化另一个类。相反,我们请求一个实例,这使得编写测试和根据平台更改实现变得更加容易。

任何外部依赖都将更加明显,因为我们必须将它们传递到类中。我们还可以在中心位置设置我们应该如何实例化对象。我们在 Program.cs 中配置 DI。

我们可以以不同的方式配置对象的创建,例如以下几种:

  • 单例

  • 作用域

  • 临时

单例

当我们使用单例时,对象将对所有网站用户都是相同的。对象只创建一次。

要配置单例服务,请使用以下方法:

services.AddSingleton<IWeatherForecastService, WeatherForecastService>(); 

当我们想要与我们的网站所有用户共享对象时,应该使用单例,但要注意,由于状态是共享的,如果对象存储了特定于单个用户或会话的数据,可能会导致问题,因为一旦这些数据被一个用户更改,更改将反映给所有可能同时使用应用程序的用户。这也可能导致数据无意中共享。

作用域

当我们使用 scoped 时,每个连接都会创建一个新的对象,由于 Blazor Server 需要连接来工作,所以只要用户有连接,它就会是同一个对象。WebAssembly 没有 scoped 的概念,因为没有连接,所以所有代码都在用户的网页浏览器内运行。如果我们使用 scoped,它将以与 Blazor WebAssembly 中的 singleton 相同的方式工作,因为我们只有一个用户,所有内容都在浏览器内运行。建议如果想要将服务范围限定在当前用户,仍然使用 scoped。这使得在 Blazor Server 和 Blazor WebAssembly 之间移动代码变得更加容易,并且对服务应该如何使用提供了更多上下文。

要配置一个 scoped 服务,请使用以下方法:

services.AddScoped<IWeatherForecastService, WeatherForecastService>(); 

如果我们有属于用户的数据,我们应该使用 scoped 对象来保持用户的状态。更多关于这一点的内容在 第十一章管理状态 – 第二部分

这里值得提一下,新的“按组件”模型会在任何组件当前以 InteractiveServer 模式运行时创建一个 SignalR 连接。如果我们导航到一个没有 InteractiveServer 组件的新页面,连接最终会被断开。这意味着状态也会被移除。因此,当使用“按组件”模型时,我们需要确保不要在 scoped 变量中保存任何重要信息,除非我们以其他方式持久化它。

Transient

当我们使用 transient 时,每次请求都会创建一个新的对象。

要配置一个 transient 服务,请使用以下方法:

services.AddTransient<IWeatherForecastService, WeatherForecastService>(); 

如果我们不需要保持任何状态,并且不介意每次请求时对象都被创建,我们可以使用 transient。

现在我们知道了如何配置服务,我们需要开始使用服务,通过注入它。

注入服务

有三种方式可以注入服务。我们可以在 Razor 文件中使用 @inject 指令:

@inject WeatherForecastService ForecastService 

这将确保我们可以在组件中访问 WeatherForecastService

第二种方式是在使用代码隐藏时通过添加 Inject 属性来创建一个属性:

[Inject]
public WeatherForecastService ForecastService { get; set; } 

第三种方式是在我们想要将服务注入到另一个服务时——这时,我们需要通过构造函数注入服务:

public class MyService
{
    public MyService(WeatherForecastService
      weatherForecastService)
    {
    }
} 

现在我们知道了依赖注入的工作原理以及为什么我们应该使用它。在 .NET 7 中,使用 scoped 服务意味着只要连接(或电路)是活跃的,数据就可以访问。但到了 .NET 8,它根据渲染模式略有变化。让我们接下来看看这一点。

改变渲染模式

当涉及到 .NET 8 时,最大的变化是能够在同一应用程序中更改渲染模式。在 .NET 7 中,我们必须选择一个或另一个,但使用 .NET 8,我们可以根据需要更改它。也许如果某个页面没有交互性,我们可以使用新的 服务器端渲染SSR)。这与 WebForms 或 MVC 非常相似。页面在服务器上渲染。没有额外的交互性将工作。我们可以在每个组件上设置渲染模式,或者在我们使用组件时进行设置。当我们创建项目时,我们选择我们想要的交互式渲染模式。

让我们来看看不同的选项:

  • – 没有交互性,只有静态渲染文件,没有 SignalR 和 WebAssembly。使用此选项,我们可以使用静态 SSR 和流式服务器端渲染。

  • 服务器 – 这将使我们能够通过 Blazor 服务器使用交互性,而不是 WebAssembly。

  • WebAssembly – 这将使我们能够通过 Blazor WebAssembly 使用交互性,而不是 Blazor 服务器。

  • 自动(服务器和 WebAssembly)- 允许我们同时使用服务器和 WebAssembly。

我们还将 交互位置 设置为 每页/组件,这意味着网站的默认行为是静态的,并且我们需要在每个组件上指定是否要使用交互性。我们也可以将其设置为 全局,这将像这样在 Routes 组件上设置交互性:

<Routes @rendermode="@InteractiveAuto" /> 

要为每个组件更改渲染模式,我们可以使用上面的语法或使用我们在 Counter 组件中看到的属性:

@rendermode InteractiveAuto 

默认情况下,所有组件都使用服务器预渲染。这意味着组件首先在服务器上渲染,然后推送到网页浏览器。SignalR 或 WebAssembly 启动,组件再次渲染,例如,对数据库进行额外的调用。我个人很少使用服务器预渲染。我喜欢页面首先发送服务器准备好的内容,然后,当数据库调用完成后,发送剩余的内容。我们也可以通过这样做来禁用预渲染:

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" /> 

在本书的学习过程中,我们将有足够的时间学习更多关于渲染模式的知识。在 .NET 7 中,我们有更多的模板,但通过使用这些设置的组合,我们可以创建相同的场景。

.NET 7 模板 .NET 8 模板 交互式渲染模式 交互位置
Blazor 服务器应用程序 Blazor Web 应用程序 服务器 全局
Blazor WebAssembly 应用程序 Blazor WebAssembly 独立应用程序
Blazor WebAssembly (ASP.NET Core 托管) Blazor Web 应用程序 WebAssembly 全局

如果你刚接触 Blazor,这个表格没有意义,但如果你在 .NET 7 中使用过 Blazor,并想使用你在 .NET 7 中使用的项目模板,这是如何操作的。在本章中,我们提到了几次代码后置。在下一节中,我们将探讨如何使用代码后置与 Razor 文件一起使用,并完全跳过 Razor 文件。

确定代码放置的位置

我们已经看到了在 Razor 文件中直接编写代码的示例。除非代码变得过长或过于复杂,否则我更喜欢这样做。我总是倾向于可读性。

我们有四种编写组件的方法:

  • 在 Razor 文件中

  • 在部分类中

  • 继承一个类

  • 只有代码

让我们更详细地查看这个列表上的每一项。

在 Razor 文件中

如果我们正在编写一个不太复杂的文件,那么在编写组件时不需要切换文件会很好。正如我们在本章中已经讨论过的,我们可以使用@code指令直接将代码添加到我们的 Razor 文件中。

如果我们想将代码移动到后置代码文件中,那么我们只需要更改指令。对于其余的代码,我们只需将其移动到后置代码类中。当我刚开始使用 Blazor 时,由于来自 MVC 世界,其中代码和标记的分离是使用 MVC 方式的一个重要部分,所以将代码和标记写在同一文件中感觉有些奇怪。但我建议你在开发你的 Web 应用时尝试一下。

在工作中,我们开始使用后置代码,但后来改为在.razor文件中编写代码,并且从那时起就没有回头了。

然而,许多开发者更喜欢后置代码,将代码与布局分离。为此,我们可以使用部分类。

在部分类中

我们可以创建一个与 Razor 文件同名的部分类,并添加.cs扩展名。

如果你已经下载了源代码(或者你可以在 GitHub 上查看代码),你可以在Examples文件夹中查看WeatherCodeBehind.razor.cs。我已经将所有代码移动到后置代码文件中;编译此代码的结果将与我们保留代码在 Razor 文件中相同。这只是一种个人偏好的问题。

后置代码看起来像这样:

namespace BlazorWebApp.Components.Pages;
[StreamRendering(true)]
public partial class WeatherWithCodeBehind
{
    private WeatherForecast[]? forecasts;
    protected override async Task OnInitializedAsync()
    {
        // Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToArray();
    }
    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
} 

由于我们正在使用部分类,因此不需要将 razor 与后置代码连接起来。如果它们具有相同的名称,它们将正常工作。我们可以混合放置代码的位置;在这种情况下,我们在后置代码中有StreamingRendering属性,如果我们想的话,我们也可以将其保留在.razor文件中。如果你更喜欢使用后置代码,这就是你想要的方式。

这不是使用后置代码文件的唯一方法;我们还可以从后置代码文件中继承。

继承一个类

我们也可以创建一个完全不同的类(常见的模式是将它与 Razor 文件同名,并在末尾添加Model),并在我们的 Razor 文件中继承它。为了使其工作,我们需要从ComponentBase继承。在部分类的情况下,类已经从ComponentBase继承,因为 Razor 文件会这样做。

字段必须是受保护的或公共的(不是私有的),以便页面可以访问这些字段。如果我们不需要从我们的基类继承,我建议使用部分类。

这是后置代码类声明的代码片段:

public class WeatherWithInheritsModel:ComponentBase 

我们需要从ComponentBase或从继承自ComponentBase的类继承。

在 Razor 文件中,我们将使用@inherits指令:

@inherits WeatherWithInheritsModel 

现在 Razor 文件将继承自我们的代码隐藏类(这是创建代码隐藏类的第一种可用方法)。

部分和继承选项都是将代码移动到代码隐藏文件中的简单方法。继承模型是第一种可用的方法,但如我所述,如果你更喜欢代码隐藏,请使用部分类。但另一个选项是跳过 Razor 文件,完全使用代码。

只有代码

Visual Studio 将使用源生成器将 Razor 代码转换为 C#。我们将在第十七章“检查源生成器”中深入了解源生成器。Razor 文件将在编译时生成代码。如果我们想的话,可以跳过 Razor 步骤,完全用代码编写布局。

此文件(CounterWithoutRazor.cs)可在 GitHub 上找到。

反例看起来可能如下所示:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
namespace BlazorWebApp.Component.Pages;
[Route("/CounterWithoutRazor")]
public class CounterWithoutRazor : ComponentBase
{
    protected override void BuildRenderTree
      (RenderTreeBuilder builder)
    {
        builder.AddMarkupContent(0, "<h1>Counter</h1>\r\n\r\n");
        builder.OpenElement(1, "p");
        builder.AddContent(2, "Current count: ");
        builder.AddContent(3, currentCount);
        builder.CloseElement();
        builder.AddMarkupContent(4, "\r\n\r\n");
        builder.OpenElement(5, "button");
        builder.AddAttribute(6, "class", "btn btn-primary");
        builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, IncrementCount));
        builder.AddContent(8, "Click me");
        builder.CloseElement();
    }
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
} 

Razor 文件首先会被转换成与之前代码大致相同的东西,然后代码被编译。它逐个添加元素,最终将渲染 HTML。

代码中的数字是 Blazor 跟踪渲染树中每个元素的方式。有些人更喜欢像上一个代码块那样编写代码,而不是使用 Razor 语法;社区中甚至有简化手动编写BuildRenderTree()函数过程的努力。

微软的一些内置组件就是这样构建的。

我建议永远不要手动编写这个方法,但我将它保留在书中,因为它展示了 Razor 文件是如何被编译的。现在我们已经知道了如何使用代码隐藏,让我们来看看 Blazor 的生命周期事件以及它们何时被调用。

生命周期事件

我们可以使用一些生命周期事件来运行我们的代码。在本节中,我们将逐一介绍它们,并了解何时应该使用它们。大多数生命周期事件都有两个版本——同步和异步。

OnInitialized 和 OnInitializedAsync

组件第一次加载时,会调用OnInitialized(),然后是OnInitializedAsync()。这是一个加载任何数据的绝佳方法,因为此时 UI 尚未渲染。如果我们正在进行长时间运行的任务(例如从数据库获取数据),我们应该将那段代码放在OnInitializedAsync()方法中。

这些方法只会运行一次。如果你想在参数更改时更新 UI,请参阅OnParametersSet()OnParametersSetAsync()

OnParametersSet 和 OnParametersSetAsync

当组件初始化时(在OnInitialized()OnInitializedAsync()之后)以及每次我们更改参数值时,都会调用OnParametersSet()OnParametersSetAsync()

例如,如果我们使用OnInitialized()方法加载数据,但使用了参数,那么如果参数更改,数据不会重新加载,因为OnInitialized()只会运行一次。我们需要在OnParametersSet()OnParametersSetAsync()中触发数据的重新加载,或者将加载移动到那个方法。

OnAfterRender 和 OnAfterRenderAsync

组件渲染完成后,会调用OnAfterRender()OnAfterRenderAsync()方法。当这些方法被调用时,所有元素都已渲染,因此如果我们想要/需要调用任何 JavaScript 代码,我们必须从这些方法中调用(如果我们尝试从任何其他生命周期事件方法中执行 JavaScript 互操作,将会得到错误)。这是预渲染的限制。当组件预渲染时,没有连接到网络浏览器,我们将无法运行任何 JavaScript。然而,如果我们禁用预渲染,我们也可以在其他生命周期方法中运行 JavaScript。我们还可以访问firstRender参数,因此我们只能在第一次渲染时运行我们的代码。

ShouldRender

当我们的组件重新渲染时,会调用ShouldRender();如果它返回false,则组件将不会再次渲染。即使此方法返回false,组件也总会渲染一次。

ShouldRender()没有异步选项。

现在我们知道了不同的生命周期事件何时发生以及它们的顺序。组件也可以有参数,这样我们就可以以不同的数据重用它们。

参数

参数使得将值发送到组件成为可能。要向组件添加参数,我们使用public属性上的[Parameter]属性:

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

如果我们使用代码后文件,此语法是相同的。我们可以通过在路由中指定它来使用@page指令将参数添加到路由中:

@page "/parameterdemo/{MyParameter}" 

在这种情况下,我们必须指定一个与花括号内名称相同的参数。要在@page指令中设置参数,我们前往/parameterdemo/THEVALUE

有时候我们想要指定另一种类型而不是字符串(字符串是默认类型)。我们可以在参数名称后添加数据类型,如下所示:

@page "/parameterdemo/{MyParameter:int}" 

只有当数据类型是整数时,这才会匹配路由。我们还可以使用级联参数传递参数。如果我们想要处理多个路由,我们可以在组件中拥有多个页面指令。

级联参数

如果我们要将一个值传递给多个组件,我们可以使用级联参数。

而不是使用[Parameter],我们可以使用[CascadingParameter],如下所示:

[CascadingParameter]
public int MyParameter { get; set; } 

要将值传递给组件,我们用CascadingValue组件将其包围,如下所示:

<CascadingValue Value="MyProperty">
<ComponentWithCascadingParameter/>
</CascadingValue> 
@code {
    public string MyProperty { get; set; } = "Test Value";
} 

CascadingValue是我们传递给组件的值,而CascadingParameter是接收该值的属性。

如我们所见,我们没有向ComponentWithCascadingParameter组件传递任何参数值;级联值将与相同数据类型的参数匹配。如果我们有多个相同类型的参数,我们可以在具有级联参数的组件中指定参数的名称,如下所示:

[CascadingParameter(Name = "MyCascadingParameter")] 

我们也可以为传递CascadingValue的组件这样做,如下所示:

<CascadingValue Value="MyProperty" Name="MyCascadingParameter">
<ComponentWithCascadingParameter/>
</CascadingValue> 

如果我们知道值不会改变,我们可以通过使用IsFixed属性来指定这一点:

<CascadingValue Value="MyProperty" Name="MyCascadingParameter" IsFixed="True">
<ComponentWithCascadingParameter/>
</CascadingValue> 

这样,Blazor 就不会寻找更改,如果我们知道值不会改变,这会更高效。级联值/参数不能向上更新,只能向下更新。这意味着要更新级联值,我们需要以另一种方式实现它;在组件内部更新它不会改变任何在层次结构中更高的组件。

第五章创建高级 Blazor 组件 中,我们将探讨事件,这是解决更新级联值问题的一种方式。

呼吁!这一章信息量很大,但现在我们知道了 Blazor 组件的基础知识。现在,是时候构建一个组件了!

编写我们的第一个组件

我们将要构建的第一个组件将显示网站上所有的博客文章。公平地说,我们还没有写过任何博客文章,但我们将暂时解决这个问题,以便我们可以开始做一些有趣的事情。

第三章管理状态 – 第一部分 中,我们创建了一个 JSON 存储库和一个 API(或接口);现在是时候使用它们了。

我们将在 BlazorWebApp 项目和 BlazorWebApp.Client 项目之间共享代码。我们甚至将根据它们是否作为 WebAssembly 运行来更改它们的实现方式。

关于共享有一个专门的章节(第九章共享代码和资源),但现在我们就开始吧。

创建组件库

我们需要做的第一件事是创建一个新的项目,然后将我们的组件添加到该项目中。我们本来可以直接将组件添加到 BlazorWebAppBlazorWebApp.Client 项目中,但这样做演示了我们可以如何构建可重用组件并在以后将它们作为包分发。

要创建我们的第一个组件,请按照以下说明操作:

  1. 右键单击 MyBlog 解决方案并选择 添加 | 新建项目

  2. 找到模板 Razor 类库 并点击 下一步

  3. 将项目命名为 SharedComponents 并点击 下一步

  4. 选择 .NET 8.0 并点击 创建

  5. 现在我们有一个名为 SharedComponents 的项目,我们可以添加所有我们想要共享的组件。删除默认创建的 Component1.razorExampleJsInterop.cs

  6. SharedComponents 项目中,添加对 Data.Models 的项目引用,并添加对 Microsoft.AspNetCore.Components.Web 的 Nuget 包引用。

我们有一个新的项目。这是我们分享组件的地方。目前,我们还没有任何可以分享的组件,但这是我们接下来要做的。

使用我们的组件库

我们有一个很好的库,但为了我们的项目在导航到路由时触发,我们需要向路由器添加额外的程序集。

要做到这一点,我们需要遵循几个步骤:

  1. 我们已经在 BlazorWebApp 项目中有一个名为 Home 的组件,所以让我们删除它。在 Components/Pages 文件夹中,删除 Home.razor 文件。

  2. 我们需要一个可以导航到的组件。在SharedComponents项目中,在Pages文件夹中,我们需要创建一个新的组件。您可以在解决方案资源管理器中选择文件夹或项目节点,然后按Shift + F2,输入Home.razor,然后按Enter。这是创建新组件最快的方法。

  3. BlazorWebApp项目中,将项目引用添加到SharedComponents项目中。

  4. BlazorWebApp.Client项目中,将项目引用添加到SharedComponents项目中。

  5. 现在,我们可以在 WebAssembly 项目(BlazorWebApp.Client)和BlazorWebProject中访问共享组件。这意味着我们可以运行我们放入共享项目(SharedComponents)中的任何组件,作为InteractiveWebAssemblyInteractiveServer

  6. 打开Router.razor。它看起来像这样:

    <Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }"> 
    

    路由器定义了组件的查找位置。AppAssembly是它首先查找的地方。我们还可以添加额外的程序集,正如我们所看到的,我们已经有了一个额外的程序集。我们正在引用BlazorWebbApp.Client项目,以便我们可以预渲染和服务器渲染(SignalR)计数器组件。但现在我们想要添加一个额外的程序集。将路由器更改为以下内容:

    <Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly,typeof(SharedComponents.Pages.Home).Assembly }"> 
    

    我们将新的程序集添加到额外的程序集中。现在,路由器将同时在SharedComponents程序集中查找组件。

  7. 我们还必须在Program.cs中添加一行。在BlazorWebApp项目中,打开Program.cs。在那里,您将找到以下行:

     app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode()
        .AddInteractiveWebAssemblyRenderMode()
        .AddAdditionalAssemblies(typeof(Counter).Assembly); 
    

    我们也需要在那里添加新的程序集,如下所示:

    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode()
        .AddInteractiveWebAssemblyRenderMode()
        .AddAdditionalAssemblies(typeof(Counter).Assembly)
    .AddAdditionalAssemblies(typeof(SharedComponents.Pages.Home).Assembly); 
    

我们还需要在Program.cs中添加额外的程序集,以便服务器端渲染能够工作。太好了!我们所有的组件都在一个单独的库中,并且我们在BlazorWebAppBlazorWebApp.Client项目之间共享组件。

创建我们自己的组件

现在是时候开始添加我们自己的组件了!

好吧,这并不完全正确,因为我们将继续在Home.razor上工作。让我们先创建一个列出我们博客文章的组件:

  1. SharedComponents项目中,打开Home.razor

  2. 用以下代码替换该文件的内容:

    @page "/"
    @using Data.Models.Interfaces
    @using Data.Models
    @inject IBlogApi _api
    @code{
    } 
    

    如果我们从顶部开始,我们可以看到一个页面指令。它将确保当路由是“/”时显示组件。然后,我们有三个@using指令,引入了命名空间,这样我们就可以在 Razor 文件中使用它们。

    然后我们注入我们的 API(使用 DI),并将其命名为_api

  3. 添加一个变量来保存我们所有的文章。在code部分,添加以下内容:

    protected List<BlogPost> posts = new(); 
    
  4. 现在,我们需要加载数据。

    要加载文章,请在code部分添加以下内容:

    protected override async Task OnInitializedAsync()
    {
        posts = await _api.GetBlogPostsAsync(10, 0);
        await base.OnInitializedAsync();
    } 
    

    现在,当页面加载时,文章也会被加载:10篇文章和页面0(第一页)。

  5. @inject行下面,添加以下代码:

    <ul>
        @foreach (var p in posts)
        {
            <li>@p.Title</li>
        }
    </ul> 
    

我们添加一个无序列表UL);在其中,我们遍历博客文章并显示标题。

现在,我们可以通过按Ctrl + F5调试 | 不调试启动)来运行应用程序。确保您已将BlazorWebApp选为启动项目。

由于我们没有任何博客文章,这会带我们到一个空页面。幸运的是,在存储库中有一个名为 ExampleData 的文件夹。如果你下载它,将这些文件放入我们在 第三章管理状态 – 第一部分 中创建的 Data 文件夹,然后重新加载网页,你应该能看到几篇文章。

干得好,我们创建了我们的第一个组件!

有几点值得注意。SharedComponents 项目对 JSON 存储库实现一无所知,只知道 IBlogApi 接口。

Home 组件请求 IBlogApi 的一个实例,而 BlazorWebApp 项目知道它应该返回 BlogApiJsonDirectAccess 的一个实例。这是我非常喜欢 Blazor 的一个原因;我们可以创建只消费接口而不知道实现细节的组件。

我们将在 第七章创建 API 中实现 WebAssembly 的 Web API 时回到这一点。

摘要

在本章中,我们学习了大量的 Razor 语法——这是我们将在整本书中使用的。我们学习了依赖注入(DI)、指令和参数,当然,我们还创建了我们的第一个组件。这些知识将帮助我们理解如何创建和重用组件。

在下一章中,我们将探讨更多高级组件场景。

第五章:创建高级 Blazor 组件

在上一章中,我们学习了创建组件的所有基础知识。这一章将教会我们如何将我们的组件提升到下一个层次。

本章将重点介绍一些将使我们的组件可重用的功能,这将使我们节省时间,并让我们了解如何使用他人制作的可重用组件。

我们还将查看一些内置组件,这些组件在构建 Blazor 应用程序时将帮助你添加额外的功能(与使用 HTML 标签相比)。

在本章中,我们将涵盖以下主题:

  • 探索绑定

  • 动作和 EventCallback

  • 使用 RenderFragment

  • 探索新内置组件

技术要求

在本章中,我们将开始构建我们的组件。为此,你需要我们在 第四章理解基本 Blazor 组件 中开发的代码。如果你遵循了前几章的说明,那么你就可以开始了。如果没有,那么请确保你克隆/下载了存储库。本章的起点可以在 chapter04 文件夹中找到,而完成的 chapterchapter05 中。

你可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter05 找到本章结果的源代码。

探索绑定

当构建应用程序时,数据很重要,我们可以使用绑定来显示或更改数据。通过使用绑定,你可以连接组件内的变量(以便自动更新)或通过设置组件属性。最神奇的是,通过使用绑定,Blazor 能够理解何时应该更新 UI 和变量(如果 UI 中的数据发生变化)。

在 Blazor 中,我们可以以两种不同的方式将值绑定到组件,如下所示:

  • 单向绑定

  • 双向绑定

通过使用绑定,我们可以在组件之间发送信息,并确保我们可以在需要时更新一个值。

单向绑定

我们已经在 第四章创建基本 Blazor 组件 中讨论了单向绑定。让我们再次查看该组件,并在本节中继续构建它。

在本节中,我们将结合参数和绑定。

Counter.razor 示例如下所示:

@page "/counter" @rendermode InteractiveAuto
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
} 

组件将显示当前计数和一个按钮,该按钮将增加当前计数。这是单向绑定。尽管按钮可以更改 currentCount 的值,但它只单向流向屏幕。

由于这部分是为了演示功能性和理论,而不是我们正在构建的完整项目的一部分,因此你不需要编写或运行此代码。这些组件的源代码可在 GitHub 上找到。

我们可以向 Counter 组件添加一个参数。代码将如下所示:

@page "/counterwithparameter"
@rendermode InteractiveAuto
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    [Parameter]
    public int IncrementAmount { get; set; } = 1;
    [Parameter]
    public int CurrentCount { get; set; } = 0;
    private void IncrementCount()
    {
        CurrentCount+=IncrementAmount;
    }
} 

代码示例有两个参数,一个用于CurrentCount,一个用于IncrementAmount。通过向组件添加参数,我们可以改变它们的行为。当然,这个示例有点愚蠢。你可能不会用到这种在按下按钮时只进行计数的组件。但它很好地说明了这个想法。

现在,我们可以将组件添加到另一个组件中。这就是我们如何创建可重用组件并通过更改参数的值来改变其行为的方法。

我们可以这样改变它的行为:

@page "/parentcounter"
@rendermode InteractiveAuto
<CounterWithParameter IncrementAmount="@incrementamount" CurrentCount="@
currentcount"/>
The current count is: @currentcount
@code {
    int incrementamount = 10;
    int currentcount = 0;
} 

在这个示例中,我们有两个变量,incrementamountcurrentcount,我们将它们传递到我们的CounterWithParameter组件中。

如果我们运行这个程序,我们会看到一个Counter组件,它会以10的增量进行计数。然而,currentcount变量将不会更新,因为它只是一个单向绑定(一个方向)。

为了帮助我们实现这一点,我们可以实现双向绑定,这样我们的父组件就会通知任何变化。

双向绑定

双向绑定在两个方向上绑定值,我们的Counter组件将能够通知父组件任何变化。在下一章,第六章使用验证构建表单中,我们将更详细地讨论双向绑定。

要使我们的CounterWithParameter组件进行双向绑定,我们需要添加一个EventCallback。名称必须由参数的名称后跟Changed组成。这样,Blazor 就会在值发生变化时更新值。在我们的例子中,我们需要将其命名为CurrentCountChanged。代码将如下所示:

[Parameter]
public EventCallback<int> CurrentCountChanged { get; set; }
private async Task IncrementCount()
{
    CurrentCount += IncrementAmount;
    await CurrentCountChanged.InvokeAsync(CurrentCount);
} 

通过仅使用该命名约定,Blazor 就知道CurrentCountChanged是当CurrentCount发生变化时将被触发的事件。

EventCallback不能为null,因此没有必要进行空检查(更多内容将在下一节中介绍)。

我们还需要更改我们监听变化的方式:

<CounterWithParameterAndEvent IncrementAmount="@incrementamount" @bind-CurrentCount="currentcount"/> 

我们需要在CurrentCount绑定前添加@bind-前缀。您也可以使用以下语法来设置事件的名称:

<CounterWithParameterAndEvent IncrementAmount="@incrementamount" @bind-CurrentCount="currentcount" @bind-CurrentCount:event="CurrentCountChanged"/> 

通过使用:event,我们可以告诉 Blazor 我们想要使用哪个事件;在这种情况下,是CurrentCountChanged事件。

在下一章,第六章使用验证构建表单中,我们将继续探讨与输入/表单组件的绑定。

我们当然也可以使用EventCallback创建事件。

动作和 EventCallback

为了通信变化,我们可以使用EventCallback,如双向绑定部分所示。EventCallback<T>与我们在.NET 中可能习惯的有所不同。EventCallback<T>是一个专门为 Blazor 设计的类,以便将事件回调暴露为组件的参数。

在.NET 中,通常可以给事件添加多个监听器(多播),但使用EventCallback<T>,你只能添加一个监听器(单播)。

值得注意的是,你可以像在.NET 中一样使用事件。然而,你可能想使用EventCallback<T>,因为与传统的.NET 事件相比,使用EventCallback有很多优点,如下所示:

  • .NET 事件使用类,而EventCallback使用结构体。这意味着在 Blazor 中,我们不需要在调用EventCallback之前执行空检查,因为结构体不能为空。

  • EventCallback是异步的,并且可以被等待。当EventCallback被调用时,Blazor 将自动在消费组件上执行StateHasChanged以确保组件更新(如果需要更新)。

因此,如果你需要多个监听器,你可以使用Action<T>。否则,最好使用EventCallback<T>

一些事件类型具有我们可以访问的事件参数。它们是可选的,所以在大多数情况下你不需要添加它们。

你可以通过在方法中指定它们来添加它们,或者你可以使用像这样的 lambda 表达式:

<button @onclick="@((e)=>message=$"x:{e.ClientX} y:{e.ClientY}")">Click me</button> 

当点击button时,它将设置一个名为message的变量,该变量包含鼠标坐标的字符串。Lambda 表达式有一个参数e,其类型为MouseArgs。然而,你不必指定类型,编译器会理解参数的类型。

现在我们已经添加了操作并使用EventCallback来通信更改,我们将在下一节中看到如何执行RenderFragment

使用 RenderFragment

为了使我们的组件更加可重用,我们可以向它们提供一段 Razor 语法。在 Blazor 中,你可以指定RenderFragment,这是一个你可以执行并显示的 Razor 语法片段。

有两种类型的渲染元素,RenderFragmentRenderFragment<T>RenderFragment是一个没有输入参数的简单 Razor 片段,而RenderFragment<T>有一个输入参数,你可以在 Razor 片段代码中使用context关键字来使用它。我们现在不会深入讨论如何使用它,但在本章的后面,我们将讨论一个使用RenderFragment<T>的组件(Virtualize),在下一章(第六章),我们将实现一个使用RenderFragment<T>的组件。

我们可以将RenderFragment作为组件标签内的默认内容,同时也可以给它一个默认值。我们将在下一节中探讨这一点,并使用这些特性构建一个组件。

当在列表中使用组件时,它可能会增加一些开销。它需要为每个组件执行整个生命周期。这就是渲染片段发挥作用的地方。我们可以创建一个返回渲染片段的方法,而不需要组件的开销。以下是一个例子:

@page "/RenderFragmentTest"
@for (int i = 0; i < 10; i++)
{
    @Render(i)
}
@code
{
    private RenderFragment Render(int number) 
    {
         return @<p>This is a render fragment @number</p>;
    }
} 

我们有一个返回渲染片段的方法的组件。

如果我们需要在其他组件中使用它,这个方法可以是静态的。当进行这样的循环时,它将提高性能,并且与有组件引用相比,对内存消耗的影响更低。

网格组件

如果你想深入了解渲染片段,请查看 Blazm.Components,它有一个使用 RenderFragment<T> 的网格组件。

你可以在 GitHub 上找到它:github.com/EngstromJimmy/Blazm.Components

子内容

通过将渲染片段命名为 ChildContent,Blazor 将自动使用组件标签之间的任何内容。然而,这仅在您使用单个渲染片段时才有效;如果您使用多个,您还必须指定 ChildComponent 标签。我们将在下一节中构建一个使用 childcontent 渲染片段的组件。

默认值

我们可以提供带有默认值的 RenderFragment 或通过使用 @ 符号在代码中设置它:

@<b>This is a default value</b>; 

构建一个警报组件

为了更好地理解如何使用渲染片段,让我们构建一个将使用渲染片段的警报组件。内置模板使用 Bootstrap,因此我们将为此组件做同样的事情。Bootstrap 有许多组件很容易导入到 Blazor 中。当在大型项目上与多个开发者合作时,构建组件是确保团队中每个人以相同方式编写代码的简单方法。

让我们基于 Bootstrap 构建一个简单的警报组件:

  1. 通过在 SharedComponents 项目 上右键单击并选择 添加 | 新建文件夹 来创建一个文件夹,并将其命名为 ReusableComponents

  2. 创建一个新的 Razor 组件,并将其命名为 Alert.razor

  3. Alert.razor 文件中,将内容替换为以下代码:

    <div class="alert alert-primary" role="alert">
        A simple primary alert—check it out!
    </div> 
    

    代码是从 Bootstrap 的网页 getbootstrap.com 复制的,它显示了一个看起来像这样的警报:

    图 5.1 – Bootstrap 警报组件的默认外观

    图 5.1:Bootstrap 警报组件的默认外观

    我们可以通过两种方式自定义这个 alert 组件。我们可以添加一个 string 参数用于消息。

    然而,由于这是一个关于渲染片段的部分,我们将探索第二种选项——是的,你已经猜到了,渲染片段

  4. 添加一个具有 RenderFragment 属性的代码部分,并将其命名为 ChildContent,并用新属性替换警报文本:

    <div class="alert alert-primary" role="alert">
        @ChildContent
    </div>
    @code{
        [Parameter]
        public RenderFragment ChildContent { get; set; } =@<b>This is a default value</b>;
    } 
    

    现在我们有一个 RenderFragment 并设置了一个默认值,在 div 标签之间显示片段。我们还想添加一个 enum 来表示不同的样式化警报框的方式。

  5. 代码 部分,添加一个包含不同样式的 enum

    public enum AlertStyle
    {
        Primary,
        Secondary,
        Success,
        Danger,
        Warning,
        Info,
        Light,
        Dark
    } 
    
  6. enum 风格添加一个参数/属性:

    [Parameter]
    public AlertStyle Style { get; set; } 
    
  7. 最后一步是更新 divclass 属性。将 class 属性更改为以下样子:

    <div class="@($"alert alert-{Style.ToString().ToLower()}")" role="alert"> 
    
  8. SharedComponents 项目中,在 Pages 文件夹中,创建一个新的 razor 组件,并将其命名为 AlertTest.razor

    将代码替换为以下片段:

    @page "/alerttest"
    @using SharedComponents.ReusableComponents
    <Alert Style="Alert.AlertStyle.Danger">
        This is a test
    </Alert>
    <Alert Style="Alert.AlertStyle.Success">
    <ChildContent>
            This is another test
        </ChildContent>
    </Alert>
    <Alert Style="Alert.AlertStyle.Success"/> 
    

    页面显示了三个警报组件:

    第一个具有 Danger 风格,我们没有指定为 This is a test 文本设置哪个属性,但按照惯例,它将使用名为 ChildContent 的属性。

    在第二个中,我们指定了 ChildContent 属性。如果你在组件中使用更多的渲染片段,你必须像这样设置它们,使用全名。

    在最后一个中,我们没有指定任何内容,这将给属性赋予我们在组件中指定的默认渲染片段。

  9. 运行 BlazorServer 项目并导航到 /AlertTest 以查看测试页面:

计算机的截图,描述自动生成

图 5.2:测试页面的截图

我们已经完成了我们的第一个可重用组件!

创建可重用组件是我更喜欢用来制作我的 Blazor 网站的方式,因为我不必写两次相同的代码。如果你在一个更大的团队中工作,这一点会更加明显。这使得所有开发者都能产生相同的代码和最终结果,并且通过这种方式,他们可以获得更高的代码质量并减少测试需求。

当我们升级到最新的 Bootstrap 版本时,一些 CSS 类已被弃用并由其他类替换。幸运的是,我们通过创建可重用组件来遵循这种方法,所以我们只需要更改几个地方。有几个地方我们仍然有一些旧的代码库(未使用组件),这变得非常明显,创建组件是值得努力的。

Blazor 有很多内置组件。在下一节中,我们将深入了解它们是什么以及如何使用它们。

当涉及到可重用组件时,我们在那上面投入了相当多的时间。如果你发现自己正在写两次相同的代码,你可能想把它添加到一个组件中。在我以前的工作中,我们开始使用 Radzen,这是一个开源的组件库(以及其他一些东西)。在我目前的工作中,我们使用 MudBlazor。我们在流中使用 Progress Telerik。使用第三方组件可以加快开发速度,但通常,这些组件是为许多不同的用户构建的。它们可以做很多事情。这意味着我们团队上的每个开发者现在都可以访问所有这些力量。权力越大,责任越大。

在我的一次演讲中,我引用了这句话,配图是蝙蝠侠,文字写着“超人”。我没有得到任何反应。我从未用这样的笑话失败过。但开个玩笑,这意味着所有开发者都需要记住如何使用组件。否则,UI 可能会因为使用组件的开发者不同而看起来不同。我投入了很多时间来设计可重用组件,帮助团队提高生产力。隐藏我们不使用的参数,为组件提供合理的默认值。所以,即使你使用第三方组件,也尽量弄清楚你在使用什么,也许可以在第三方组件之上创建一个抽象层。如果你不知道,这句话来自蜘蛛侠,或者更准确地说,是本叔叔。但它确实让我想起了我最喜欢的双关语。你知道为什么蜘蛛侠总是有如此机智的回应吗?因为能力越大,责任越大。我要退场了。

探索新的内置组件

当 Blazor 首次推出时,有几件事情很难做,在某些情况下,我们需要涉及 JavaScript 来解决挑战。在本节中,我们将查看我们在 .NET 5 到 .NET 8 中获得的一些新组件。

我们将查看以下新组件或函数:

  • 设置 UI 的焦点

  • 影响 HTML 头部

  • 组件虚拟化

  • 错误边界

  • 部分

设置 UI 的焦点

我的第一篇 Blazor 博客文章是关于如何在 UI 元素上设置焦点,但现在这已经内置到框架中了。之前的解决方案涉及对 UI 元素的 JavaScript 调用来更改焦点。

通过使用 ElementReference,你现在可以设置元素的焦点。

让我们构建一个组件来测试这个新特性的行为:

  1. SharedComponents 项目中,在 Pages 文件夹中,添加一个新的 Razor 组件,并将其命名为 SetFocus.razor

  2. 打开 SetFocus.razor 并添加一个 page 指令:

    @page "/setfocus" @rendermode InteractiveAuto 
    
  3. 添加一个元素引用:

    @code {
        ElementReference textInput;
    } 
    

    ElementReference 就像它的名字一样,是对一个元素的引用。在这种情况下,它是一个输入文本框。

    _Imports 文件中,添加以下行:

    @using static Microsoft.AspNetCore.Components.Web.RenderMode 
    
  4. 添加文本框和按钮:

    <input @ref="textInput" />
    <button @onclick="() => textInput.FocusAsync()">Set focus</button> 
    

    使用 @ref,我们指定了对任何类型组件或标签的引用,我们可以使用它来访问输入框。button onclick 方法将执行 FocusAsync() 方法并将焦点设置在文本框上。

  5. 按下 F5 运行项目,然后导航到 /setfocus

  6. 按下 设置焦点 按钮,注意文本框如何获得焦点。

这个例子可能看起来很愚蠢,因为它只设置了焦点,但这是一个实用的功能,而 autofocus HTML 属性在 Blazor 中不起作用。在 OnAfterRender 方法中调用 FocusAsync 以在页面加载时获取焦点更改会更合理,但这不会让演示变得那么酷。

在我的博客帖子中,我采取了另一种方法。我的目标是设置一个元素的焦点,而无需使用代码。在即将到来的第六章,使用验证构建表单中,我们将实现我的博客帖子中的autofocus功能,但使用新的.NET 功能。

.NET 5 的发布解决了我们之前必须用 JavaScript 编写的一些问题;设置焦点就是一个例子。在.NET 6 中,我们有一种方法可以影响 HTML 头。

影响HTML head

有时,我们想要设置页面的标题或更改社交网络meta标签。head标签位于App组件中,页面这部分不会重新加载/重新渲染(只有路由组件内的组件会重新渲染)。在 Blazor 的早期版本中,你必须自己使用 JavaScript 编写代码来实现这一点。

但.NET 有一个名为HeadOutlet的新组件可以解决这个问题。

要使用这些组件,我们将创建一个页面来查看我们的博客帖子之一。我们将使用我们学到的大多数技术:

  1. SharedComponents项目中,打开Home.razor

  2. foreach循环修改如下:

    <li><a href="/Post/@p.Id">@p.Title</a></li> 
    

    我们在标题中添加了一个链接来查看一个博客帖子。注意我们如何在href属性内部使用@符号来获取帖子的 ID。

  3. Pages文件夹中,添加一个 Razor 组件,并将其命名为Post.razor

  4. code部分中,添加一个参数来保存帖子的 ID:

    [Parameter]
    public string BlogPostId { get; set; } 
    

    这将保存来自 URL 的博客帖子的 ID。

  5. 添加一个page指令以获取集合、URL 和 ID:

    @page "/post/{BlogPostId}" 
    

    page指令将为我们的博客帖子设置 URL 为/post/,后跟帖子的 ID。我们不必在所有组件中添加using语句。相反,打开_Imports.razor并添加以下命名空间:

    @using Data.Models.Interfaces
    @using Data.Models 
    

    这将确保所有我们的组件默认具有这些命名空间。

  6. 再次打开Post.razor,在page指令下方注入 API(命名空间现在由_Imports.razor提供):

    @inject IBlogApi _api
    @inject NavigationManager _navman 
    

    我们现在将 API 注入到组件中,我们可以检索我们的博客帖子。我们还可以访问导航管理器。

  7. code部分中,为我们的博客帖子添加一个属性:

    public BlogPost? BlogPost { get; set; } 
    

    这将包含我们想在页面上显示的博客帖子。

  8. 要加载博客帖子,请添加以下代码:

    protected async override Task OnParametersSetAsync()
    {
        BlogPost=await _api.GetBlogPostAsync(BlogPostId);
        await base.OnParametersSetAsync();
    } 
    

    在这种情况下,我们使用OnParametersSetAsync()方法。这是为了确保我们在从数据库获取数据时设置参数,并且当参数更改时内容会更新。

  9. 我们还必须在帖子中显示并添加必要的meta标签。为此,只需在code部分上方添加以下代码:

    @if (BlogPost != null)
    {
        <PageTitle>@BlogPost.Title</PageTitle>
    <HeadContent>
    <meta property="og:title"
     content="@BlogPost.Title" />
    <meta property="og:description" content="@(new
          string(BlogPost.Text.Take(100).ToArray()))" />
    <meta property="og:image" content=
     "@($"{_navman.BaseUri}/pathtoanimage.png")" />
    <meta property="og:url" content="@_navman.Uri" />
    <meta name="twitter:card" content="@(new string(BlogPost.Text.Take(100).ToArray()))" />
    </HeadContent>
    <h2>@BlogPost.Title</h2>
        @((MarkupString)BlogPost.Text)
    
    } 
    

    当页面首次加载时,BlogPost参数可能为空,因此我们首先需要检查是否应该显示内容。

    通过添加Title组件,Blazor 将设置我们网站的标题,在本例中是博客帖子的标题。

    根据我收集的关于 搜索引擎优化SEO)的信息,我们添加的元标签是使用 Facebook 和 X(以前称为 Twitter)时所需的最基本内容。我们没有为每篇博客文章添加图片,但如果我们愿意,我们可以有一个网站范围内的图片(适用于所有博客文章)。只需将 Pathtoanimage.png 更改为图片名称,并将图片放入 wwwroot 文件夹中。

    如果加载了博客文章,则显示一个带有标题和下方文本的 H3 标签。你可能还记得来自 第四章理解基本 Blazor 组件 中的 MarkupString。这将输出我们博客文章中的字符串,而不会更改 HTML(不会转义 HTML)。

  10. 通过按 F5 键运行项目,并导航到一篇博客文章,可以看到标题的变化:

计算机屏幕截图  描述自动生成

图 5.3:博客文章截图

我们的博客开始成形。我们有一个博客文章列表,可以查看单个文章;我们还有很长的路要走,但我们已经走上了正轨。

组件虚拟化

Virtualize 是 Blazor 中的一个组件,它将确保只渲染可以适应屏幕的组件或行。如果你有一个包含大量项的列表,渲染所有这些项将对内存产生重大影响。

许多第三方组件供应商提供具有相同虚拟化功能的网格组件。在我看来,Virtualize 组件是 .NET 5 版本中最令人兴奋的事情之一。

Virtualize 组件将计算屏幕上可以容纳多少项(基于窗口大小和项的高度)。如果你滚动页面,Blazor 将在内容列表前后添加一个 div 标签,确保滚动条显示正确的位置和比例(即使没有渲染任何项)。

Virtualize 组件的工作方式与 foreach 循环类似。

以下是我们目前在 Home.razor 文件中的代码:

<ul>
    @foreach (var p in posts)
    {
        <li><a href="/Post/@p.Id">@p.Title</a></li>
    }
</ul> 

目前,它将显示我们数据库中的所有博客文章的长列表。诚然,我们现在只有几个,但有一天我们可能会有很多文章。

我们可以将代码(现在不要更改代码)更改为使用新的 Virtualize 组件,将其更改为以下内容:

<Virtualize Items="posts" Context="p">
<li><a href="/Post/@p.Id">@p.Title</a></li>
</Virtualize> 

我们不是使用 foreach 循环,而是使用 Virtualize 组件并添加一个渲染片段,以显示每个项应该如何渲染。Virtualize 组件使用 RenderFragment<T>,默认情况下,它将发送类型为 T 的项到渲染片段。在 Virtualize 组件的情况下,对象将是一篇博客文章(因为项是博客文章的 List<T>)。我们使用名为 context 的变量访问每篇文章。然而,我们可以在 Virtualize 组件上使用 Context 属性来指定另一个名称,因此我们现在使用的是 p 而不是 context

Virtualize 组件比这更强大,正如我们将在下一个要实现的功能中看到的那样:

  1. SharedComponents 项目中,打开 Home.razor

  2. 删除OnInitializedAsync方法和protected List<BlogPost> posts = new List<BlogPost>();我们不再需要它们了。

  3. 将帖子的加载改为Virtualize

    <ul>
    <Virtualize ItemsProvider="LoadPosts" Context="p">
    <li><a href="/Post/@p.Id">@p.Title</a></li>
    </Virtualize>
    </ul> 
    

    在这种情况下,我们使用ItemsProvider委托,它将负责从我们的 API 获取帖子。

    我们传递一个名为LoadPosts的方法,我们还需要将其添加到文件中。

  4. 现在,让我们通过添加以下代码来添加LoadPosts方法:

    public int totalBlogposts { get; set; }
    private async ValueTask<ItemsProviderResult<BlogPost>> LoadPosts(ItemsProviderRequest request)
    {
        if (totalBlogposts == 0)
        {
            totalBlogposts = await _api.GetBlogPostCountAsync();
        }
        var numblogposts = Math.Min(request.Count, totalBlogposts - request.StartIndex);
        var blogposts= await _api.GetBlogPostsAsync(numblogposts,request.StartIndex);
        return new ItemsProviderResult<BlogPost>(blogposts, totalBlogposts);
    } 
    

我们将在数据库中添加一个totalBlogposts属性,用于存储我们目前拥有的帖子数量。LoadPost方法返回带有ItemsProviderResult<Blogpost>ValueTask。该方法以ItemsProviderRequest作为参数,其中包含Virtualize组件想要的帖子数量以及它想要跳过的数量。

如果我们不知道总共有多少帖子,我们需要通过调用GetBlogPostCountAsync方法从我们的 API 检索该信息。然后,我们需要确定我们应该获取多少帖子;要么获取我们需要的所有帖子,要么获取所有剩余的帖子(无论值是多少)。

然后,我们通过调用GetBlogPostsAsync并返回ItemsProviderResult来调用我们的 API 获取实际的帖子。

我们实现了一个Virtualize组件,它将只加载和渲染填充屏幕所需的博客帖子数量。但这是一个需要交互才能工作的交互式组件。如果你现在尝试运行项目,你会注意到屏幕是空的。如果我们向Home组件添加@rendermode InteractiveServer,它就会再次开始工作。目前,InteractiveServer是我们唯一能用的东西。这是我们在 DI 方面设置好的唯一场景。

错误边界

在.NET 6 中,我们有一个非常方便的组件来处理错误,称为ErrorBoundary

我们可以用ErrorBoundary组件包围组件;如果发生错误,它将显示错误消息而不是整个页面失败:

<ErrorBoundary>
<ComponentWithError />
</ErrorBoundary> 

此组件接受两个渲染片段。通过指定如前例所示,我们只设置了ChildContent渲染片段。这是默认的。

我们也可以提供自定义的错误消息,如下所示:

<ErrorBoundary>
<ChildContent>
<ComponentWithError />
</ChildContent>
<ErrorContent>
<h1 style="color: red;">Oops… something broke</h1>
</ErrorContent>
</ErrorBoundary> 

在这个示例中,我们指定了ChildContent,这使得我们能够指定多个属性,就像ErrorContent一样。这是一个很好的组件,可以扩展并创建自己的功能。您可以通过使用context参数(就像我们在virtualize中做的那样)来访问异常:

<ErrorBoundary Context="ex">
<ChildContent>
<p>@(1/zero)</p>
</ChildContent>
<ErrorContent>
       An error occurred
       @ex.Message
    </ErrorContent>
</ErrorBoundary>
@code {
    int zero = 0;
} 

这是在 UI 中处理错误的好方法。

部分

.NET 8 为我们提供了添加部分的能力。你可能还记得WebForms中类似的特性。

我们可以使用SectionOutlet组件在布局组件中定义一个区域,我们想在其中插入内容。然后,在我们的组件内部,我们可以添加一个SectionContent,在其中添加我们想要在出口中显示的内容。

如果我们有一个以上的 SectionContent 引用了 SectionOutlet,它将渲染最新的 SectionContent。我们可以通过使用部分名称或部分 ID 来引用一个 SectionOutlet。部分名称只是一个我们可以使用的字符串。ID 是一个对象,因此我们可以获得更优雅的语法来跟踪我们的部分。

我们可以在布局文件中添加一个部分,并从我们的组件中添加内容到该部分。这是一个布局问题。假设我们想添加上下文菜单。例如,这样我们就可以更改一个完全不同的组件中的菜单。

让我们看看一些代码。

首先,我们可能需要添加这个命名空间:

Microsoft.AspNetCore.Components.Sections; 

最好在 _imports.razor 文件中(因为这是内置组件之一)。

在布局组件中,我们添加一个出口,如下所示:

<SectionOutlet SectionName="top-header"/> 

然后,在我们的组件中,我们可以添加一个 SectionContent,如下所示:

<SectionContent SectionName="top-header">
<b>Test</b>
</SectionContent> 

如果我们想使用部分 ID,可以这样做:在布局文件中,假设它被命名为 MainLayout

<SectionOutlet SectionId="MainLayout.TopHeader"/> 

MainLayout 的代码部分:

@code
{
    public static SectionOutlet TopHeader = new()
} 

然后,在组件内部,我们将其更改为这样:

<SectionContent SectionId="Layout.MainLayout.TopHeader">
<b>Using SectionId</b>
</SectionContent> 

这是一种改变布局文件的好方法。通过这样做,我们可以创建更高级的布局,这些布局可以与每个页面/组件一起工作。我们可以将更多的布局移动到布局文件中,而不是将其放在每个组件中。我喜欢这个功能。这将清理掉很多代码。

摘要

在本章中,我们探讨了构建组件的更高级场景。构建组件正是 Blazor 的核心所在。组件还使得在过程中进行更改变得容易,因为只有一个地方必须实现更改。我们还实现了我们的第一个可重用组件,这将有助于在整个团队中保持相同的标准并减少重复代码。

我们还使用了一些 Blazor 功能来加载和显示数据。

在下一章中,我们将探讨表单和验证,以开始构建我们博客的管理部分。

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/WebDevBlazor3e

第六章:使用验证构建表单

在本章中,我们将学习创建和验证表单,这是一个构建我们的管理界面的绝佳机会,我们可以管理我们的博客文章,并查看新的增强表单导航。我们还将构建多个可重用组件,并了解 Blazor 中的一些新功能。

本章将非常有趣,我们将使用到目前为止所学的许多东西。

在本章中,我们将涵盖以下主题:

  • 探索表单元素

  • 添加验证

  • 自定义验证类属性

  • 查看绑定

  • 构建管理界面

  • 添加抽象层

技术要求

确保您已经遵循了前面的章节,或者以 Chapter05 文件夹作为起点。

您可以在github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter06找到本章结果的源代码。

探索表单元素

HTML 中有许多表单元素,我们可以在 Blazor 中使用它们。最终,Blazor 输出的是 HTML。

Blazor 确实有一些组件可以增加功能,因此我们可以也应该尝试使用这些组件而不是 HTML 元素。内置组件为我们提供了免费的功能。

Blazor 提供以下组件:

  • EditForm

  • InputBase<>

  • InputCheckbox

  • InputDate<TValue>

  • InputNumber<TValue>

  • InputSelect<TValue>

  • InputText

  • InputTextArea

  • InputRadio

  • InputRadioGroup

  • ValidationMessage

  • ValidationSummary

让我们在下一节中逐一介绍它们。

EditForm

EditFormform 标签的形式渲染,但它具有更多功能。

首先,我们不会像传统的 form 标签那样创建操作或方法;Blazor 将处理所有这些。

EditForm 将创建一个 EditContext 实例作为级联值,以便所有放入 EditForm 中的组件都可以访问相同的 EditContextEditContext 跟踪有关编辑过程的相关元数据,例如哪些字段已被编辑,并跟踪任何验证消息。

您需要分配一个模型(您希望编辑的类)或一个 EditContext 实例。

对于大多数用例,分配模型是最佳选择,但对于更高级的场景,您可能希望能够触发 EditContext.Validate(),例如,验证与 EditContext 连接的所有控件。这种情况很少发生,但了解它是可能的可能是个好主意。

EditForm 具有以下事件,您可以使用它们来处理表单提交:

  • OnValidSubmit 在表单中的数据验证正确时被触发(我们将在稍后回到验证)。

  • OnInvalidSubmit 如果表单没有正确验证,则被触发。

  • OnSubmit在表单提交时被触发,无论表单是否正确验证。如果你想要自己控制验证,请使用OnSubmit

让我们来看一个例子。

考虑一个包含一个人的类的例子;这个类有一个人的名字和年龄,看起来像这样:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
} 

这个类的EditForm看起来像这样(目前没有其他元素):

<EditForm Model="personmodel" OnValidSubmit="@validSubmit">
    ...
    <button type="submit">Submit</button>
</EditForm>
@code {
    Person personmodel = new Person();
    private Task validSubmit()
    {
        //Do database stuff
return Task.CompletedTask;
    }
} 

EditForm指定了一个模型(在这种情况下,personmodel),我们正在监听OnValidSubmit事件。

Submit按钮是一个普通的 HTML 按钮,不是一个特定的 Blazor 组件。

InputBase<>

所有的 Blazor 输入类都从InputBase类派生。它有一系列我们可以用于所有input组件的东西;我们将介绍其中最重要的。

InputBase处理AdditionalAttributes,这意味着如果我们向标签添加任何其他属性,它们将自动转移到输出中。这意味着从该类派生的组件可以利用任何 HTML 属性,因为它们将是输出的一部分。

InputBaseValue属性,我们可以将其绑定,还有一个当值改变时被调用为ValueChanged的事件回调。

我们还可以更改DisplayName,以便自动验证消息将反映正确的名称,而不是属性的名称,这是默认行为。

并非所有控件都支持DisplayName属性。一些属性仅用于组件内部,我们将在稍后回到这些属性。

InputCheckbox

InputCheckbox组件将渲染为<input type="checkbox">

InputDate<TValue>

InputDate组件将渲染为<input type="date">。我们可以使用DateTimeDateOnlyTimeOnlyDateTimeOffset作为InputDate组件的值。

没有方法可以格式化日期;它将使用网络浏览器的当前设置。这种行为是按设计进行的,也是 HTML5 规范的一部分。

InputNumber<TValue>

InputNumber组件将渲染为<input type="number">。我们可以使用Int32Int64SingleDoubleDecimal作为InputNumber组件的值。

InputSelect<TValue>

InputSelect组件将渲染为<select>。我们将在本章的后面创建InputSelect,所以在这里我不会进一步详细介绍。

InputText

InputText组件将渲染为<input type="text">

InputTextArea

InputSelect组件将渲染为<textarea>。在本章中,我们将构建这个控件的自定义版本。

InputRadio

InputRadio组件将渲染为<input type="radio">。用于单个选项。

InputRadioGroup

InputRadioGroup组件本身不是一个元素,而是将其他InputRadioInputs分组。用于分组选项。我们可以在InputRadioGroup内部添加多个InputRadio组件。

InputFile

InputFile组件将渲染为<Input type="file">。此组件将使获取文件数据变得更容易。它将为每个文件的内容提供一个流。

我们可以通过查看以下文档进一步了解InputFilelearn.microsoft.com/en-us/aspnet/core/blazor/file-uploads?view=aspnetcore-8.0

如我们所见,几乎所有 HTML 表单控件都有一个 Blazor 组件,其中包含一些附加功能,例如验证,我们将在下一节中看到。

添加验证

我们已经触及了验证;input组件和EditForm中有些内置的功能可以处理验证。

向我们的表单添加验证的一种方法是通过使用DataAnnotations。使用 DataAnnotations,我们不需要编写任何自定义逻辑来确保表单中的数据正确;相反,我们可以在数据模型中添加属性,并让DataAnnotationsValidator处理其余部分。

.NET 中已经存在许多我们可以使用的DataAnnotations实例;我们也可以构建自己的注释。

一些内置的数据注释如下:

  • Required:这使得字段成为必填项。

  • Email:这将检查输入的值是否为电子邮件地址。

  • MinLength:这将检查字符数是否不少于指定值。

  • MaxLength:这将检查字符数是否不超过指定值。

  • Range:这将检查值是否在特定范围内。

有许多其他注释可以帮助我们验证数据。为了测试这一点,让我们在我们的数据类中添加数据注释:

  1. Data.Models项目中,打开Models/BlogPost.cs文件。

  2. 在文件顶部添加一个using语句用于System.ComponentModel.DataAnnotations

    using System.ComponentModel.DataAnnotations; 
    
  3. RequiredMinLength属性添加到Title属性:

    [Required]
    [MinLength(5)]
    public string Title { get; set; } = string.Empty; 
    

    Required属性将确保标题不能为空,而MinLength将确保它至少有5个字符。

  4. Required属性添加到Text属性:

    [Required]
    public string Text { get; set; } = string.Empty; 
    

    Required属性将确保Text属性不能为空,这是有意义的——我们为什么要创建一个空的博客文章?

  5. 打开Models/Category.cs文件,并在文件顶部添加一个using语句用于System.ComponentModel.DataAnnotations

  6. Required属性添加到Name属性:

    [Required]
    public string Name { get; set; } = string.Empty; 
    

    Required属性将确保我们无法留空名称。

  7. 打开Models/Tag.cs文件,并在文件顶部添加一个using语句用于System.ComponentModel.DataAnnotations

  8. Required属性添加到Name属性:

    [Required]
    public string Name { get; set; } = string.Empty; 
    

    Required属性将确保我们无法留空名称。

  9. 打开Models/Comment.cs文件,并在文件顶部添加一个using语句用于System.ComponentModel.DataAnnotations

  10. Required属性添加到NameText属性:

    [Required]
    public string Text { get; set; } = string.Empty;
    [Required]
    public string Name { get; set; } = string.Empty; 
    

太好了,现在我们的数据模型已经内置了验证。我们需要向我们的用户提供有关验证错误的反馈。

我们可以通过使用ValidationMessageValidationSummary组件来实现这一点。

ValidationMessage

ValidationMessage组件可以显示特定属性的个别错误消息。我们希望使用此组件在表单元素下显示验证错误。

要添加ValidationMessage组件,我们必须指定For属性,并给出我们想要显示验证错误的属性的名称:

<ValidationMessage For="@(() => personmodel.Name)"/> 

ValidationSummary

ValidationSummary组件将显示整个EditContext的所有验证错误列表:

<ValidationSummary/> 

由于ValidationSummary组件通过级联值访问EditContext,我们不需要向其提供任何模型或属性。

我更喜欢将错误显示在问题附近,这样用户就可以看到问题所在。然而,我们也有选项使用ValidationSummary将验证错误显示为列表。

为了确保我们的输入控件与 Bootstrap 主题(或我们可能使用的任何主题)相匹配,我们可以创建我们的自定义验证类

自定义验证类属性

通过简单地使用编辑表单、输入组件和DataAnnotationValidator,框架将在组件有效或无效时自动向其添加类。

默认情况下,这些类是.valid.invalid。在.NET 5 中,我们被赋予了自定义这些类名的途径。

当使用 Bootstrap 时,默认的类名是.is-valid.is-invalid,类名列表还必须包括.form-control以获取适当的样式。

我们接下来构建的下一个组件将帮助我们为所有表单组件获取适当的 Bootstrap 样式。

我们将创建自己的FieldCssClassProvider来自定义 Blazor 将使用的类。

  1. SharedComponents项目中,在ResuableComponents文件夹中,添加一个名为BootstrapFieldCssClassProvider.cs的新类。

  2. 打开新类并添加以下代码:

    using Microsoft.AspNetCore.Components.Forms;
    namespace SharedComponents.ResuableComponents ;
    public class BootstrapFieldCssClassProvider : FieldCssClassProvider
    {
        public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
       {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
            var isModified = editContext.IsModified(fieldIdentifier);
            return (isModified, isValid) switch
            {
                (true, true) => "form-control modified is-valid",
                (true, false) => "form-control modified is-invalid",
                (false, true) => "form-control",
                (false, false) => "form-control"
            };
        }
    } 
    

    BootstrapFieldCssClassProvider需要一个EditContext实例来工作。

    代码将检查表单(或具体为EditContext)是否有效以及是否已被修改。基于此,它将返回正确的 CSS 类。

    它为所有元素返回表单控件;这样,我们就不必在表单中的每个元素上添加它。我们可以验证未修改的表单为有效或无效,但我们不希望它仅仅因为尚未更改就显示表单是好的。

    在我们即将构建的代码之外,我们需要从我们的EditForm中获取EditContext实例,然后在EditContext上设置FieldCssClassProvider,如下所示:

    CurrentEditContext.SetFieldCssClassProvider(provider);

    接下来,我们将使用我们即将创建的CustomCssClassProvider以更优雅的方式(以我谦卑的观点)来完成这项工作。

    在本章的早期部分,我提到EditForm将其EditContext作为CascadingValue公开。

    这意味着我们将构建一个可以放入我们的EditForm中并以此方式访问EditContext的组件。

  3. SharedComponents项目中,在项目根目录下,添加一个新类,并将其命名为CustomCssClassProvider.cs

  4. 打开新文件,并用以下代码替换内容:

    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Forms;
    namespace SharedComponents;
    public class CustomCssClassProvider<ProviderType> : ComponentBase where ProviderType : FieldCssClassProvider, new()
    {
        [CascadingParameter]
        EditContext? CurrentEditContext { get; set; }
        public ProviderType Provider { get; set; } = new();
        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException($"{nameof(CustomCssClassProvider <ProviderType>)} requires a cascading parameter of type {nameof(EditContext)}. For example, you can use {nameof(CustomCssClassProvider<ProviderType>)} inside an EditForm.");
            }
            CurrentEditContext.SetFieldCssClassProvider
             (Provider);
        }
    } 
    

这个泛型组件接受一个type值,在这种情况下,是Provider的类型。

我们指定type必须继承自FieldCssClassProvider并且必须有一个无参数的构造函数。

该组件继承自ComponentBase,这使得我们可以在 Blazor 组件内部放置该组件。

在这种情况下,我们只用 C#编写我们的组件,但它并没有渲染任何内容。

我们有一个Cascading参数,它将从EditForm中填充。如果由于某种原因(例如,如果我们将组件放置在EditForm之外)缺少EditContext,我们将抛出一个异常。

最后,我们在EditContext上设置FieldCssClassProvider

要使用该组件,我们必须在EditForm内部添加以下代码(别担心,我们很快就会创建一个EditForm):

<CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider"/> 

我们为我们的CustomCssClassProvider组件提供正确的ProviderTypeBootstrapFieldCssClassProvider

这是一种实现组件以帮助我们封装功能的方法。我们也可以这样编写代码:

<EditForm Model="personmodel" @ref="CurrentEditForm">
…
</EditForm>
@code {
    public EditForm CurrentEditForm { get; set; }
    protected override Task OnInitializedAsync()
    {
        CurrentEditForm.EditContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider())
        return base.OnInitializedAsync();
    }
} 

但有了新的CustomCssClassProvider组件,我们可以这样编写相同的内容:

<EditForm Model="personmodel">
<CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" />
</EditForm> 

如果我们在做与EditContext相关的事情,我们可以始终创建一个像这样的组件,因为它是一个cascading参数。

现在,我们有一个组件,可以使我们的表单控件看起来像 Bootstrap 控件,而且我们不再需要为每个组件添加特定的代码,现在我们可以添加CustomCssClassProvider组件。接下来,是时候将其付诸实践,通过构建我们的管理界面来创建几个表单。

查看绑定

在本章中,我们使用绑定将数据绑定到我们的表单控件上。我们在第五章创建高级 Blazor 组件中简要讨论了绑定,但现在是我们深入探讨绑定的时候了。

绑定到 HTML 元素

对于 HTML 元素,我们可以使用@bind将变量绑定到元素上。

因此,如果我们正在绑定到一个文本框,我们会这样做:

<input type="text" @bind="Variable"/> 

@bind@bind-value都起作用,并且做同样的事情。注意value中的小写v。输入元素是一个 HTML 元素,它将以一个普通的 HTML 元素渲染,没有额外的功能(除了绑定)。将其与InputText进行比较,它将以类似的方式工作,但也会提供额外的功能,如验证和样式。

默认情况下,当离开文本框时,变量中的值会发生变化。但我们可以通过添加一个@bind:event属性来改变这种行为,如下所示:

<input type="text" @bind="Variable" @bind:event="oninput"/> 

我们甚至可以使用像@bind:get@bind:set这样的属性来完全控制正在发生的事情:

<input type="text" @bind:get="SomeText" @bind:set="SetAsync" /> 

这些操作与 @bind 做的是同一件事情,所以我们不能与 @bind 同时使用。@bind:set 属性还有一个很好的特性。当我们设置一个值时,我们可以运行异步方法。

在设置值之后,我们可以使用 @bind:after 来运行一个方法,如下所示:

<input type="text" @bind="SomeText" @bind:after="AfterAsync" /> 

这在绑定到 HTML 元素时提供了很大的灵活性。

此外,我们还可以使用 @bind:culture 来设置文化。日期和数字字段使用不变的文化,并将使用适当的浏览器格式化,但如果使用文本字段,我们可以像这样更改行为:

<input type="text" @bind="SomeNumber" @bind:culture="GBCulture" /> 

其中 GBCulture 在这个例子中是一个 CultureInfo 对象。最后,我们可以使用 @bind:format 来设置格式。目前这仅适用于 DateTime

<input type="text" @bind="SomeDate" @bind:format="MM/dd/yyyy" />
<input type="text" @bind="SomeDate" @bind:format="yyyy-MM-dd" /> 

我们现在已经知道如何绑定到 HTML 元素。接下来,我们将看看如何绑定到组件。

绑定到组件

当绑定到组件时,GetSetAfter 也会起作用。CultureEventFormat 在某些组件上也会起作用。

当绑定到组件时,我们使用 @bind-{ParameterName},所以对于 Value 参数,它看起来是这样的:

<InputText @bind-Value="text" /> 

在后台,@bind-Value 将影响另外两个参数,ValueExpressionValueChanged。这意味着如果你使用 @bind-Value,将无法手动设置它们。当我们更改值时,ValueChanged 将被触发,我们可以监听这个事件,并在它改变时执行某些操作。

我们也可以像这样使用 GetSet

<InputText @bind-Value:get="text" @bind-Value:set="(value) => {text=value; }" />
<InputText @bind-Value:get="text" @bind-Value:set="Set" />
<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" /> 

我们必须始终提供 GetSet,并且它们不能与 @bind-Value 结合使用。这些示例使用 InputText,这是一个内置的 Blazor 组件,但这个概念适用于任何组件上的任何参数。对于 After 也是如此。它可以与任何组件一起使用,如下所示:

<InputText @bind-Value="text" @bind-Value:after="() => { }" />
<InputText @bind-Value="text" @bind-Value:after="After" />
<InputText @bind-Value="text" @bind-Value:after="AfterAsync" /> 

我们可以访问一些很好的绑定功能,并且它们在绑定到组件以及 HTML 元素时都有效。

接下来,我们将使用绑定来构建一个管理界面。

构建管理界面

现在,是时候为我们的博客构建一个简单的管理界面了。

我们需要能够做到以下几点:

  • 列出分类

  • 编辑分类

  • 列出标签

  • 编辑标签

  • 列出博客文章

  • 编辑博客文章

如果我们查看前面的列表,我们可能会注意到一些事情看起来很相似——也许我们可以为这些事情构建共享组件。分类和标签非常相似;它们有名称,而且我们唯一能编辑的就是名称。

让我们为这个目的创建一个组件。该组件将负责列出、添加、删除和更新对象。

由于我们正在处理的对象要么是 Category 要么是 Tag,我们需要能够根据对象调用不同的 API,因此我们的组件需要是通用的:

  1. SharedComponents 项目中,在项目根目录下添加一个新的 Razor 组件,并命名为 ItemList.razor

  2. 打开新创建的文件,并在文件顶部添加:

    @typeparam ItemType 
    

    @typeparam是为了使组件泛型,持有泛型类型的变量被称为ItemType

  3. 添加一个code部分(如果您还没有的话),并添加以下代码行:

    @code{
    [Parameter]
    public List<ItemType> Items { get; set; } = new();
    [Parameter, EditorRequired]
        public required RenderFragment<ItemType> ItemTemplate { get; set; }
    } 
    
  4. 我们需要两个参数:一个列表,我们可以添加所有项目,以及一个ItemTemplate实例,我们可以用它来改变我们希望显示项目的方式。EditorRequired属性确保我们在使用组件时需要设置此值。否则,Visual Studio 将显示敌对错误消息,直到我们修复它。

    在这种情况下,我们使用RenderFragment<T>,这将使我们能够访问模板内部的项目(一旦我们实现它,一切就会变得清晰起来)。

  5. 我们还需要几个事件;在code部分添加以下代码:

    [Parameter]
    public EventCallback<ItemType> DeleteEvent { get; set; }
    [Parameter]
    public EventCallback<ItemType> SelectEvent { get; set; } 
    

    我们添加了两个事件;第一个是在我们删除一个标签或一个类别时。我们将向父组件发送一个事件,在那里我们可以添加删除项目的所需代码。

    第二个是在我们选择一个项目以便我们可以编辑项目时。

  6. 现在,是时候添加 UI 了;将文件中@typeparam下面的顶部替换为以下代码:

    @using System.Collections.Generic
    <h3>List</h3>
    <table>
    <Virtualize Items="@Items" Context="item">
    <tr>
    <td>
    <button class="btn btn-primary" @onclick="@(()=> {SelectEvent.InvokeAsync(item); })"> Select</button>
    </td>
    <td>@ItemTemplate(item)</td>
    <td>
    <button class="btn btn-danger" @onclick="@(()=> {DeleteEvent.InvokeAsync(item);})"> Delete</button>
    </td>
    </tr>
    </Virtualize>
    </table> 
    

如果我们回顾到步骤 3,我们会注意到我们使用了列表的变量和RenderFragment

然后,我们使用新的Virtualize组件来列出我们的项目;公平地说,我们可能没有那么多类别或标签,但为什么不用呢?我们将Items属性设置为"Items"(这是我们列表的名称)并将Context参数设置为"item"

我们可以给它任何我们想要的名称;我们只会在Virtualize渲染模板内部使用它。

我们添加了两个按钮,这两个按钮简单地调用了我们在步骤 4中添加的EventCallback实例。在这两个按钮之间,我们添加了@ItemTemplate(item);我们希望 Blazor 渲染模板,同时也发送循环中的当前项目。

这意味着我们可以在模板内部访问项目的值。

列出和编辑类别

使用我们的新组件,现在是时候创建一个用于列出和编辑我们类别的组件了:

  1. SharedComponents项目中,打开_Imports.razor。确保包含以下命名空间:@using SharedComponents@using Microsoft.AspNetCore.Components.Forms

  2. 右键单击Pages文件夹,选择添加 | 新建文件夹,并将文件夹命名为Admin

  3. Pages/Admin文件夹中,添加一个新的 Razor 组件,并将其命名为CategoryList.razor

  4. 在组件顶部,将<h3>CategoryList</h3>替换为以下代码:

    @page "/admin/categories"
    @rendermode InteractiveServer @using SharedComponents.ReusableComponents @inject IBlogApi _api <h3>Categories</h3> 
    

    我们从@page指令开始,告诉 Blazor,如果我们导航到"``admin/categories" URL,我们将到达CategoryList.Razor组件。这个组件有一些交互性,因此我们需要设置我们希望使用的交互模式。在这种情况下,我们使用InteractiveServer。如果我们想使用InteractiveAutoInteractiveWebAssembly,我们需要将组件放在BlazorWebApp.Client项目中。我们将添加一个using语句并注入我们的 API。

  5. 下一步是添加一个用于编辑类别的表单。在上一步骤的代码下方添加以下代码:

    <EditForm OnValidSubmit="Save" Model="Item">
    <DataAnnotationsValidator />
    <CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" />
    <InputText @bind-Value="@Item.Name" />
    <ValidationMessage For="@(()=>Item.Name)" />
        <button class="btn btn-success" type="submit">Save</button>
    </EditForm> 
    

    我们添加了 EditForm,如果表单验证通过,它将执行 Save 方法。对于验证,我们添加了 DataAnnotationsValidator,它将验证提供的数据与我们添加到 TagCategory 类的注解。

    由于我们正在使用 Bootstrap,我们希望我们的表单控件看起来相同,因此我们添加了之前在本章中创建的 CustomCssClassProvider

    我们添加了 InputText 并将其连接到一个名为 ItemCategory 对象(我们将在下一秒添加它)。

    在下面,我们添加了 ValidationMessage,它将显示 name 属性的任何错误,然后是一个 提交 按钮。

  6. 现在,是时候添加我们的 ItemList 组件了;在上一步骤添加的代码下方添加以下代码:

    <ItemList Items="Items" DeleteEvent="@Delete" SelectEvent="@Select" ItemType="Category">
    <ItemTemplate>
            @{
                var item = context as Category;
                if (item != null)
                {
                    @item.Name
                }
            }
        </ItemTemplate>
    </ItemList> 
    

    我们添加我们的组件并将 Items 属性绑定到一个项目列表(我们将在下一步创建该列表)。

    我们将 SelectDelete 事件绑定到方法,并在 ItemType 属性中指定列表的类型。然后,我们有 ItemTemplate。由于我们正在使用 RenderFragment<T>,我们现在可以访问一个名为 context 的变量。

    我们将那个变量转换为类别并打印出类别的名称。这是将在列表上显示的每个项目的模板。

  7. 最后,我们将以下代码添加到替换 code 部分的代码中:

    @code {
        private List<Category> Items { get; set; } = new();
        public Category Item { get; set; } = new();
        protected async override Task OnInitializedAsync()
        {
            Items = (await _api.GetCategoriesAsync()) ?? new();
            await base.OnInitializedAsync();
        }
        private async Task Delete(Category category)
        {
            try
            {
                await _api.DeleteCategoryAsync(category.Id!);
                Items.Remove(category);
            }
            catch { }
        }
        private async Task Save()
        {
            try
            {
                await _api.SaveCategoryAsync(Item);
                if (!Items.Contains(Item))
                {
                    Items.Add(Item);
                }
                Item = new Category();
            }
            catch { }
        }
        private Task Select(Category category)
        {
            try
            {
                Item = category;
            }
            catch { }
            return Task.CompletedTask;
        }
    } 
    

    我们添加了一个用于存储所有类别的列表和一个用于存储一个项目(当前正在编辑的项目)的变量。我们使用 OnInitializedAsync 从 API 加载所有类别。

    DeleteSave 方法调用 API 的相应方法,而 Select 方法将提供的项目放入 item 变量中(准备进行编辑)。

    在将项目添加到列表之前,我们检查列表中是否已经存在该项目。运行项目并导航到 /admin/categories

  8. 尝试添加、编辑或删除一个类别,如图 6.1 所示:

计算机屏幕截图  描述自动生成

图 6.1:编辑类别视图

现在,我们需要一个用于列出和编辑标签的组件,这几乎与之前相同,但我们需要使用 Tag 而不是 Category

列出和编辑标签

我们刚刚创建了一个用于列出和编辑类别的组件;现在,我们需要创建一个用于列出和编辑标签的组件:

  1. BlazorWebApp.Client 项目中,在 Pages 文件夹下,添加一个名为 Admin 的新文件夹。

  2. 添加一个名为 TagList.razor 的新 Razor 组件。

  3. 在组件顶部,将 <h3>TagList</h3> 替换为以下代码:

    @page "/admin/tags"
    @rendermode InteractiveServer
    @using Data.Models
    @using Data.Models.Interfaces
    @using SharedComponents
    @using SharedComponents.ReusableComponents
    @inject IBlogApi _api <h3>Tags</h3> 
    

    我们从 @page 指令开始,告诉 Blazor 如果我们导航到 "admin/tags" URL,我们将到达 TagList.Razor 组件。我们还指定了渲染模式为 InteractiveServer。我们添加了一个 using 语句,然后注入我们的 API。

  4. 下一步是添加一个用于编辑标签的表单。在上一步骤的代码下方添加以下代码:

    <EditForm OnValidSubmit="Save" Model="Item">
    <DataAnnotationsValidator />
    <CustomCssClassProvider
     ProviderType="BootstrapFieldCssClassProvider" />
    <InputText @bind-Value="@Item.Name" />
    <ValidationMessage For="@(()=>Item.Name)" />
    <button class="btn btn-success" type="submit">Save</button>
    </EditForm> 
    

    我们添加了EditForm,如果表单验证没有问题,它将执行Save方法。为了验证,我们添加了DataAnnotationsValidator,它将验证提供的数据与我们添加到TagCategory类中的注释。

    由于我们使用 Bootstrap,我们希望我们的表单控件看起来一样,因此我们添加了CustomCssClassProvider,这是我们在此章之前创建的。

    我们添加了InputText并将其连接到名为ItemTag对象(我们将在稍后添加它)。

    在其下方,我们添加了一个ValidationMessage实例,它将显示name属性的任何错误,然后是一个提交按钮。

  5. 现在,是时候添加我们的ItemList组件了。在上一步添加的代码下方,添加以下代码:

    <ItemList Items="Items" DeleteEvent="@Delete" SelectEvent="@Select" ItemType="Tag">
    <ItemTemplate>
            @{
                var item = context as Tag;
                if (item != null)
                {
                    @item.Name
                }
            }
        </ItemTemplate>
    </ItemList> 
    

    我们添加了我们的组件,并将Items属性绑定到项目列表(我们将在下一步创建该列表)。我们将SelectDelete事件绑定到方法,并在ItemType属性中指定List类型。

    然后是ItemTemplate;由于我们正在使用RenderFragment<T>,我们现在可以访问一个名为context的变量。我们将该变量转换为标签并打印出标签的名称。

    这是列表中显示的每个项目的模板。

  6. 最后,我们将代码部分替换为以下代码:

    @code {
        private List<Tag> Items { get; set; } = new List<Tag>();
        public Tag Item { get; set; } = new Tag();
        protected async override Task OnInitializedAsync()
        {
            Items = (await _api.GetTagsAsync())??new();
            await base.OnInitializedAsync();
        }
        private async Task Delete(Tag tag)
        {
            try
            {
                await _api.DeleteTagAsync(tag.Id!);
                Items.Remove(tag);
            }
            catch { }
        }
        private async Task Save()
        {
            try
            {
                await _api.SaveTagAsync(Item);
                if (!Items.Contains(Item))
                {
                    Items.Add(Item);
                }
                Item = new Tag();
            }
            catch { }
        }
        private Task Select(Tag tag)
        {
            try
            {
                Item = tag;
            }
            catch { }
            return Task.CompletedTask;
        }
    } 
    

    我们添加了一个列表来存储所有标签,以及一个变量来存储一个项目(当前正在编辑的项目)。我们使用OnInitializedAsync从 API 加载所有标签。

    DeleteSave方法调用 API 的相应方法,而Select方法将提供的项目放入Item变量中(准备编辑)。

    在我们将项目添加到列表之前,我们检查列表中是否已经存在该项目。

  7. 运行项目并导航到/admin/tags

  8. 尝试添加、编辑和删除标签,如图6.2所示:

计算机屏幕截图 自动生成描述

图 6.2:编辑标签视图

现在,我们需要列出和编辑博客文章的方法。

列出和编辑博客文章

让我们从列出和编辑博客文章开始:

  1. SharedComponents项目中,在Pages/Admin文件夹中,添加一个名为BlogPostList.razor的新 Razor 组件。

  2. BlogPostList.razor文件顶部,将<h3>BlogPostList</h3>替换为以下代码:

    @page "/admin/blogposts"
    @attribute [StreamRendering(true)]
    @inject IBlogApi _api
    <a href="/admin/blogposts/new">New blog post</a>
    @if (posts?.Count == 0)
    {
        <p>No blog posts found</p>
    }
    else if (posts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <ul>
            @foreach (var p in posts)
            {
                <li>
                    @p.PublishDate
                    <a href="/admin/blogposts/@p.Id">@p.Title</a>
    </li>
            }
        </ul>
    } 
    

    我们添加了一个page指令,注入我们的 API,并使用foreach循环列出博客文章。我们还启用了StreamingRendering,因为这个页面没有任何交互性,所以没有必要添加。这也意味着我们不能使用Virtualize组件,因为它具有交互性。

    我们还通过博客的Id实例将帖子链接到 URL。

  3. code部分添加以下代码:

    private List<BlogPost>? posts = null;
    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(1000);
        var numberofposts = await _api.GetBlogPostCountAsync();
        posts = await _api.GetBlogPostsAsync(numberofposts, 0);
        await base.OnInitializedAsync();
    } 
    

    我们添加了从数据库加载帖子功能,并添加了小延迟,以便我们可以看到加载中…仅短暂的一瞬间。现在,这个部分只剩下最后一件事:添加一个可以编辑博客文章的页面。

    写博客的一个非常流行的方式是使用 Markdown;我们的博客引擎将支持这一点。由于 Blazor 支持任何 .NET 标准的 动态链接库DLL),我们将添加一个名为 Markdig 的现有库。

    这与微软用于他们 docs 网站的相同引擎。

    我们可以将 Markdig 扩展为不同的扩展(就像微软所做的那样),但让我们保持简单,只添加对 Markdown 的支持,而不添加所有花哨的扩展。

  4. SharedComponents 项目中,在解决方案资源管理器中的 依赖项 节点右键单击,并选择 管理 NuGet 包

  5. 搜索 Markdig 并点击 安装

  6. 在项目的根目录下添加一个名为 InputTextAreaOnInput.cs 的新类。

  7. 打开新文件,并用以下代码替换其内容:

    using System.Diagnostics.CodeAnalysis;
    using Microsoft.AspNetCore.Components.Rendering;
    namespace Microsoft.AspNetCore.Components.Forms;
        public class InputTextAreaOnInput :
         InputBase<string?>
        {
            protected override void BuildRenderTree(RenderTreeBuilder builder)
            {
                builder.OpenElement(0, "textarea");
                builder.AddMultipleAttributes(1, AdditionalAttributes);
                builder.AddAttribute(2, "class", CssClass);
                builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue));
                builder.AddAttribute(4, "oninput", EventCallback.Factory.CreateBinder <string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
                builder.CloseElement();
            }
            protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
            {
                result = value;
                validationErrorMessage = null;
                return true;
            }
        } 
    

    上述代码来自微软的 GitHub 仓库;这是他们实现 InputTextArea 组件的方式。

    在他们的构建系统中,他们无法处理 .razor 文件,这就是为什么他们以这种方式实现代码。我在微软的代码中做了一处更改,那就是 oninput,它以前说的是 OnChange

    对于大多数情况,OnChange 就足够了,这意味着当我离开文本框时,值将被更新(并触发验证)。但在我们的情况下,我们希望 HTML 预览实时更新,这就是为什么我们必须实现自己的。

    一个选择可能是不要使用 InputTextArea 组件,而是使用 TextArea 标签,但我们会失去验证高亮。如果我们需要自定义输入控件的行为,这就是我们采取的方式。

    如果你对实现进行很多更改,我建议使用 .razor 文件而不是 .cs 文件。

  8. Pages/Admin 文件夹中,添加一个名为 BlogPostEdit.razor 的新 Razor 组件。

  9. BlogPostEdit.razor 文件顶部,将 <h3>BlogPostEdit</h3> 替换为以下代码:

    @page "/admin/blogposts/new"
    @page "/admin/blogposts/{Id}"
    @rendermode InteractiveServer
    @inject IBlogApi _api
    @inject NavigationManager _manager
    @using Markdig; @using Microsoft.AspNetCore.Components.Forms 
    

    我们添加了两个不同的 page 指令,因为我们想能够创建一个新的博客文章,同时也提供一个 ID 来编辑已经存在的文章。如果我们不提供 ID,则 Id 参数将为空(或默认值)。

    我们注入我们的 API 和 NavigationManager,以及添加 using 语句。

  10. 我们还需要一些变量。在 code 部分添加以下代码:

    [Parameter]
    public string? Id { get; set; }
    BlogPost Post { get; set; } = new();
    List<Category> Categories { get; set; }=new();
    List<Tag> Tags { get; set; }= new();
    string? selectedCategory = null;
    string? markDownAsHTML { get; set; } 
    

    我们添加了一个用于博客文章 ID 的参数(如果我们想编辑一个),一个用于保存我们正在编辑的文章的变量,一个用于保存所有类别,一个用于保存所有标签。我们还添加了一个用于保存当前所选类别和一个用于保存转换为 HTML 的 Markdown 的变量。

    现在,我们需要添加表单;添加以下代码:

    <EditForm Model="Post" OnValidSubmit="SavePost">
        <DataAnnotationsValidator />
        <CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" />
        <InputText @bind-Value="Post.Title"/>
        <ValidationMessage For="()=>Post.Title"/>
        <InputDate @bind-Value="Post.PublishDate"/>
        <ValidationMessage For="()=>Post.PublishDate"/>
        <InputSelect @bind-Value="selectedCategory">
            <option value="0" disabled>None selected</option>
            @foreach (var category in Categories)
            {
                <option value="@category.Id">@category.Name </option>
            }
        </InputSelect>
        <ul>
            @foreach (var tag in Tags)
            {
            <li>
                @tag.Name
                @if (Post.Tags.Any(t => t.Id == tag.Id))
                {
                    <button type="button" @onclick="@(() => {Post.Tags.Remove(Post.Tags.Single(t=>t.Id==tag.Id)); })">Remove</button>
                }
                else
                {
                    <button type="button" @onclick="@(()=> { Post.Tags.Add(tag); })">Add</button>
                }
            </li>
            }
        </ul>
            <InputTextAreaOnInput @bind-Value="Post.Text" @onkeyup="UpdateHTML"/>
            <ValidationMessage For="()=>Post.Text"/>
            <button type="submit" class="btn btn-success">Save</button>
        </EditForm> 
    

    我们添加了一个 EditForm,当我们提交表单(如果它是有效的)时,我们执行 SavePost 方法。我们添加 DataAnnotationValidator,它将验证我们的模型与类中的数据注释。

    我们添加CustomCssClassProvider以确保我们获得正确的 Bootstrap 类名。然后,我们添加标题、发布日期、类别、标签,最后但同样重要的是,文本(博客文章的内容)的组件。

    最后,我们使用在步骤 4中创建的组件(该组件会针对每个按键更新)添加文本。

    我们还将@onkeyup事件连接起来,以便针对每个按键更新预览。

  11. 我们还需要添加我们的SavePost方法。在code部分添加以下代码:

    public async Task SavePost()
    {
        if (!string.IsNullOrEmpty(selectedCategory) && Categories != null)
        {
            var category = Categories.FirstOrDefault(c =>c.Id == selectedCategory);
            if (category != null)
            {
                Post.Category = category;
            }
        }
        await _api.SaveBlogPostAsync(Post);
        _manager.NavigateTo("/admin/blogposts");
    } 
    
  12. 现在,是时候显示预览了。在EditForm关闭标签下方添加以下代码:

    @((MarkupString)markDownAsHTML) 
    

    我们使用MarkupString来确保 Blazor 输出 HTML 代码而不转义字符。你可能还记得这是从第四章理解基本 Blazor 组件中提到的。

  13. 现在,是时候设置Markdig了。在code部分添加以下代码:

    MarkdownPipeline pipeline = default!;
    protected override Task OnInitializedAsync()
    {
        pipeline = new MarkdownPipelineBuilder()
                 .UseEmojiAndSmiley()
                 .Build();
        return base.OnInitializedAsync();
    } 
    

    要配置Markdig,我们需要创建一个管道。如我之前在本章中提到的,这是微软用于他们文档网站的引擎。它有许多扩展可用,包括源代码高亮和表情符号。

    我们还在管道中添加了表情符号,使其更加有趣。

  14. 我们还必须添加代码来加载数据(blog postscategoriestags)。在code部分添加以下方法:

    protected void UpdateHTML()
    {
        markDownAsHTML = Markdig.Markdown.ToHtml(Post.Text, pipeline);
    }
     protected override async Task OnParametersSetAsync()
        {
            if (Id != null)
            {
                var p = await _api.GetBlogPostAsync(Id);
                if (p != null)
                {
                    Post = p;
                    if (Post.Category != null)
                    {
                        selectedCategory = Post.Category.Id;
                    }
                    UpdateHTML();
                }
            }
            Categories = (await _api.GetCategoriesAsync());
            Tags = (await _api.GetTagsAsync());
            base.OnParametersSet();
        } 
    

    现在,运行网站,导航到/admin/blogposts,点击一个博客文章进行编辑,并测试新的 Markdown 支持。图 6.3显示了带有 Markdown 支持的编辑页面:

    计算机屏幕截图  描述自动生成

    图 6.3:带有 Markdown 支持的编辑页面

    我们还有一件事要做:我们需要确保博客文章页面显示 Markdown 的转换后的 HTML 版本。

  15. 打开/Pages/Post.razor并在文件顶部添加以下using语句:

    @using Markdig; 
    
  16. 将以下代码添加到code部分:

    MarkdownPipeline pipeline;
    protected override Task OnInitializedAsync()
    {
        pipeline = new MarkdownPipelineBuilder()
                 .UseEmojiAndSmiley()
                 .Build();
        return base.OnInitializedAsync();
    } 
    
  17. 替换以下行:

    @((MarkupString)BlogPost.Text) 
    

    用以下行替换前面的行:

    @((MarkupString)Markdig.Markdown.ToHtml(BlogPost.Text, pipeline)) 
    

干得好!现在,我们有一个正在运行的行政界面,这样我们就可以开始写博客文章了。

查看我们编写的代码,没有文本框有标签;我们可以做的是在所有使用文本框的地方添加标签。一些组件供应商已经将标签内置到他们的组件中。我更喜欢自己来做这件事。在所有内置组件或第三方组件之上添加一个抽象层。

添加抽象层

这已经帮我们节省了无数次。添加抽象层确实需要一些时间和精力,但我向你保证,你会得到回报。那么,我们为什么要这样做呢?好吧,有几个原因:如果我们使用 Bootstrap,例如,并且我们想要升级到最新版本,可能会有一些类发生了变化。通过使用组件,我们很容易只更改那些组件。如果将来你有自己的组件封装第三方组件,这也使得更改组件供应商变得更容易。但真正的理由是,如果我们添加一层,我们可以设定团队的编程风格/语言。

我们构建的每一项都将具有相同的默认值,相同的属性访问权限,以及相同的用户体验。我们可以添加功能,但在大多数情况下,限制功能更为重要。

第三方组件有很多功能;它们应该满足许多不同的用例。但这也意味着你的团队现在可以访问许多不同的功能,这些功能可以使每个实现功能的开发者的用户体验不同。

让我们在项目中添加一些共享组件。

第一个是一个带有内置标签和验证消息的文本框。

如果我们看看我们的CategoryList组件,代码看起来是这样的:

 <InputText @bind-Value="@Item.Name" />
<ValidationMessage For="@(()=>Item.Name)" /> 

使用 Bootstrap 的标签看起来像这样:

<label for="validationCustomCategoryName" class="form-label">Category name</label>
<div class="input-group has-validation">
<input type="text" class="form-control" id="validationCustomCategoryName">
<div class="invalid-feedback">
        Please choose a category name.
      </div>
</div> 

让我们看看我们是否可以将这些功能结合起来;一些功能已经内置了。由于我们只添加了一层,所以我们不需要处理那么多的功能。我们更需要的是将父组件的值发送到封装的组件。让我们看看一些代码,看看发生了什么:

  1. SharedComponent项目中,在ResusableComponents文件夹中,添加一个新的 Razor 组件,命名为BlogInputText.razor

  2. code部分,添加以下代码:

     [Parameter]
      public string Id { get; set; } = Guid.NewGuid().ToString();
      [Parameter]
      public string? Label { get; set; }
      [CascadingParameter]
      public required EditContext CurrentEditContext { get; set; }
      [Parameter]
      public required string Value { get; set; }
      [Parameter]
      public EventCallback<string> ValueChanged { get; set; }
      [Parameter]
      public required Expression<Func<string>> ValueExpression { get; set; } 
    
  3. 让我们看看发生了什么。首先,我们添加一个参数,以便我们有一个可以用于下一步中label标签的 ID。我们添加一个可以包含标签文本的字符串,如果有的话,我们渲染标签。如果是null,我们不渲染标签。我更喜欢没有“ShowLabel"属性。如果有文本,应该显示标签。我们还有当前的编辑上下文,我们将将其发送到下一级组件。

  4. 在我们的表单中,我们有一个EditFormEditFormEditContext发送到所有子组件,并跟踪表单的状态。我们希望获取这个编辑上下文并将其发送到这个组件内部的所有组件。

  5. 然后,我们有三个值参数,ValueValueChangedValueExpression

  6. 在页面的非代码部分,添加以下内容(替换三个标签):

    @using System.Linq.Expressions
    <CascadingValue Value="CurrentEditContext">
        @if(Label!=null)
        {
            <label for="@Id" class="form-label">@Label</label>
        }
        <InputText id="@Id" Value="@Value" ValueChanged="ValueChanged" ValueExpression="ValueExpression" />
    <ValidationMessage For="@ValueExpression" />
    </CascadingValue> 
    
  7. 首先,我们获取CurrentEditContext并将其发送给子组件;这样,所有子组件都将具有与父EditForm相同的编辑上下文。如果我们有任何文本在Label参数中,我们应该显示标签。然后我们添加InputText,内置组件。如果我们想用第三方库来做这件事,我们会以类似的方式来做。接下来,事情会变得稍微复杂一些;我们本可以说@bind-Value,这将通知 Blazor 发生了变化,但它将通知EditContext我们的组件的Value参数已被更改,而不是模型。

    因此,我们不是这样做,而是将Value参数和ValueChanged参数设置为发送给组件的参数。这样,值更改的通知将直接通知模型已被更改。ValueExpression将确保EditContext被通知更改,并将显示相应的验证消息。说实话,在这个例子中,这并不真的重要,但如果我们使用具有内置验证的第三方组件,它可能不起作用(取决于他们如何构建组件)。因此,使用这种方法应该始终有效。

  8. 然后,我们有ValidationMessage,它显示模型中的任何错误,我们在这里使用相同的ValueExpression

  9. 现在,我们需要使用这个组件。让我们首先更改Taglist。在BlazorWebApp.Client项目中,在Pages/Admin文件夹中,打开Taglist.razor

  10. 现在,我们有以下代码:

    <InputText @bind-Value="@Item.Name" />
    <ValidationMessage For="@(()=>Item.Name)" /> 
    

    将以下代码替换为:

    <BlogInputText @bind-Value="@Item.Name" Label="Name" /> 
    

    现在,难道这不是一种优雅的做法吗?

  11. 让我们用CategoryList做同样的事情。在SharedComponent项目中,在Pages/Admin文件夹中,打开CategoryList.razor

  12. 替换以下代码:

    <InputText @bind-Value="@Item.Name" />
    <ValidationMessage For="@(()=>Item.Name)" /> 
    

    将前面的代码替换为以下代码:

    <BlogInputText @bind-Value="@Item.Name" Label="Name" /> 
    

这种改变让我真正感到高兴——它简化了使用,使 UI 更容易理解,并消除了重复代码的需求。尽管我们现在知道如何做到这一点,但我还想添加一个额外的例子,也许可以展示这种工作方式的真正好处。让我们也创建一个按钮组件。这个组件将更加复杂:

  1. SharedComponents项目中,在ReusableComponents文件夹中,添加一个新的组件,并将其命名为BlogButton.razor

  2. 将内容替换为以下代码:

    @using Microsoft.AspNetCore.Components.Forms
    <button type="@InternalButtonType" disabled="@Disabled" class="@InternalCssClass" title="@Title" @onclick="OnButtonClick">@ChildContent</button> 
    
  3. 我们添加了一个普通的 HTML 按钮,没有太多花哨的功能。我们添加了更改类型(buttonsubmit)、是否禁用、应该具有的 CSS 类、要运行的方法和标题的功能。

  4. 将以下代码添加到code部分:

    [CascadingParameter]
     public EditContext? EditContext { get; set; }
     [Parameter]
     public RenderFragment? ChildContent { get; set; } 
    
  5. 在这里,我们正在做与BlogTextbox相同的事情,并引入了EditContext,我们将在稍后使用它。

  6. 我们还有一个用于按钮内容的RenderFragment

  7. 接下来,添加以下代码:

    `private bool? _disabled = null;
     private string? _disabledHelpText = "";
     private string formerrors = "";
        [Parameter]
        public bool Disabled
        {
            get
            {
                if (_disabled != null && (_disabled == null || _disabled.Value))
                {
                    return _disabled!.Value;
                }
                if (EditContext == null)
                {
                    return false;
                }
                formerrors = "";
                if (!TryGetValidationMessages(out var validationmessages))
                {
                    return true;
                }
                foreach (var m in validationmessages)
                {
                    formerrors += m + (MarkupString)" \r\n";
                }
                return !EditContext.IsModified() || validationmessages.Any();
            }
            set => _disabled = value;
        } 
    
  8. 首先,我们添加几个 private 字段,我们将在组件中使用它们,然后添加一个属性来表示按钮是否被禁用。它将使用 EditContext 来检查表单中是否有任何错误,并将这些错误保存到变量中。如果表单正常,则启用按钮;如果不正常,则禁用按钮。这种实现方式有一个缺点;为了触发验证,我们需要在页面的其他地方点击以触发字段的变化。所以,现在,如果表单不正常,按钮将会被禁用。

  9. 添加以下代码:

    private string? Title => Disabled && !string.IsNullOrWhiteSpace(DisabledHelpText) ? DisabledHelpText : HelpText;
    [Parameter]
    public string? DisabledHelpText { get { return _disabledHelpText + (MarkupString)"\r\n" + formerrors; } set { _disabledHelpText = value; } }
    [Parameter]
    public string? HelpText { get; set; } 
    
  10. 这段代码将为按钮获取一个 Title,当鼠标悬停在按钮上时将显示。我们还可以设置 HelpText 或禁用帮助文本。如果由于任何原因函数被禁用,我们可能不会显示原因,而是显示一个解释为什么按钮被禁用的文本。它还会将任何表单错误添加到按钮上,这样就可以轻松理解哪个表单元素有问题,而无需滚动到该元素。

  11. 有时候,我们可能想使用按钮,但没有表单,只是简单地执行一个方法。添加以下代码:

     [Parameter] public EventCallback OnClick { get; set; }
        private string InternalButtonType => OnClick.HasDelegate ? "button" : "submit";
        private async Task OnButtonClick(EventArgs args)
        {
            if (OnClick.HasDelegate)
            {
                await OnClick.InvokeAsync(args);
            }
        } 
    
  12. 如果我们有一个 OnClick 的代理,我们希望按钮仅仅是一个按钮。如果没有代理,我们假设按钮是在 EditForm 内部使用的。当按钮被点击时,OnButtonClick 方法将会执行。

  13. 现在,我们来到了真正有趣的部分。让我们添加一个 enum;我们可以在 code 部分添加它:

    public enum ButtonType
    {
        Save,
        Cancel,
        Delete,
        Remove,
        Select
    } 
    
  14. 注意,我们没有使用像“主要”或“危险”这样的词汇——那是 Bootstrap 的术语。我们想知道按钮的用途。当我们添加一个按钮时,最可能的使用场景是什么?

  15. 然后,我们添加一个 ButtonType 的参数,如下所示:

    [Parameter] public ButtonType Type { get; set; }
    private string InternalCssClass
    {
        get
        {
            return Type switch
            {
                ButtonType.Save => "btn btn-success",
                ButtonType.Cancel => "btn btn-danger",
                ButtonType.Delete => "btn btn-danger",
                ButtonType.Remove => "btn btn-danger",
                ButtonType.Select => "btn btn-primary",
                _ => "btn btn-primary"
            };
        }
    } 
    

    我们添加一个 ButtonType 参数和一个内部属性,将“保存”用例等价转换为 Bootstrap CSS 类。

  16. 我们团队不需要费心去记住应该使用哪个 Bootstrap 类;他们知道这是一个按钮,也知道按钮的用途。组件会处理其余部分。

    让我们测试一下!

  17. BlazorWebApp 项目中,在 Pages/Admin 文件夹中,打开 TagList.razor

  18. 替换以下行:

    <button class="btn btn-success" type="submit">Save</button> 
    

    替换前面的行为以下行:

    <BlogButton Type="BlogButton.ButtonType.Save">Save</BlogButton> 
    
  19. 如果你现在运行项目,你会看到如果我们没有对表单进行任何更改,按钮将会被禁用;如果我们向文本框中添加内容,按钮将会变为启用状态。

  20. 让我们对 CategoryList 也做同样的处理。在 SharedComponents 项目中,在 Pages/Admin 文件夹中,打开 CategoryList.razor

  21. 替换以下行:

    <button class="btn btn-success" type="submit">Save</button> 
    

    替换前面的行为以下行:

    <BlogButton Type="BlogButton.ButtonType.Save">Save</BlogButton> 
    

我们还可以在更多地方修改并添加这个按钮,但我们现在先不花时间在这个上面。如果你想,你可以回到这里并确保我们在每个地方都使用了新的按钮和 InputText

我们还有一个组件需要构建。

锁定导航

在 .NET 7 中,我们得到了一个新的组件,称为 NavigationLock。目前,如果我们写一篇博客文章并在菜单中点击某个地方,我们的更改将会丢失。如果我们更改 URL 并按 Enter,也会发生同样的事情。使用 NavigationLock,我们可以防止这种情况发生。

NavigationLock 可以防止我们从页面离开并导航到我们网站上的另一个页面。在这种情况下,我们可以使用 JavaScript 显示自定义消息。如果我们导航到另一个网站,它可以触发一个警告,但我们无法控制显示的消息。此功能是内置在浏览器中的。

我们将以与 FieldCssClassProvider 相同的方式实现此功能,作为一个可重用组件。我们想检查我们的 EditContext 是否有任何更改,以便我们可以触发导航锁定:

  1. SharedComponents 项目中,在 ReusableComponents 文件夹中,添加一个新的 Razor 组件,并将其命名为 BlogNavigationLock.razor

  2. 在组件顶部添加以下代码:

    @using Microsoft.AspNetCore.Components.Forms
    @using Microsoft.AspNetCore.Components.Routing
    @using Microsoft.JSInterop @inject IJSRuntime JSRuntime
    @implements IDisposable 
    

    我们注入一个 IJSRuntime 来调用 JavaScript。我们将在第十章,JavaScript 互操作中返回 JavaScript 互操作。

    我们还实现了 IDisposable 接口。

  3. code 部分中,添加以下代码:

    [CascadingParameter]
    public required EditContext CurrentEditContext { get; set; }
    public string InternalNavigationMessage { get; set; } = "You are about to loose changes, are you sure you want to navigate away?"; public bool CheckNavigation { get; set; } = true; 
    

    我们有一个 CascadingParameter,它获取当前的 EditContext,就像我们使用 FieldCssClassProvider 一样。

    我们还添加了一个字符串,这是我们尝试从页面导航时显示的消息。

  4. EditContext 发生更改时,我们需要更新组件并确保它锁定导航。添加以下代码:

    protected override Task OnInitializedAsync()
    {
        CurrentEditContext.OnFieldChanged += OnFieldChangedAsync;
        return base.OnInitializedAsync();
    }
    private async void OnFieldChangedAsync(object? Sender,FieldChangedEventArgs args)
        {
            await InvokeAsync(StateHasChanged);
        }
    void Idisposable.Dispose()
    {
            CurrentEditContext.OnFieldChanged -= OnFieldChangedAsync;
        } 
    

    我们开始监听字段更改,如果字段发生变化,我们调用 StateHasChanged 方法来更新组件。

    由于调用来自另一个线程,需要 InvokeAsync

    我们还重写了 Dispose 方法并移除了事件监听器。

  5. code 部分中,添加以下代码:

    private async Task OnBeforeInternalNavigation (LocationChangingContext context)
    {
        if (CurrentEditContext.IsModified() && CheckNavigation)
        {
            var isConfirmed = await JSRuntime.InvokeAsync<bool>("confirm",
                InternalNavigationMessage);
            if (!isConfirmed)
            {
                context.PreventNavigation();
            }
        }
    } 
    

    如果在 EditContext(或模型)中发生变化,此方法将调用 JavaScript,显示确认对话框和我们所添加的消息。如果我们不确认,导航将被阻止。

  6. 现在,我们可以添加 NavigationLock 组件。在指令下方添加以下代码:

    <NavigationLock ConfirmExternalNavigation="@(CurrentEditContext.IsModified() && CheckNavigation)" OnBeforeInternalNavigation="OnBeforeInternalNavigation" /> 
    

    NavigationLock 组件将阻止外部导航(导航到另一个网站)和内部导航(在我们的博客中导航到另一个页面)。它检查 EditContext(模型)是否有任何更改,并阻止外部导航。在内部导航时,它将执行 OnBeforeInternalNavigation 方法,该方法检查 EditContext 是否已更改。

    现在,我们只剩下一件事要做。

  7. Pages/Admin/BlogPostEdit.razor 中,在我们创建的新 Razor 组件 CustomCssClassProvider 下方添加:

    <BlogNavigationLock @ref="NavigationLock"/> 
    

    这将从级联值中获取 EditContext,并执行我们刚刚编写的代码。

    添加以下内容:

    @using SharedComponents.ReusableComponents 
    
  8. code 部分中,添加以下内容:

    BlogNavigationLock? NavigationLock { get; set; } 
    
  9. SavePostAsync 方法中,在导航到 admin/blogposts 之前,添加以下内容:

    NavigationLock?.CurrentEditContext.MarkAsUnmodified(); 
    

    当保存对象时,EditContext 并不知道这一点,所以我们正在告诉 EditContext 模型现在没有修改,因此导航不应该停止。

  10. 运行网站,导航到 Admin/BlogPosts,然后点击一篇博客文章。

  11. 尝试导航到另一个网站(应该可以工作)。

  12. 尝试导航到另一个页面(应该可以工作)。

  13. 修改博客文章。

  14. 尝试导航到另一个网站(应该会显示一个消息框)。

  15. 尝试导航到另一个页面。你可能会注意到,在我们的情况下,当我们导航到另一个页面时,它不会显示一个消息框。这是怎么回事?这似乎是这个组件工作方式的一个限制。如果我们运行 InteractiveServerInteractiveWebAssembly,它就会工作。使用静态服务器端渲染的导航(这就是我们导航时发生的情况)不会触发导航更改。如果我们想测试这一点,我们可以将我们的项目更改为以 Blazor 服务器模式运行。

  16. BlazorWebApp 项目中,在 Components 文件夹中,打开 App.razor 文件。

    将以下行替换:

    <Routes  /> 
    

    将上一行替换为以下行:

    <Routes @rendermode="RenderMode.InteractiveServer" /> 
    
  17. 现在我们可以再次尝试:

    1. 运行网站,导航到 Admin/BlogPosts,然后点击一篇博客文章。

    2. 尝试导航到另一个网站(应该可以工作)。

    3. 尝试导航到另一个页面(应该可以工作)。

    4. 修改博客文章。

    5. 尝试导航到另一个网站(应该会显示一个消息框)。

    6. 尝试导航到另一个页面(应该会显示一个消息框)。

    现在,将其改回 <Routes />

这样做的目的是为了展示在某些情况下,内置组件的行为会根据渲染模式的不同而有所不同。我真诚地认为这与其说是一个特性,不如说是一个错误,但关于这一点有一些讨论。

太棒了!我们实现了另一个可重用组件。接下来,让我们看看如何使用增强表单导航来使用静态服务器端渲染组件。

增强表单导航

在 .NET 8 中,我们得到了服务器端渲染。正如我们所见,向组件添加交互性很简单。但有时我们只想有一个表单和一个提交按钮。我们真的需要启用 WebAssembly 或 SignalR 连接来做到这一点吗?我很高兴你问了!答案是,不需要。

让我们添加一个组件来展示我们的博客文章需要评论:

  1. SharedComponents 项目中,在 Pages 文件夹中,添加一个名为 Comments.razor 的新 Razor 组件。这个组件应该做两件事:列出评论和创建新的评论。

  2. comments 文件中,将内容替换为以下内容:

    @using SharedComponents.ReusableComponents
    @using Microsoft.AspNetCore.Components.Forms
    @inject IBlogApi _api
    <h3>Comments</h3>
    @foreach (var c in comments)
    {
        <div class="media mb-4">
    <div class="media-body">
    <h5 class="mt-0">@c.Name</h5>
    <p>@c.Text</p>
    <small class="text-muted">@c.Date</small>
    </div>
    </div>
    } 
    

    这是一个评论列表和一些用于使其看起来更好的 Bootstrap 类。

  3. 继续添加表单:

    @if (Model != null)
    {
        <EditForm method="post" Model="@Model" OnValidSubmit="@ValidSubmitAsync" FormName="CommentForm">
    <DataAnnotationsValidator />
    <CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" />
    <BlogInputText @bind-Value="Model.Name" Label="Name" />
    <BlogInputText @bind-Value="Model.Text" Label="Comment" />
    <button type="submit">Add Comment</button>
    </EditForm>
    } 
    
  4. 我们仍然像之前一样使用 EditFormOnValidSubmit 属性。尽管如此,这里也有一些新内容。我们指定了用于提交表单的方法——在这种情况下,是一个 POST 方法。我们还使用 FormName 参数命名表单。这两个属性必须存在。我们使用了之前创建的 DataAnnotationValidatorCustomCssClassProvider。我们还使用了 BlogInputText。但由于这个组件不是交互式的,我们的按钮默认是禁用的,如果我们移除交互性,它将保持禁用状态。因此,在这种情况下,我们必须以传统方式添加一个按钮。我们可以确保在这种情况下也有一个非交互式按钮。

    现在是时候编写表单的代码部分了:

    @code {
        [Parameter,EditorRequired]
        public required string BlogPostId { get; set; }
        [SupplyParameterFromForm]
        public Comment? Model { get; set; } = new();
        List<Comment> comments = new();
        protected override async Task OnInitializedAsync()
        {
            comments = await _api.GetCommentsAsync(BlogPostId);
        }
        public async Task ValidSubmitAsync()
        {
            Model.Id = Guid.NewGuid().ToString();
            Model.Date = DateTime.Now;
            Model.BlogPostId = BlogPostId;
            await _api.SaveCommentAsync(Model);
            comments = await _api.GetCommentsAsync(BlogPostId);
        }
    } 
    
  5. 在这里,我们使用一个参数,以便我们的组件知道要显示评论的博客文章。它有 EditorRequired 属性,因此如果缺少它,Visual Studio 将会警告你。Model 参数具有 SupplyParameterFromForm 属性,这是必需的。这样 Blazor 就知道在表单提交时用数据填充哪个属性。其余的代码与之前我们使用的代码相同。

  6. 打开 Post.cs 文件,并在以下代码行下方添加以下代码:@((MarkupString)Markdig.Markdown.ToHtml(BlogPost.Text, pipeline)):

    <Comments BlogPostId="@BlogPostId" /> 
    

    那么,发生了什么?

    当我们提交表单时,组件将被重新渲染,创建组件的新实例,并将(在我们的案例中)Model 参数填充为帖子的数据。对于交互式组件,OnInitializedOnInitializedAsync 只会运行一次(除非我们在运行预渲染)。对于这些静态组件,它将重新加载组件。但我们也遇到了一个问题,因为当页面重新加载时,滚动位置会丢失。我们现在会发现自己处于页面的顶部,这不是一个好的用户体验。幸运的是,有一个解决方案;这就是 Enhance 部分发挥作用的地方。通过在我们的表单中添加 Enhance,它现在在提交页面后会保持其滚动位置。非常酷,对吧?我们也可以使用普通表单而不是 EditForm

    它看起来可能像这样:

    <form method="post" @onsubmit="…" @formname="name" data-enhance> 
    

    我们不是添加 Enhance,而是添加 data-enhance。我个人更喜欢在可能的情况下使用 EditForm,但了解还有其他选项可能是个好主意。

    我们还可以使用 data-permanent 来保持表单字段中的信息,例如用于搜索参数。这样,增强导航在响应返回时不会更新那些数据。我们有多少次使用搜索字段,拼写错误,然后搜索字段为空,我们需要重新输入所有内容?这就是 data-permanent 帮助我们的地方。

太棒了!

这章内容很丰富,但哇,我们做了很多——一大堆新的可重用组件和完整的管理员界面。

摘要

这章教会了我们如何创建表单并调用 API 来获取和保存数据。

我们构建了自定义输入控件,并给我们的控件添加了 Bootstrap 样式。大多数商业应用程序都使用表单;我们可以通过注释数据来在数据附近添加逻辑。

我们还创建了多个可重用组件,并使用了之前章节中讨论的许多内容。我们甚至触及了 JavaScript 互操作性的话题,我们将在第十章JavaScript 互操作性中对其进行更详细的介绍。

Blazor 在验证和输入控件方面的功能将帮助我们构建令人惊叹的应用程序,并为我们的用户提供极佳的体验。你可能已经注意到,目前管理页面是开放的。我们需要通过登录来保护我们的博客,但这一点将在第八章认证和授权中详细说明。

在下一章中,我们将创建一个 Web API,以便在运行InteractiveAutoInteractiveWebAssembly等组件时获取数据。

第七章:创建 API

当使用 WebAssembly 运行 Blazor(InteractiveWebAssembly 或 InteractiveAuto)时,我们需要能够检索数据并更改我们的数据。为了实现这一点,我们需要一个 API 来访问数据。在本章中,我们将使用 Minimal API 创建一个 Web API。

当使用 Blazor Server 时,API 将由页面(如果我们添加 Authorize 属性)进行保护,因此我们免费获得这个功能。但是,与 WebAssembly 一起使用时,所有操作都将执行在浏览器中,因此我们需要 WebAssembly 可以与之通信以更新服务器上的数据。

要做到这一点,我们需要涵盖以下主题:

  • 创建服务

  • 创建客户端

技术要求

确保您已经阅读了前面的章节或使用 Chapter06 文件夹作为起点。

您可以在此处找到本章最终结果的源代码:github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter07

创建服务

创建服务有多种方式,例如通过 REST。

对于那些之前没有使用过 REST 的人来说,REST 代表 representational state transfer。简单来说,它是一种机器使用 HTTP 与其他设备通信的方式。

在 REST 中,我们使用不同的 HTTP 动词来执行不同的操作。它们可能看起来像这样:

URI Verb Action
/BlogPosts Get 获取博客文章列表
/BlogPosts Post 创建新的博客文章
/BlogPosts/{id} Get 获取具有特定 ID 的博客文章
/BlogPosts/{id} Put 替换博客文章
/BlogPosts/{id} Patch 更新博客文章
/BlogPosts/{id} Delete 删除博客文章

表 7.1:REST 调用

我们将为 标签类别博客文章实现 API。

由于 API 负责处理 Post 是否应该被创建,我们将作弊并仅实现 Put(替换),因为我们不知道我们是创建还是更新数据。

我们将在 BlazorWebApp 项目中实现 API。

了解 Minimal API

在我们深入实现 Minimal API 之前,让我们花点时间了解一下它。回到 2019 年 11 月,分布式应用运行时Dapr)团队的一名成员编写了几个教程,介绍了如何使用不同的语言构建分布式计算器。

他们提供了使用 Go、Python、Node.js 和 .NET Core 的示例。代码展示了在 C# 中编写分布式计算器比在其他语言中要困难得多。

微软询问了各种非 .NET 开发者他们对 C# 的看法。他们的回应并不理想。然后,微软让他们使用 Minimal APIs 的早期版本完成一个教程。

在教程之后,他们被询问他们的看法,他们的回应发生了变化,现在更加积极;感觉就像在家一样。

最小 API 的目标是减少复杂性和仪式,拥抱简约。我以为“最小”意味着我无法做所有事情,但深入代码后,我很快意识到并非如此。

从我的角度来看,最小 API 是编写 API 的更好方式。其理念是,如果我们需要,我们可以扩展我们的 API,一旦我们觉得合适,我们可以将代码移动到控制器中以获得更多结构。在我的工作场所,我们转向了最小 API,因为我们认为语法更简洁。

添加最小 API 的一个非常简单的示例就是在 Program.cs 中添加这一行:

app.MapGet("/api/helloworld", () => "Hello world!"); 

如果我们导航到一个没有指定任何路由的 URL,只是 "/",我们将返回一个包含 "Hello World" 的字符串。

当然,这是一个可能的简单示例,但也可以实现更复杂的事情,正如我们将在下一节中看到的那样。

添加 API 控制器

我们有三个数据模型:博客文章、标签和分类。

让我们创建三个不同的文件,每个数据模型一个,以展示使用最小 API 添加更复杂 API 的友好方式。对于一个小项目,可能更合理的是在 Program.cs 中添加所有内容。

添加处理博客文章的 API

让我们先添加处理博客文章的 API 方法。

执行以下步骤以创建 API:

  1. BlazorWebApp 项目中,添加一个名为 Endpoints 的新文件夹。

  2. Endpoints 文件夹中,创建一个名为 BlogPostEndpoints.cs 的类。目的是创建一个扩展方法,我们可以在 Program.cs 中稍后使用。

    在文件顶部添加这些 using 语句:

    using Data.Models;
    using Data.Models.Interfaces;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc; 
    
  3. 将类替换为以下代码:

    public static class BlogPostEndpoints
    {
        public static void MapBlogPostApi(this WebApplication app)
        {
            app.MapGet("/api/BlogPosts",
            async (IBlogApi api, [FromQuery] int numberofposts, [FromQuery] int startindex) =>
            {
                return Results.Ok(await api.GetBlogPostsAsync(numberofposts, startindex));
            });
           }
    } 
    

    由于我们正在创建一个扩展方法,我们必须确保类是静态的。MapBlogPostApi 方法使用 this 关键字,这使得该方法在任何 WebApplication 类上都可用。

    我们通过使用 MapGet 和一个路径来设置最小 API,这意味着如果使用 Get 动词通过正确的参数访问该路径,该方法将运行。

    该方法接受几个参数。第一个是 IBlogApi 类型,它将使用依赖注入来获取我们需要的类的实例,在这种情况下,是 BlogApiJsonDirectAccess,它将访问我们存储的 JSON 文件。

    其他参数将使用查询字符串(因为我们使用的是 query 属性);在大多数情况下,最小 API 会自动处理这些事情,但引导它走向正确的方向从不会错。

    我们创建了一个直接从数据库返回数据的方法(与 Blazor 服务器项目使用的相同 API)。

    我们还需要确保从 Program.cs 中调用它。

  4. Program.cs 中添加以下命名空间:

    using BlazorWebApp.Endpoints; 
    
  5. 还在 app.Run(); 之上添加以下代码:

    app.MapBlogPostApi(); 
    
  6. 是时候测试 API 了;确保启动 BlazorWebApp 项目。在 .NET 6 中,端口号是随机的,所以将 {REPLACEWITHYOURPORTNUMBER} 替换为你的项目端口号。

    请访问以下网址:https://localhost:{REPLACEWITHYOURPORTNUMBER}/Api/BlogPosts?numberofposts=10&startindex=0(端口号可能不同)。我们将获取一些 JSON 返回,其中包含我们的博客文章列表。

    我们已经取得了良好的开端!现在,我们需要实现 API 的其余部分。

  7. Endpoints/BlogPostEndpoint.cs文件中,在MapBlogPostApi方法中,让我们添加获取博客文章计数的代码:

    app.MapGet("/api/BlogPostCount",
    async (IBlogApi api) =>
    {
        return Results.Ok(await api.GetBlogPostCountAsync());
    }); 
    

    我们使用Get动词,但使用另一个路由。

  8. 我们还需要能够获取一篇博客文章。添加以下代码:

    app.MapGet("/api/BlogPosts/{*id}",
    async (IBlogApi api, string id) =>
    {
        return Results.Ok(await api.GetBlogPostAsync(id));
    }); 
    

    在这种情况下,我们使用Get动词,但使用另一个包含我们想要获取的Post ID 的 URL。

    我们使用一个字符串作为 ID,例如,像 RavenDB 这样的数据库使用类似这样的 ID:CollectionName/IdOfThePost;我们还确保在参数中添加*。这样,它将使用任何跟在后面的内容作为 ID,否则它将把斜杠解释为路由的一部分,而找不到端点。

    接下来,我们需要一个受保护的 API,通常是更新或删除内容的 API。

  9. 让我们添加一个保存博客文章的 API。在刚刚添加的代码下方添加以下代码:

    app.MapPut("/api/BlogPosts",
    async (IBlogApi api, [FromBody] BlogPost item) =>
    {
        return Results.Ok(await api.SaveBlogPostAsync(item));
    }).RequireAuthorization(); 
    

    如我在本章前面提到的,我们只会添加一个用于创建和更新博客文章的 API,并且我们将使用Put动词(替换)来完成这个操作。我们在最后添加了RequireAuthorization方法,这将确保用户需要经过身份验证才能调用该方法。

  10. 接下来,我们添加删除博客文章的代码。为此,添加以下代码:

    app.MapDelete("/api/BlogPosts/{*id}",
    async (IBlogApi api, string id) =>
    {
        await api.DeleteBlogPostAsync(id);
        return Results.Ok();
    }).RequireAuthorization(); 
    

在这种情况下,我们使用Delete动词,就像保存一样,我们在最后添加了RequireAuthorization方法。

接下来,我们还需要对CategoriesTags进行同样的操作。

添加处理类别的 API

让我们从Categories开始。按照以下步骤操作:

  1. Endpoints文件夹中,添加一个名为CategoryEndpoints.cs的新类。用以下代码替换代码:

    using Data.Models;
    using Data.Models.Interfaces;
    using Microsoft.AspNetCore.Mvc;
    namespace BlazorWebApp.Endpoints;
    public static class CategoryEndpoints
    {
        public static void MapCategoryApi(this WebApplication app)
        {
            app.MapGet("/api/Categories",
            async (IBlogApi api) =>
            {
                return Results.Ok(await api.GetCategoriesAsync());
            });
            app.MapGet("/api/Categories/{*id}",
            async (IBlogApi api, string id) =>
            {
                return Results.Ok(await api.GetCategoryAsync(id));
            });
            app.MapPut("/api/Categories",
            async (IBlogApi api, [FromBody] Category item) =>
            {
                return Results.Ok(await api.SaveCategoryAsync(item));
            }).RequireAuthorization();
            app.MapDelete("/api/Categories/{*id}",
            async (IBlogApi api, string id) =>
            {
                await api.DeleteCategoryAsync(id);
                return Results.Ok();
            }).RequireAuthorization();
        }
    } 
    
  2. Program.cs中,在app.Run()上方添加以下代码:

    app.MapCategoryApi(); 
    

这些都是处理Categories所需的所有方法。

接下来,让我们用Tags做同样的事情。

添加处理标签的 API

让我们按照以下步骤为标签做同样的事情:

  1. Endpoints文件夹中,添加一个名为TagEndpoints.cs的新类。添加以下代码:

    using Data.Models;
    using Data.Models.Interfaces;
    using Microsoft.AspNetCore.Mvc;
    namespace BlazorWebApp.Endpoints;
    public static class TagEndpoints
    {
        public static void MapTagApi(this WebApplication app)
        {
            app.MapGet("/api/Tags",
            async (IBlogApi api) =>
            {
                return Results.Ok(await api.GetTagsAsync());
            });
            app.MapGet("/api/Tags/{*id}",
            async (IBlogApi api, string id) =>
            {
                return Results.Ok(await api.GetTagAsync(id));
            });
            app.MapPut("/api/Tags",
            async (IBlogApi api, [FromBody] Tag item) =>
            {
                return Results.Ok(await api.SaveTagAsync(item));
            }).RequireAuthorization();          app.MapDelete("/api/Tags/{*id}",
            async (IBlogApi api, string id) =>
            {
                await api.DeleteTagAsync(id);
                return Results.Ok();
            }).RequireAuthorization();
        }
    } 
    
  2. Program.cs中,在app.Run()上方添加以下代码:

    app.MapTagApi(); 
    

但是等等!关于评论怎么办?我们实现评论的方式意味着该组件永远不会作为 WebAssembly 运行,所以我们实际上不需要在 API 中实现它。但我们不会让评论悬而未决——让我们也实现这些评论!

添加处理评论的 API

让我们按照以下步骤为评论做同样的事情:

  1. Endpoints文件夹中,添加一个名为CommentEndpoints.cs的新类。添加以下代码:

    using Data.Models;
    using Data.Models.Interfaces;
    using Microsoft.AspNetCore.Mvc;
    namespace BlazorWebApp.Endpoints;
    public static class CommentEndpoints
    {
        public static void MapCommentApi(this WebApplication app)
        {
            app.MapGet("/api/Comments/{*blogPostid}",
            async (IBlogApi api, string blogPostid) =>
            {
                return Results.Ok(await api.GetCommentsAsync(blogPostid));
            });
            }).RequireAuthorization();
            app.MapPut("/api/Comments",
            async (IBlogApi api, [FromBody] Comment item) =>
            {
                return Results.Ok(await api.SaveCommentAsync(item));
            }).RequireAuthorization();
            app.MapDelete("/api/Comments/{*id}",
            async (IBlogApi api, string id) =>
            {
                await api.DeleteCommentAsync(id);
                return Results.Ok();
        }
    } 
    
  2. Program.cs中,在app.Run()上方添加以下代码:

    app.MapCommentApi(); 
    

太好了!我们有了 API!现在,是时候创建一个客户端来访问这个 API 了。

创建客户端

为了访问 API,我们需要创建一个客户端。有多种方法可以做到这一点,但我们将以最简单的方式通过编写代码来实现。

客户端将实现相同的IBlogApi接口。这样,无论我们使用哪种实现,代码都是相同的,并且可以直接通过BlogApiJsonDirectAccessBlogApiWebClient(我们接下来将要创建的)进行 JSON 访问:

  1. BlazorWebApp.Client下的Dependencies节点上右键点击,并选择Manage NuGet Packages

  2. 搜索Microsoft.AspNetCore.Components.WebAssembly.Authentication并点击Install

  3. 此外,搜索Microsoft.Extensions.Http并点击Install

  4. BlazorWebApp.Client项目中,在项目根目录下添加一个新的类,并将其命名为BlogApiWebClient.cs

  5. 打开新创建的文件并添加以下命名空间:

    using Data.Models;
    using Data.Models.Interfaces;
    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    using System.Net.Http.Json;
    using System.Text.Json; 
    
  6. IBlogApi添加到类中,并使其公开,如下所示:

    namespace BlazorWebApp.Client;
    public class BlogApiWebClient : IBlogApi
    {
    } 
    
  7. 一些 API 调用将是公开的(不需要认证),但HttpClient将被配置为需要令牌。

    令牌的处理由 Blazor 自动处理,所以我们只需要一个客户端,在这个案例中,我们将其称为Api

    为了能够调用 API,我们需要注入HttpClient。将以下代码添加到类中:

     private readonly IHttpClientFactory _factory;
        public BlogApiWebClient(IHttpClientFactory factory)
        {
            _factory = factory;
        } 
    
  8. 现在,是时候实现对 API 的调用了。让我们从博客文章的Get调用开始。添加以下代码:

    public async Task<BlogPost?> GetBlogPostAsync(string id)
        {
            var httpclient = _factory.CreateClient("Api");
            return await httpclient.GetFromJsonAsync<BlogPost>($"api/BlogPosts/{id}");
        }
        public async Task<int> GetBlogPostCountAsync()
        {
            var httpclient = _factory.CreateClient("Api");
            return await httpclient.GetFromJsonAsync<int>("/api/BlogPostCount");
        }
        public async Task<List<BlogPost>?> GetBlogPostsAsync(int numberofposts, int startindex)
        {
            var httpclient = _factory.CreateClient("Api");
            return await httpclient.GetFromJsonAsync<List<BlogPost>>($"/api/BlogPosts?numberofposts={numberofposts}&startindex={startindex}");
        } 
    

    我们使用注入的HttpClient,然后调用GetFromJsonAsync,这将自动下载 JSON 并将其转换为提供给泛型方法的类。

    现在,事情变得有点复杂:我们需要处理认证。幸运的是,这已经内置在HttpClient中,所以我们只需要处理AccessTokenNotAvailableException。如果令牌缺失,它将自动尝试更新它,但如果出现问题(例如,用户未登录),我们可以重定向到登录页面。

    我们将在第八章认证和授权中回到令牌和认证的工作方式。

  9. 接下来,我们添加需要认证的 API 调用,例如保存或删除博客文章。

    在我们刚刚添加的代码下方添加以下代码:

    public async Task<BlogPost?> SaveBlogPostAsync(BlogPost item)
    {
        try
        {
            var httpclient = _factory.CreateClient("Api");
            var response = await httpclient.PutAsJsonAsync<BlogPost>
               ("api/BlogPosts", item);
            var json = await response.Content.ReadAsStringAsync();
            return JsonSerializer.Deserialize<BlogPost>(json);
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
        return null;
    }
    public async Task DeleteBlogPostAsync(string id)
    {
        try
        {
            var httpclient = _factory.CreateClient("Api");
            await httpclient.DeleteAsync($"api/BlogPosts/{id}");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    } 
    

    如果调用抛出AccessTokenNotAvailableException,这意味着HttpClient无法自动获取或更新令牌,用户需要登录。

    这种状态可能永远不会发生,因为我们将会确保当用户导航到那个页面时,他们需要登录,但防患于未然总是好的。

  10. 现在,我们需要为Categories做同样的事情。将以下代码添加到BlogApiWebClient类中:

    public async Task<List<Category>?> GetCategoriesAsync()
    {
        var httpclient = _factory.CreateClient("Api");
        return await httpclient.GetFromJsonAsync<List<Category>>($"api/Categories");
    }
    public async Task<Category?> GetCategoryAsync(string id)
    {
        var httpclient = _factory.CreateClient("Api");
        return await httpclient.GetFromJsonAsync<Category>($"api/Categories/{id}");
    }
    public async Task DeleteCategoryAsync(string id)
    {
        try
        {
            var httpclient = _factory.CreateClient("Api");
            await httpclient.DeleteAsync($"api/Categories/{id}");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
    public async Task<Category?> SaveCategoryAsync(Category item)
    {
        try
        {
            var httpclient = _factory.CreateClient("Api");
            var response = await httpclient.PutAsJsonAsync<Category>("api/Categories", item);
            var json = await response.Content.ReadAsStringAsync();
            return JsonSerializer.Deserialize<Category>(json);
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
        return null;
    } 
    
  11. 接下来,我们将为Tags做同样的事情。将以下代码添加到我们刚刚添加的代码下方:

    public async Task<Tag?> GetTagAsync(string id)
    {
        var httpclient = _factory.CreateClient("Api");
        return await httpclient.GetFromJsonAsync<Tag>($"api/Tags/{id}");
    }
    public async Task<List<Tag>?> GetTagsAsync()
    {
        var httpclient = _factory.CreateClient("Api");
        return await httpclient.GetFromJsonAsync<List<Tag>>($"api/Tags");
    }
    public async Task DeleteTagAsync(string id)
    {
        try
        {
            var httpclient = _factory.CreateClient("Api");
            await httpclient.DeleteAsync($"api/Tags/{id}");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
    public async Task<Tag?> SaveTagAsync(Tag item)
    {
        try
        {
            var httpclient = _factory.CreateClient("Api");
            var response = await httpclient.PutAsJsonAsync<Tag>("api/Tags", item);
            var json = await response.Content.ReadAsStringAsync();
            return JsonSerializer.Deserialize<Tag>(json);
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
        return null;
    } 
    
  12. 让我们不要忘记我们的评论!将以下代码添加到我们刚刚添加的代码下方:

    public async Task<List<Comment>> GetCommentsAsync(string blogpostid)
        {
            var httpclient = _factory.CreateClient("Api");
            return await httpclient.GetFromJsonAsync<List<Comment>>($"api/Comments/{blogpostid}");
        }
    
        public async Task DeleteCommentAsync(string id)
        {
            try
            {
                var httpclient = _factory.CreateClient("Api");
                await httpclient.DeleteAsync($"api/Comments/{id}");
            }
            catch (AccessTokenNotAvailableException exception)
            {
                exception.Redirect();
            }
        }
        public async Task<Comment?> SaveCommentAsync(Comment item)
        {
            try
            {
                var httpclient = _factory.CreateClient("Api");
                var response = await httpclient.PutAsJsonAsync<Comment>("api/Comments", item);
                var json = await response.Content.ReadAsStringAsync();
                return JsonSerializer.Deserialize<Comment>(json);
            }
            catch (AccessTokenNotAvailableException exception)
            {
                exception.Redirect();
            }
            return null;
        } 
    

干得好!我们的 API 客户端现在已经完成了!

摘要

在本章中,我们学习了如何使用 Minimal APIs 和 API 客户端创建一个 API,这是大多数应用程序的重要组成部分。这样,我们就可以从数据库中获取博客文章,并在运行在 WebAssembly 时展示它们。值得一提的是,我们始终可以使用 Web API 来运行我们的应用程序;这只是为了展示我们可以根据当前使用的托管模型使用不同的方式来访问我们的数据。

在下一章中,我们将向我们的网站添加登录功能,并首次调用我们的 API。

第八章:身份验证和授权

在本章中,我们将学习如何将 身份验证授权 添加到我们的博客中,因为我们不希望任何人都能创建或编辑博客文章。

覆盖身份验证和授权可能需要一整本书,所以在这里我们将保持简单。本章的目标是让内置的身份验证和授权功能正常工作,基于已经内置到 ASP.NET 中的功能。这意味着这里没有太多 Blazor 魔法;已经存在许多我们可以利用的资源。

几乎每个系统今天都有一种登录方式,无论是管理员界面(如我们的)还是成员登录门户。有许多不同的登录提供者,如 Google、Twitter 和 Microsoft。我们可以使用所有这些提供者,因为我们只是在现有的架构上构建。

一些网站可能已经有一个用于存储登录凭证的数据库,但对我们博客来说,我们将使用名为 Auth0 的服务来管理我们的用户。这是一种非常强大的方式来添加许多不同的社交提供者(如果我们想的话),而且我们不必自己管理用户。

我们可以在创建项目时选择添加身份验证的选项。当涉及到 Blazor 服务器、Blazor WebAssembly 和 API 时,身份验证的工作方式不同,我们将在本章中更详细地探讨。

在本章中,我们将涵盖以下主题:

  • 设置身份验证

  • 保护 Blazor 服务器

  • 保护 Blazor WebAssembly

  • 保护 API

  • 添加授权

技术要求

确保你已经遵循了前面的章节,或者以 Chapter07 文件夹作为起点。

你可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter08 找到本章最终结果的源代码。

设置身份验证

在身份验证方面有许多内置功能。添加身份验证的最简单方法是创建项目时选择身份验证选项。

我们需要分别对 Blazor 服务器项目和 Blazor WebAssembly 项目实现身份验证,因为它们的工作方式不同。

但我们仍然可以在这两个项目之间共享一些东西。首先,我们需要设置 Auth0。

Auth0 是一种服务,可以帮助我们处理用户。有许多不同的服务,但 Auth0 对我们来说是一个很好的选择。我们可以连接一个或多个社交连接器,这将允许我们的用户使用 Facebook、Twitter、Twitch 或我们在网站上添加的任何其他服务进行登录。

尽管所有这些都可以通过我们自己编写代码来实现,但这种集成方式是快速添加身份验证并获得非常强大解决方案的绝佳方式。此外,身份验证很复杂,所以除非你确定自己在做什么,否则不要编写它。Auth0 对最多 7,000 个用户免费(我们博客可能达不到这个数字,尤其是管理员界面)。

它还具有添加我们有权访问的用户数据的强大功能。我们将在本章后面添加用户角色时进行此操作。你需要执行以下步骤:

  1. 访问 auth0.com 并创建一个账户。

  2. 点击创建应用程序按钮。

  3. 现在,是时候给我们的应用程序命名了。例如使用 MyBlog。然后,是时候选择我们正在使用哪种应用程序类型了。是原生应用程序吗?是单页 Web 应用程序常规 Web 应用程序还是机器到机器应用程序

这取决于我们将要运行的主机模型。

目前项目设置的美好之处在于服务器将处理所有身份验证并将这些操作交给 WebAssembly(如果我们有一个在 InteractiveAuto 或 InteractiveWebAssembly 中运行的可组件)。但这不会限制功能,只会限制我们在设置应用程序时需要配置的内容。

如果我们打算只以 Blazor 服务器(InteractiveServer)运行,我们应该使用常规的 Web 应用程序。但我们可能希望将所有内容都改为在 InteractiveWebAssembly 中运行,所以在这里不要限制自己。

选择单页应用程序,这样我们就可以在任何主机模型中使用我们的身份验证。

接下来,我们将选择我们项目使用的技术。我们有 Apache、.NET、Django、Go 以及许多其他选择,但我们没有针对 Blazor 的特定选择,至少在撰写本文时没有。

只需跳过这一步并点击设置选项卡。

现在,我们将设置我们的应用程序。我们需要保存并稍后使用一些值。你需要确保你记下了域名客户端 ID客户端密钥,因为我们将在稍后使用这些。

如果我们向下滚动,我们可以更改徽标,但我们将跳过这一步。

  1. 保持应用程序登录 URI为空。从 .NET 6 开始,端口号是随机的,所以请确保添加你应用程序的端口号:

    1. 允许的回调 URLhttps://localhost:PORTNUMBER/callback

    2. 允许的注销 URLhttps://localhost:PORTNUMBER/

允许的回调 URL 是 Auth0 在用户身份验证后将要调用的 URL,而允许的注销 URL 是用户注销后应重定向到的位置。

现在,在页面底部点击保存更改

配置我们的 Blazor 应用程序

我们已经完成了 Auth0 的配置。接下来,我们将配置我们的 Blazor 应用程序。

在 .NET 中存储密钥的方式有很多(一个未签入的文件、Azure Key Vault 等)。你可以使用你最熟悉的一种。

我们将保持非常简单,并将机密信息存储在 appsettings.json 中。确保在提交时记住排除该文件。您不要将机密信息存入源代码控制。您可以在文件上右键单击并选择 Git忽略和取消跟踪项

要配置我们的 Blazor 项目,请按照以下步骤操作:

  1. BlazorWebApp.Client 项目的根目录下,添加一个名为 UserInfo.cs 的新类,并添加以下内容:

    namespace BlazorWebApp.Client;
    public class UserInfo
    {
        public required string UserId { get; set; }
        public required string Email { get; set; }    public required string[] Roles { get; set; }
    } 
    

这是从 Blazor 模板中提取的。

  1. BlazorWebApp 项目中,打开 appsettings.json 并将以下代码添加到现有应用程序设置对象的根目录:

     "Auth0": {
        "Authority": "Get this from the domain for your application at Auth0",
        "ClientId": "Get this from Auth0 setting"
      } 
    

这些是我们之前章节中记录的值。用我们自己的 Auth0 中的值替换这些值。

由于我们的网站是一个带有一些附加 Blazor 功能的 ASP.NET 网站,这意味着我们可以使用 NuGet 包来获得一些开箱即用的功能。

  1. BlazorWebApp 项目中,添加对 Auth0.AspNetCore.Authentication NuGet 包的引用。

  2. 在项目的根目录下,创建一个名为 PersistingServerAuthenticationStateProvider.cs 的新类,并添加以下代码:

    using BlazorWebApp.Client;
    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Authorization;
    using Microsoft.AspNetCore.Components.Server;
    using Microsoft.AspNetCore.Components.Web;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Options;
    using System.Diagnostics;
    namespace BlazorWebApp;
    internal sealed class PersistingServerAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable
    {
        private readonly PersistentComponentState state;
        private readonly IdentityOptions options;
        private readonly PersistingComponentStateSubscription subscription;
        private Task<AuthenticationState>? authenticationStateTask;
        public PersistingServerAuthenticationStateProvider(
            PersistentComponentState persistentComponentState,
            IOptions<IdentityOptions> optionsAccessor)
        {
            state = persistentComponentState;
            options = optionsAccessor.Value;
            AuthenticationStateChanged += OnAuthenticationStateChanged;
            subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
        }
        private void OnAuthenticationStateChanged(Task<AuthenticationState> task)
        {
            authenticationStateTask = task;
        }
        private async Task OnPersistingAsync()
        {
            if (authenticationStateTask is null)
            {
                throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}().");
            }
            var authenticationState = await authenticationStateTask;
            var principal = authenticationState.User;
            if (principal.Identity?.IsAuthenticated == true)
            {
                var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value;
                var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value;
                var roles = principal.FindAll(options.ClaimsIdentity.RoleClaimType);
    
                if (userId != null)
                {
                    state.PersistAsJson(nameof(UserInfo), new UserInfo
                    {
                        UserId = userId,
                        Email = email,
                        Roles=roles.Select(r=>r.Value).ToArray()
                    });
                }
            }
        }
        public void Dispose()
        {
            subscription.Dispose();
            AuthenticationStateChanged -= OnAuthenticationStateChanged;
        }
    } 
    
  3. 我已经从 Blazor 模板(当我们选择立即添加身份验证时)中提取了这个文件。我添加了角色,以便如果 Auth0 提供任何角色,它们也会存储在状态中。目前,Auth0 不会给我们任何角色,所以我们稍后再回到角色。正在发生的事情是,当我们登录时,它将在 PersistentComponentState 中保存已登录用户,这样我们就可以轻松地将用户转移到 WebAssembly。服务器将在 DOM 上渲染数据,然后 WebAssembly 将获取这些数据。

  4. 打开 Program.cs 并在文件顶部添加以下代码:

    using BlazorWebApp;
    using Auth0.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.Cookies; 
    
  5. WebApplication app = builder.Build(); 之前添加以下代码:

    builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();
    builder.Services.AddCascadingAuthenticationState();
    builder.Services
        .AddAuth0WebAppAuthentication(options =>
        {
            options.Domain = builder.Configuration["Auth0:Authority"]??"";;
            options.ClientId = builder.Configuration["Auth0:ClientId"]??"";;
        }); 
    
  6. 在 Blazor 的早期版本中,我们必须首先确保当我们的组件请求 AuthenticationStateProvider 时,我们返回一个 PersistingServerAuthenticationStateProvider 的实例。我们还添加了对 AddCascadingAuthenticationState 的调用,这将确保无论托管方法如何,都会向所有组件发送 AuthenticationState

  7. 此外,在 app.UseAntiforgery(); 之后添加以下代码。这段代码将允许我们保护我们的网站:

    app.UseAuthentication();
    app.UseAuthorization(); 
    
  8. Program.cs 中,在 app.Run() 之前添加以下代码:

    app.MapGet("account/login", async (string redirectUri, HttpContext
     context) =>
    {
        var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
             .WithRedirectUri(redirectUri)
             .Build();
        await context.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
    }); 
    

当我们的网站重定向到 authentication/login 时,Minimal API 端点将启动登录功能。

  1. 我们需要添加类似的注销功能。在 步骤 7 中的上一个端点下方添加以下代码:

    app.MapGet("authentication/logout", async (HttpContext context) =>
    {
        var authenticationProperties = new LogoutAuthenticationPropertiesBuilder()
             .WithRedirectUri("/")
             .Build();
        await context.SignOutAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
        await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    }); 
    

需要注销两次,一次用于 Auth0 认证方案,一次用于 cookie 认证方案。配置全部完成。现在,我们需要一些东西来保护。

保护我们的 Blazor 应用

Blazor 使用 App.razor 进行路由。为了启用 Blazor 的保护,我们需要在应用程序组件中添加一些组件。

我们需要添加CascadingAuthenticationState,这将把身份验证状态发送到所有监听它的组件。我们还需要将路由视图更改为AuthorizeRouteView,它可以根据您是否已进行身份验证显示不同的视图:

  1. BlazorWebApp项目中,打开Components/_Imports.razor并添加以下命名空间:

    @using Microsoft.AspNetCore.Components.Authorization
    @using BlazorWebApp.Components.Layout
    @using BlazorWebApp.Components 
    
  2. 打开Components/Routes.razor组件,并将Router组件内的所有内容替换为以下内容:

    <Found Context="routeData">
         <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
             <Authorizing>
                 <p>Determining session state, please wait...</p>
             </Authorizing>
             <NotAuthorized>
                 <h1>Sorry</h1>
                 <p>You're not authorized to reach this page. You need to log in.</p>
             </NotAuthorized>
         </AuthorizeRouteView>
         <FocusOnNavigate RouteData="@routeData" Selector="h1" />
     </Found>
     <NotFound>
         <PageTitle>Not found</PageTitle>
         <LayoutView Layout="@typeof(MainLayout)">
             <p role="alert">Sorry, there's nothing at this address.</p>
         </LayoutView>
     </NotFound> 
    

在 Blazor 的早期版本中,我们不得不将代码包裹在<CascadingAuthenticationState>标签内,但使用.NET 8 后,这可以通过添加对AddCascadingAuthenticationState的调用来自动处理。

现在,只剩下两件事:一个我们可以保护页面和一个登录链接显示。

  1. Components文件夹中,添加一个名为LoginStatus.razor的新 Razor 组件。

将内容替换为以下内容:

<AuthorizeView>
    <Authorized>
        <a href="authentication/logout">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="account/login?returnUrl=/">Log in</a>
    </NotAuthorized>
</AuthorizeView> 

LoginStatus是一个组件,如果未进行身份验证,将显示登录链接;如果已进行身份验证,将显示注销链接。

  1. 打开Components/Layout/MainLayout.razor

    <AuthorizeView Roles="Administrator">
            <div class="sidebar">
                <NavMenu />
            </div>
    </AuthorizeView> 
    

将关于链接替换为以下内容:

<LoginStatus /> 

现在,我们的布局页面将显示我们是否已登录,并给我们提供登录或注销的机会。

  1. SharedComponentsBlazorWebApp.Client项目中,在每个项目的_Imports文件中添加以下内容:

    @using Microsoft.AspNetCore.Authorization 
    
  2. authorize属性添加到我们希望保护的组件中。

  3. SharedComponents项目中,我们有以下组件:

Pages/Admin/BlogPostEdit.razor

Pages/Admin/BlogPostList.razor

Pages/Admin/CategoryList.razor

Pages/Admin/TagList.razor(在BlazorWebApp.Client项目中)

在上述每个组件中添加以下属性:

@attribute [Authorize] 

这就是全部所需,一些配置,然后我们就可以设置好了。

现在,启动我们的BlazorWebApp并查看您是否可以访问/admin/blogposts页面(剧透:您不应该能够访问);登录(创建用户)并查看您现在是否可以访问该页面。

我们的管理界面已经得到了保护。

在下一节中,我们将确保我们的 Blazor WebAssembly 版本博客和 API 的安全性。

保护 Blazor WebAssembly

在本书的前几版中,我们构建了两个版本的博客,一个用于 Blazor 服务器,一个用于 Blazor WebAssembly。在本版中,整个重点是我们不必在两者之间做出选择。如前所述,有两个项目,BlazorWebAppBlazorWebApp.Client。在客户端项目中,我们添加了所有我们希望作为 WebAssembly 运行的组件。这里有一个真正酷的部分。我们在客户端项目中有一个 TagList 组件。如果我们以 InteractiveAuto 运行它,它将首先使用 BlazorWebApp 项目的配置在服务器上使用 SignalR 进行渲染。但下次网站运行时,它将加载 WebAssembly 版本并使用 BlazorWebApp.Client 项目的配置。因此,同一个组件可以使用不同的依赖注入。在这种情况下,它将使用直接数据访问,而在另一种情况下,它将使用我们在上一章中创建的 API 客户端。

为了我们能够访问我们的 API,我们需要设置 HttpClient

但首先,我们需要从服务器获取身份验证信息。WebAssembly 并不真正登录;它从服务器获取信息并使用身份验证 cookie 进行对服务器的额外调用。

在服务器项目中,我们添加了一个 PersistingServerAuthenticationStateProvider 来存储有关登录用户的信息。在客户端,我们需要获取这些信息。

  1. BlazorWebApp.Client 项目中,在根目录下添加一个名为 PersistentAuthenticationStateProvider.cs 的新类。

  2. 将代码替换为以下内容:

    using BlazorWebApp.Client;
    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Authorization;
    using System.Security.Claims;
    namespace BlazorApp1.Client;
    internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider
    {
        private static readonly Task<AuthenticationState> defaultUnauthenticatedTask =
            Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
        private readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask;
        public PersistentAuthenticationStateProvider(PersistentComponentState state)
        {
            if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
            {
                return;
            }
            List<Claim> claims = new();
            claims.Add(new Claim(ClaimTypes.NameIdentifier, userInfo.UserId));
            claims.Add(new Claim(ClaimTypes.Name, userInfo.Email??""));
            claims.Add(new Claim(ClaimTypes.Email, userInfo.Email??""));
            foreach (var role in userInfo.Roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
            authenticationStateTask = Task.FromResult(
                new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
                    authenticationType: nameof(PersistentAuthenticationStateProvider)))));
        }
        public override Task<AuthenticationState> GetAuthenticationStateAsync() => authenticationStateTask;
    } 
    
  3. 这也是从 Blazor 模板(带有身份验证)中提取的。这里只进行了少量修改,比如添加角色。

  4. BlazorWebApp.Client 项目中的 Program.cs 文件中,在 builder.Build().RunAsync() 之上添加以下行;

    builder.Services.AddAuthorizationCore();
    builder.Services.AddCascadingAuthenticationState();
    builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
    builder.Services.AddHttpClient("Api",client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); 
    

这将启用身份验证,将级联 身份验证状态 添加到我们的组件中,并从 Persistent Component State 中获取登录用户。HttpClient 的名称是 “Api”;这是我们 第七章创建一个 API 中使用的名称。

我们还需要设置依赖注入,以便当我们请求一个 IBlogAPI 时,我们将得到我们在 第七章创建一个 API 中创建的 BlogApiWebClient

  1. Program.cs 文件中,在 builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>(); 行之下添加以下代码:

    builder.Services.AddTransient<IBlogApi, BlogApiWebClient>(); 
    

现在,当我们请求一个 IBlogApi 时,我们将得到通过 API 访问数据的 API 客户端。这里真正酷的地方是,根据组件是在服务器(静态、InteractiveServer)上渲染、客户端(InteractiveWebAssembly)上渲染,还是组合(InteractiveAuto)渲染,它将选择适合该场景的正确客户端。我们有选择最适合的那个的能力。

  1. 确保添加所需的命名空间。

现在,一切准备就绪,我们可以为在 WebAssembly 模式下运行进行安全设置了。

这个示例是关于在运行 ASP.NET 后端时保护 WebAssembly。在第十六章深入 WebAssembly中,我们将探讨如何保护 Blazor WebAssembly 应用程序。

让我们试一试:

  1. BlazorWebApp.Client项目中,打开Pages/Admin/TagList.razor。到目前为止,我们一直使用 InteractiveServer 渲染模式运行我们的组件。现在,让我们将其更改为 InteractiveWebAssembly 并运行它。

@rendermode InteractiveServer更改为@rendermode InteractiveWebAssembly。就这样!现在,我们的组件将首先在服务器上渲染(因为我们正在运行服务器预渲染),然后 WebAssembly 将接管并再次渲染组件。它将拾取存储在组件状态中的认证信息,并使用我们的 Web API 从我们的 API 检索数据。这是因为当请求IBlogApi的实例时,WebAssembly 应用程序被配置为使用BlogApiWebClient。因此,相同的组件首先使用直接数据访问在服务器上预渲染,然后再次使用 Web API。非常酷!

现在,运行项目,导航到/Admin/Tags,并尝试编辑一些标签。这个组件现在正在 WebAssembly 上运行。您可以尝试将其更改为@rendermode InteractiveAuto。为了看到行为,这将首先连接 SignalR,然后在下一次加载时切换到 WebAssembly。

但如果不同的用户有不同的权限怎么办?

正是这里,角色派上用场。

添加角色

Blazor Server 和 Blazor WebAssembly 处理角色的方式略有不同;这不是什么大问题,但我们需要进行不同的实现。在本章中,我们将探讨为我们的当前项目(每个组件)实现它,并在第十六章深入 WebAssembly中返回角色。

通过添加角色配置 Auth0

让我们从在 Auth0 中添加角色开始:

  1. 登录到Auth0,导航到用户管理 | 角色,然后点击创建角色

  2. 输入名称管理员和描述可以做任何事情,然后按创建

  3. 前往用户选项卡,点击添加用户,搜索您的用户,然后点击分配。您也可以从左侧的用户菜单管理角色。

  4. 默认情况下,角色不会发送到客户端,因此我们需要丰富数据以包括角色。

我们通过添加一个操作来实现这一点。

  1. 前往操作,然后流程

流程是在特定流程中执行代码的一种方式。

我们希望Auth0在我们登录时添加我们的角色。

  1. 选择登录,在那里我们将看到流程;在我们的情况下,我们还没有任何内容。

  2. 在右侧,点击自定义和加号。当一个小弹出菜单出现时,选择从头开始构建

  3. 将操作命名为添加角色,保持触发器运行时不变,然后按创建

我们将看到一个窗口,我们可以在这里编写我们的操作。

  1. 将所有代码替换为以下内容:

    /**
     * @param {Event} event - Details about the user and the context in which they are logging in.
     * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
     */
    exports.onExecutePostLogin = async (event, api) => {
      const claimName  = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role'
      if (event.authorization) {
        api.idToken.setCustomClaim(claimName, event.authorization.roles);
        api.accessToken.setCustomClaim(claimName, event.authorization.roles);
      }
    } 
    
  2. 点击部署然后返回流程

  3. 再次点击自定义,我们将看到我们刚刚创建的操作。

  4. 添加角色操作拖到开始完成之间的箭头上。

  5. 点击应用

现在,我们有一个将角色添加到我们的登录令牌中的操作。

我们的用户现在是一个管理员。值得注意的是,角色是 Auth0 的付费功能,并且仅在试用期间免费。

现在,让我们设置 Blazor 以使用这个新角色。

将角色添加到 Blazor

由于我们正在使用 Auth0 库,Blazor 的设置几乎已经完成。

让我们修改一个组件以显示用户是否是管理员:

  1. Components 项目中,打开 Shared/NavMenu.razor

  2. 在组件顶部添加以下内容:

    <AuthorizeView Roles="Administrator">
        <Authorized>
            Hi admin!
        </Authorized>
        <NotAuthorized>
            You are not an admin =(
        </NotAuthorized>
    </AuthorizeView> 
    
  3. 现在,运行我们的 BlazorWebApp 项目。

如果我们登录,我们应该能够看到左侧的文字,在深蓝色背景上显示黑色文字“Hi Admin!”,这可能不是很明显。我们将在第九章共享代码和资源中解决这个问题。

摘要

在本章中,我们学习了如何将身份验证添加到我们现有的网站上。在创建项目时添加身份验证更容易。然而,现在我们对底层发生了什么以及如何处理添加外部身份验证源有了更好的理解。

在整本书中,我们已经在不同的托管模型之间共享了组件。

在下一章中,我们将探讨共享更多内容,例如静态文件和 CSS,并尝试使一切看起来都很漂亮。

第九章:分享代码和资源

在整本书中,我们一直在构建一个可以在许多不同的托管模型中运行的项目。如果我们想在将来进一步切换技术,或者像我们在工作中做的那样,在客户门户和我们的内部客户关系管理CRM)系统之间共享组件,这是一个构建项目的绝佳方式。

总是考虑我们正在构建的组件中是否可能有一个可共享的部分;这样,我们可以重用它,如果我们向组件中添加了某些内容,我们就可以为所有组件获得这种好处。

但这不仅仅是我们自己项目内部共享组件的问题。如果我们想创建一个可以与其他部门共享的库,或者甚至是一个与世界共享组件的开源项目,那会怎样?

在本章中,我们将探讨一些我们在共享组件时已经使用的内容,以及共享 CSS 和其他静态文件。

在本章中,我们将涵盖以下主题:

  • 添加静态文件

  • CSS 隔离

技术要求

确保您已经遵循了前面的章节,或者以Chapter08文件夹作为起点。

您可以在github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter09找到本章结果的源代码。

如果您使用 GitHub 上的代码跳转到本章,请确保您已在设置文件中添加了Auth0账户信息。您可以在第八章身份验证和授权中找到说明。

添加静态文件

Blazor 可以使用静态文件,如图片、CSS 和 JavaScript。如果我们把我们的文件放在wwwroot文件夹中,它们将自动暴露给互联网,并可以从我们网站的根目录访问。Blazor 的好处在于,我们也可以用库来做同样的事情;在库内分发静态文件非常简单。

在工作中,我们在所有的 Blazor 项目中共享组件,共享库也可以依赖于其他库。通过共享组件和构建我们自己的组件(有时是在其他库之上),我们确保整个网站具有相同的视觉和感觉。我们还共享静态内容,如图片和 CSS,这使得如果我们需要更改某些内容并且希望所有网站都受到影响时,变得简单快捷。

要链接到另一个库/程序集的资源,我们可以使用_content文件夹。

看一下这个例子:

<link rel="stylesheet" href="_content/SharedComponents/MyBlogStyle.min.css" /> 

HTML 的link标签、relhref是普通的 HTML 标签和属性,但添加以_content开头的 URL 告诉我们,我们想要访问的内容位于另一个库中。在我们的例子中,库(程序集名称)是SharedComponents,后面是我们想要访问的文件,该文件存储在我们的库中的wwwroot文件夹中。

Blazor 最终只是 HTML,而 HTML 可以使用 CSS 进行样式化。正如之前提到的,Blazor 模板默认使用 Bootstrap,我们也将继续使用它。

有一个优秀的网站,提供了易于使用的 Bootstrap 主题,可供下载,网址为bootswatch.com/

我喜欢 Darkly 主题,所以我们将使用这个,但你可以自由地稍后尝试其他主题。

选择框架

我经常被问到如何样式化 Blazor 应用程序,事实是你可以使用你习惯的所有工具。最终,Blazor 会输出 HTML。我们可以使用许多语言和框架来编写我们的 CSS。

我们可以使用 CSS、语法优美的样式表SASS)和更简洁的 CSSLESS)。只要输出是 CSS,我们就可以使用它。

在本章中,我们将继续使用 Bootstrap 和 CSS。SASS 和 LESS 超出了本书的范围。

Tailwind 是 Blazor 的一个流行框架,绝对可以与 Blazor 一起使用。Tailwind 非常注重组件,并且需要一些配置才能开始,但如果它是你熟悉并喜欢的,你可以与 Blazor 一起使用它。

添加新的样式

许多模板以 Bootstrap 为基础,所以如果你在寻找网站的设计,使用基于 Bootstrap 的模板将是一个易于实现的方案。

Bootstrap 的问题(以及为什么有些人不喜欢它)在于许多网站使用 Bootstrap,导致“所有网站看起来都一样”。如果我们正在构建一个业务线LOB),这可能是个好事,但如果我们试图创新,这可能是个坏事。Bootstrap 在下载时也相当大,这也是反对它的一个论点。

本章是关于让我们的博客看起来更美观,所以我们将继续使用 Bootstrap,但我们应该知道,如果我们使用其他东西来处理我们的 CSS,它也会与 Blazor 一起工作。

这些模板网站之一是Bootswatch,它为我们提供了从传统 Bootstrap 主题中的一些美好变化:

  1. 导航到bootswatch.com/darkly/

  2. 在顶部菜单Darkly中,有一些链接。下载bootstrap.min.css

  3. SharedComponents项目中,在wwwroot文件夹中,添加bootstrap.min.css文件。

我们可以为我们的网站添加所有必要的 CSS。

添加 CSS

现在,是时候为我们网站添加新的样式了:

  1. BlazorWebApp项目中,打开Components/App.razor文件。

  2. 定位到这一行:

    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> 
    

    将前面的行替换为以下内容:

    <link rel="stylesheet" href="_content/SharedComponents/bootstrap.min.css" /> 
    
  3. 通过按Ctrl + F5来运行项目。

太好了!我们的 Blazor 项目现在已更新为使用新的样式。主色调现在应该是深色,但仍有一些工作要做。

使管理界面更易于使用

让我们现在进一步清理。我们刚刚开始实现管理功能,所以让我们让它更容易访问。左侧的菜单不再需要,所以让我们将其改为仅在您是管理员时才可见:

  1. 打开Components/Layout/MainLayout.razor文件,并将AuthorizeView放在sidebar div周围,如下所示:

    <AuthorizeView Roles="Administrator">
        <div class="sidebar">
            <NavMenu />
        </div>
    </AuthorizeView> 
    

在这种情况下,我们没有指定AuthorizedNotAuthorized。默认行为是Authorized,所以如果我们只寻找授权状态,我们不需要通过名称指定它。

启动项目以查看其效果。如果我们未登录,菜单不应显示。

现在,我们需要让菜单看起来更好。尽管计数器点击起来很有趣,但它对我们博客来说意义不大。

使菜单更实用

我们应该将链接替换为指向我们管理页面的链接:

  1. BlazorWebApp项目中,打开Components/Layout/Navmenu.razor文件。

    编辑代码,使其看起来像这样:

    <div class="top-row ps-3 navbar navbar-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="">MyBlog Admin</a>
        </div>
    </div>
    <input type="checkbox" title="Navigation menu" class="navbar-toggler" />
    <div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
        <nav class="flex-column">
            <div class="nav-item px-3">
                <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                    <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
                </NavLink>
            </div>
            <div class="nav-item px-3">
                <NavLink class="nav-link" href="Admin/Blogposts">
                    <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Blog posts
                </NavLink>
            </div>
            <div class="nav-item px-3">
                <NavLink class="nav-link" href="Admin/Tags">
                    <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Tags
                </NavLink>
            </div>
            <div class="nav-item px-3">
                <NavLink class="nav-link" href="Admin/Categories">
                    <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Categories
                </NavLink>
            </div>
        </nav>
    </div> 
    
  2. 如果我们启动项目并调整屏幕大小,我们会注意到菜单在大屏幕上显示,但在小屏幕上隐藏。值得注意的是,NavMenu不包含代码,所以菜单的隐藏和显示完全依赖于 CSS。

太好了!我们的博客看起来更像一个博客了,但我们还可以做得更多!

使博客看起来更像一个博客

管理界面已经完成(至少,目前是这样),我们应该专注于我们博客的首页。首页应该包含博客文章的标题和一些描述:

  1. SharedComponents项目中,打开Pages/Home.razor文件。

  2. 在文件顶部添加一个using语句用于Markdig

    @using Markdig; 
    
  3. 添加一个OnInitializedAsync方法来处理Markdig管道的实例化(这是我们在Post.razor文件中有的相同代码):

    MarkdownPipeline pipeline;
    protected override Task OnInitializedAsync()
    {
        pipeline = new MarkdownPipelineBuilder()
                    .UseEmojiAndSmiley()
                    .Build();
        return base.OnInitializedAsync();
    } 
    
  4. Virtualize组件内部,将内容(RenderFragment)更改为以下内容:

     <article>
            <h2>@p.Title</h2>
            @((MarkupString)Markdig.Markdown.ToHtml(new string(p.Text.Take(100).ToArray()), pipeline))
            <a href="/Post/@p.Id">Read more</a>
        </article> 
    
  5. 此外,删除<ul>标签。

现在,使用Ctrl + F5运行项目,查看我们新的首页。我们的博客开始成形,但我们还有工作要做。

CSS 隔离

在.NET 5 中,Microsoft 添加了一个名为隔离 CSS 的功能。许多其他框架也有类似的功能。这个想法是为一个组件编写特定的 CSS。当然,好处是我们创建的 CSS 不会影响其他任何组件。

Blazor 的模板使用隔离的 CSS 为Components/Layout/MainLayout.razorNavMenu.Razor。如果我们展开MainLayout.razor,我们会看到一个名为MainLayout.razor.css的文件。这个树状结构是通过使用相同的命名方式实现的。

我们还可以通过添加一个名为MainLayout.razor.scss的文件来使用 SASS。重要的是,我们添加的文件应该生成一个名为MainLayout.razor.css的文件,以便编译器可以识别。

这个命名约定将确保重写 CSS 和 HTML 输出。

CSS 具有以下命名约定:

main {
    flex: 1;
} 

它将被重写如下:

main[b-bfl5h5967n] {
    flex: 1;
} 

这意味着元素需要有一个名为 b-bfl5h5967n 的属性(在这个例子中),以便应用该样式。这是一个为该组件随机生成的字符串。

MainLayout 组件内部包含 CSS 标签的 div 标签将被输出如下:

<main b-bfl5h5967n> 

为了让所有这些发生,我们还需要有一个指向 CSS 的链接(由模板提供),它看起来是这样的:

<link href="{Assemblyname}.styles.css" rel="stylesheet"> 

这对于组件库来说很有用。在我们的共享库(NavMenuMainLayout)中,组件的 CSS 被包含在 {Assemblyname}.styles.css 文件中。

我们不需要做任何额外的事情来包含我们的共享 CSS。如果我们为任何人创建库,我们应该考虑在组件需要一些 CSS 才能正确工作的情况下使用隔离的 CSS 方法。

如果我们从空模板开始创建 Blazor 项目,我们需要添加一个指向隔离 CSS 的链接。

这样,我们的用户就不需要添加对 CSS 的引用,并且没有风险在我们的 CSS 会破坏用户的 app(因为它被隔离)。重要的是,我们在需要的时候使用正确的方法。

假设我们正在创建一个具有非常特定样式的组件,这个样式只由该组件使用。在这种情况下,使用隔离的 CSS 是一个很好的选择,因为它更容易找到(紧挨着组件),并且我们可以使用 CSS 变量来设置颜色等。

我们在隔离 CSS 中对类似事物进行样式设计时应该小心,以免最终出现一堆不同的 CSS 文件来设计按钮等。

如前所述,隔离的 CSS 只影响组件内部的 HTML 标签,但如果我们组件内部有其他组件呢?

如果我们打开 Component/Layout/NavMenu.razor.css,我们可以看到,对于 .nav-item 样式,其中一些使用了 ::deep 关键字;这意味着即使是子组件也应该受到这种样式的影响。

看看以下代码:

.nav-item ::deep a {…} 

它针对的是 <a> 标签,但 Razor 代码看起来是这样的:

<li class="nav-item px-3">
     <NavLink class="nav-link" href="Admin/Blogposts">
         <span class="oi oi-signpost" aria-hidden="true"></span> Blog posts
     </NavLink>
</li> 

NavLink 组件渲染了 <a> 标签;通过添加 ::deep,我们表示我们希望将此样式应用于具有 .nav-item 类的所有元素以及该元素内部的所有 <a> 标签。

我们还需要了解的另一件事是 ::deep;它确保共享属性的 ID(例如 b-bfl5h5967n),并且需要一个 HTML 标签来实现这一点。因此,如果我们有一个由其他组件组成的组件(完全不添加任何 HTML 标签),我们需要在内容周围添加一个 HTML 标签,以便 ::deep 能够工作。

在我们总结本章内容之前,让我们再做一些事情。

让我们修复菜单的背景颜色:

  1. 打开 Components/Layout/MainLayout.razor.css

  2. 查找 .sidebar 样式并将其替换为以下代码:

    .sidebar {
        background-image: linear-gradient(180deg, var(--bs-body-bg) 0%,var(--bs-gray-800) 70%);
    } 
    
  3. .top-row 样式替换为以下代码:

    .top-row {
        background-color: var(--bs-primary);
        justify-content: flex-end;
        height: 3.5rem;
        display: flex;
        align-items: center;
    } 
    

    我们替换了背景颜色并移除了边框。

  4. .top-row ::deep a, .top-row ::deep .btn-link 样式下,添加以下内容:

    color:white; 
    

现在,我们能够更好地看到登录/登出链接。

我们现在有一个工作的管理界面和一个看起来不错的网站。

摘要

在本章中,我们添加了共享 CSS。

我们看到了如何创建共享库(供他人使用)。这也是结构化我们内部项目的一个很好的方法(这样就可以轻松地从 Blazor Server 转换到 Blazor WebAssembly,或者反过来)。

如果你已经有了网站,你可以在共享库中构建你的 Blazor 组件,我们在整本书中都这样做过。

将组件作为你网站的一部分(使用 Blazor Server),你可以逐步开始使用 Blazor,直到你将整个网站转换完成。当这一切都完成时,你可以决定是否继续使用 Blazor Server(如我提到的,我们在我的工作场所使用 Blazor Server)或者转向 Blazor WebAssembly,或者像我们的项目一样两者都使用。

我们讨论了如何在我们的网站上使用 SASS 和 CSS,包括常规 CSS 和隔离 CSS。

在下一章中,我们将了解我们试图避免的一件事(至少,我是这样)——作为 Blazor 开发者——JavaScript。

第十章:JavaScript 互操作

在本章中,我们将探讨 JavaScript。在特定场景中,我们仍然需要使用 JavaScript,或者我们可能想使用依赖于 JavaScript 的现有库。Blazor 使用 JavaScript 来更新 文档对象模型DOM),下载文件,并访问客户端上的本地存储。

因此,总会有,并且始终会有,我们需要与 JavaScript 通信或让 JavaScript 与我们通信的情况。别担心,Blazor 社区是一个了不起的社区,所以很可能有人已经构建了我们需要的互操作。

在本章中,我们将涵盖以下主题:

  • 为什么我们需要 JavaScript?

  • .NET 到 JavaScript

  • JavaScript 到 .NET

  • 实现现有的 JavaScript 库

  • WebAssembly 中的 JavaScript 互操作

技术要求

确保你已经遵循了前面的章节或使用 Chapter09 文件夹作为起点。

你可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter10 找到本章结果的源代码。

如果你正在使用 GitHub 上的代码跳入这一章,请确保你已经在设置文件中添加了 Auth0 账户信息。你可以在 第八章身份验证和授权 中找到说明。

为什么我们需要 JavaScript?

许多人说 Blazor 是 JavaScript 杀手,但事实是 Blazor 需要 JavaScript 来工作。某些事件仅在 JavaScript 中触发,如果我们想使用这些事件,我们需要进行互操作。

我开玩笑地说,我在开始使用 Blazor 进行开发时从未写过这么多 JavaScript。这并不糟糕。

我已经编写了几个需要 JavaScript 才能工作的库。它们被称为 Blazm.ComponentsBlazm.Bluetooth

第一个是一个网格组件,它使用 JavaScript 互操作在窗口大小调整时触发 C# 代码(JavaScript 到 .NET),以移除无法适应窗口的列。

当触发时,C# 代码会调用 JavaScript 来获取列的大小,这是只有网络浏览器才知道的,然后根据这个答案,如果需要的话,它会移除列。

第二个,Blazm.Bluetooth,使得使用 Web Bluetooth 与蓝牙设备交互成为可能,这是一个通过 JavaScript 可访问的 Web 标准。

它使用双向通信;蓝牙事件可以触发 C# 代码,而 C# 代码可以遍历设备并向它们发送数据。它们都是开源的,所以如果你对查看一个真实世界的项目感兴趣,你可以在我的 GitHub 上查看它们:github.com/EngstromJimmy

如前所述,在大多数情况下,我会说我们不需要自己编写 JavaScript。Blazor 社区非常大,所以很可能有人已经编写了我们需要的代码。但我们也不必害怕使用 JavaScript。接下来,我们将探讨向我们的 Blazor 项目添加 JavaScript 调用的不同方法。

.NET 到 JavaScript

从 .NET 调用 JavaScript 非常简单。有两种这样做的方法:

  • 全局 JavaScript

  • JavaScript 隔离

我们将探讨这两种方法,看看它们有什么区别。

全局 JavaScript(旧方法)

要访问 JavaScript 方法,我们需要使其可访问。一种方法是通过 JavaScript 窗口对象全局定义它。这是一种不好的做法,因为它对所有脚本都是可访问的,并且可能会替换其他脚本中的功能(如果我们不小心使用了相同的名称)。

例如,我们可以使用作用域,在全局空间中创建一个对象,并将我们的变量和方法放在该对象上,这样我们就能降低一点风险,至少。

使用作用域可能看起来像这样:

<script>
window.myscope = {};
window.myscope.methodName = () => { alert("this has been called"); }
</script> 

我们创建一个名为 myscope 的对象。然后,我们在该对象上声明一个名为 methodName 的方法。在这个例子中,方法中没有代码;这只是为了演示如何实现。

然后,要从 C# 调用该方法,我们将使用 JSRuntime 如此调用:

@using Microsoft.JSInterop
@inject IJSRuntime jsRuntime
await jsRuntime.InvokeVoidAsync("myscope.methodName"); 

我们可以使用两种不同的方法来调用 JavaScript:

  • InvokeVoidAsync,它调用 JavaScript 但不期望返回值

  • InvokeAsync<T>,它调用 JavaScript 并期望返回类型为 T 的值

如果我们想的话,我们也可以向我们的 JavaScript 方法发送参数。我们还需要引用 JavaScript,并且 JavaScript 必须存储在 wwwroot 文件夹中。

另一种方法是 JavaScript 隔离,它使用这里描述的方法,但使用模块。

JavaScript 隔离

在 .NET 5 中,我们得到了一种使用 JavaScript 隔离添加 JavaScript 的新方法,这是一种调用 JavaScript 的更好方式。它不使用全局方法,也不需要我们引用 JavaScript 文件。

这对于组件供应商和最终用户来说都很棒,因为 JavaScript 只在需要时加载。它只会加载一次(Blazor 为我们处理这一点),我们不需要添加对 JavaScript 文件的引用,这使得开始和使用库变得更容易。

所以,让我们来实现它。

隔离的 JavaScript 可以存储在 wwwroot 文件夹中,但自从 .NET 6 更新以来,我们可以像添加隔离 CSS 一样添加它们。将它们添加到组件的文件夹中,并命名为,在末尾添加 .jsmycomponent.razor.js)。

让我们就这样做!

在我们的项目中,我们可以删除类别和组件。让我们实现一个简单的 JavaScript 调用来显示一个提示,以确保用户想要删除类别或标签。但我们已经讨论了以可重用方式做事,所以让我们这样做:

  1. SharedComponents项目中,选择ReusableComponents/BlogButton.razor文件,创建一个新的 JavaScript 文件,并将文件命名为BlogButton.razor.js

  2. 打开新文件(位于解决方案资源管理器中的BlogButton.razor下)并添加以下代码:

    export function showConfirm(message) {
        return confirm(message);
    } 
    

    JavaScript Isolation 使用标准的EcmaScriptES)模块,并且可以按需加载。它公开的方法只能通过该对象访问,而不是像旧方法那样全局访问。

  3. 打开BlogButton.razor并在文件顶部注入IJSRuntime

    @using Microsoft.JSInterop
    @inject IJSRuntime jsRuntime 
    
  4. code部分,让我们添加一个将调用 JavaScript 的方法:

     IJSObjectReference jsmodule;
        [Parameter]
        public string? ConfirmMessage { get; set; } = null;
        private async Task<bool> ShouldExecute()
        {
            if (ConfirmMessage != null)
            {
                jsmodule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "/_content/SharedComponents/ReusableComponents/BlogButton.razor.js");
                return await jsmodule.InvokeAsync<bool>("showConfirm", ConfirmMessage);
            }
            else
            {
                return true;
            }
        } 
    

    IJSObjectReference是对我们将进一步导入的特定脚本的引用。它有权访问我们的 JavaScript 中导出的方法,而其他什么都没有。

    我们运行Import命令并将文件名作为参数发送。这将运行let mymodule = import("/_content/SharedComponents/ReusableComponents/BlogButton.razor.js") JavaScript 命令并返回模块。我们还添加了一个ConfirmMessage参数,这样我们知道如果有ConfirmMessage,我们应该显示确认消息。

    然后,在我们的OnButtonClick方法中,我们首先检查是否应该执行该方法。将其更改为以下内容:

    if (OnClick.HasDelegate && await ShouldExecute())
     {
         await OnClick.InvokeAsync(args);
     } 
    

    现在,我们可以使用我们的按钮来确认我们是否想要删除CategoryTag

  5. 打开ItemList.razor,并将我们的BlogButton添加到组件中。在Virtualize组件内部,将内容更改为以下内容:

     <tr>
    <td>
    <BlogButton OnClick="@(()=> {SelectEvent.InvokeAsync(item); })"> Select</BlogButton>
    </td>
    <td>@ItemTemplate(item)</td>
    <td>
    <BlogButton ConfirmMessage="Are you sure you want to delete this item?" Type="BlogButton.ButtonType.Delete" OnClick="@(()=> {DeleteEvent.InvokeAsync(item);})"> Delete</BlogButton>
    </td>
    </tr> 
    

而不是仅仅调用我们的Delete事件回调,我们首先调用我们的新方法。让 JavaScript 确认你确实想要删除它,如果是的话,然后运行Delete事件回调。

这是一个简单的 JavaScript 实现。

JavaScript 到.NET

反过来呢?我认为从 JavaScript 调用.NET 代码并不是一个非常常见的场景,如果我们发现自己处于那种场景,我们可能需要考虑我们在做什么。

作为 Blazor 开发者,我们应该尽可能避免使用 JavaScript。

我并不是在以任何方式批评 JavaScript,但我看到开发者经常将他们之前使用过的东西强行塞入 Blazor 项目中。

他们用 JavaScript 解决的是用 Blazor 中的if语句容易解决的问题。这就是为什么我认为考虑何时使用 JavaScript 以及何时不使用 JavaScript 是至关重要的。

当然,有时 JavaScript 是唯一的选择,如我之前提到的,Blazm使用双向通信。

从 JavaScript 到.NET 代码的回调有三种方式:

  • 静态.NET 方法调用

  • 实例方法调用

  • 组件实例方法调用

让我们更仔细地看看它们。

静态.NET 方法调用

要从 JavaScript 调用.NET 函数,我们可以使函数静态,并且还需要将JSInvokable属性添加到方法上。

我们可以在 Razor 组件的code部分或类内部添加一个这样的函数:

[JSInvokable]
public static Task<int[]> ReturnArrayAsync()
{
   return Task.FromResult(new int[] { 1, 2, 3 });
} 

在 JavaScript 文件中,我们可以使用以下代码调用该函数:

DotNet.invokeMethodAsync('BlazorWebAssemblySample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      }); 

DotNet对象来自Blazor.jsblazor.server.js文件。

BlazorWebAssemblySample是程序集的名称,ReturnArrayAsync是静态.NET 函数的名称。

如果我们不想让函数名与方法名相同,我们也可以在JSInvokeable属性中指定函数名,如下所示:

[JSInvokable("DifferentMethodName")] 

在这个示例中,JavaScript 回调到.NET 代码,该代码返回一个int数组。

它在 JavaScript 文件中以 promise 的形式返回,我们等待它,然后(使用then运算符)继续执行,将4添加到数组中,然后在控制台输出值。

实例方法调用

这个方法有点棘手;我们需要传递.NET 对象的实例来调用它(这是Blazm.Bluetooth使用的方法)。

首先,我们需要一个类来处理方法调用:

using Microsoft.JSInterop;
public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }
    public string Name { get; set; }
    [JSInvokable]
    public string SayHello() => $"Hello, {Name}!";
} 

这个类在构造函数中接受一个字符串(一个名字)和一个名为SayHello的方法,该方法返回一个包含"Hello,"和我们在创建实例时提供的名字的字符串。

因此,我们需要创建这个类的实例,提供一个名字,并创建DotNetObjectReference<T>,这将使 JavaScript 能够访问这个实例。

但首先,我们需要能够调用.NET 函数的 JavaScript:

export function sayHello (dotnetHelper) {
    return dotnetHelper.invokeMethodAsync('SayHello').then(r => alert(r));
} 

在这种情况下,我们使用导出语法,并导出一个名为sayHello的函数,它接受一个名为dotnetHelperDotNetObjectReference实例。

在该实例中,我们调用SayHello方法,这是.NET 对象上的SayHello方法。在这种情况下,它将引用HelloHelper类的一个实例。

我们还需要调用 JavaScript 方法,我们可以从一个类或,在这种情况下,从一个组件中这样做:

@page "/interop" @using Microsoft.JSInterop
@inject IJSRuntime jsRuntime
@implements IDisposable
<button type="button" class="btn btn-primary" @onclick="async ()=> { await TriggerNetInstanceMethod(); }">    Trigger .NET instance method HelloHelper.SayHello </button>
@code {
    private DotNetObjectReference<HelloHelper> objRef;

    IJSObjectReference jsmodule;
    public async ValueTask<string>
 TriggerNetInstanceMethod()
    {
        objRef = DotNetObjectReference.Create(new HelloHelper("Bruce Wayne"));
        jsmodule = await jsRuntime. InvokeAsync<IJSObjectReference>("import", "/_content/MyBlog.Shared/Interop.razor.js");
        return await jsmodule.InvokeAsync<string>("sayHello", objRef);
    }
    public void Dispose()
    {
        objRef?.Dispose();
    }
} 

让我们来看看这个类。我们注入IJSRuntime,因为我们需要它来调用 JavaScript 函数。为了避免任何内存泄漏,我们还需要确保实现IDisposable接口,并在文件的底部确保销毁DotNetObjectReference实例。

我们创建一个DotNetObjectReference<HelloHelper>类型的私有变量,它将包含我们对HelloHelper实例的引用。我们创建IJSObjectReference以便我们可以加载我们的 JavaScript 函数。

然后,我们创建一个DotNetObjectReference.Create(new HelloHelper("Bruce Wayne"))的新实例,它是我们对HelloHelper类的新实例的引用,我们向其提供名字"Bruce Wayne"

现在,我们有objRef,我们将将其发送到 JavaScript 方法,但首先,我们加载 JavaScript 模块,然后调用JavaScriptMethod并传入我们的HelloHelper实例的引用。现在,JavaScript 的sayHello方法将运行hellohelperref.invokeMethodAsync('SayHello'),这将调用SayHelloHelper并返回一个包含"Hello, Bruce Wayne"的字符串。

我们还可以使用两种其他方法从 JavaScript 调用 .NET 函数。我们可以在组件实例上调用一个方法来触发一个动作,但这不是 Blazor Server 的推荐方法。我们还可以通过使用 helper 类在组件实例上调用一个方法。

由于从 JavaScript 调用 .NET 很少见,我们不会深入探讨两个示例。相反,我们将深入了解实现现有 JavaScript 库时需要考虑的事项。

实现现有的 JavaScript 库

在我看来,最好的方法是不移植 JavaScript 库。Blazor 需要保持 DOM 和渲染树的同步,而让 JavaScript 操作 DOM 可能会危及这一点。

大多数组件供应商,如 Telerik、Synfusion、Radzen,当然还有 Blazm,都有原生组件。它们不仅仅是包装 JavaScript,而是用 C# 明确为 Blazor 编写的。即使这些组件在某种程度上使用 JavaScript,目标也是将其保持在最低限度。

因此,如果您是库维护者,我的建议是编写一个本地的 Blazor 版本的库,尽量减少 JavaScript 的使用,最重要的是,不要强迫 Blazor 开发者编写 JavaScript 来使用您的组件。

一些组件将无法使用 JavaScript 实现,因为它们需要操作 DOM。

Blazor 在同步 DOM 和渲染树方面相当智能,但尽量避免操作 DOM。如果您需要使用 JavaScript 做某事,请确保将 HTML 标签放在操作区域之外,然后 Blazor 将跟踪该标签,而不会考虑标签内部的内容。

由于我们在工作场所很早就开始使用 Blazor,许多供应商还没有推出 Blazor 组件。我们需要一个图表组件,而且要快。在我们的上一个网站(Blazor 之前),我们使用了一个名为 Highcharts 的组件。

Highcharts 可以免费用于非商业项目。在构建我们的包装器时,我们有一些想要确保的事情。我们希望组件的工作方式与现有的类似,并且我们希望它尽可能简单易用。

让我们回顾一下我们做了什么。

首先,我们添加了对 Highcharts JavaScript 的引用:

<script src="img/highcharts.js"></script> 

然后,我们添加了一个 JavaScript 文件,如下所示:

export function loadHighchart(id, json) {
var obj = looseJsonParse(json);
    Highcharts.chart(id, obj);
};
export function looseJsonParse(obj) {
    return Function('"use strict";return (' + obj + ')')();
} 

loadHighchart 方法接受要转换为图表的 div 标签的 id 和配置的 JSON。

也有一种方法可以将 JSON 转换为 JSON 对象,以便它可以传递给 chart 方法。

Highchart Razor 组件看起来是这样的:

@using Microsoft.JSInterop
@inject Microsoft.JSInterop.IJSRuntime jsruntime
<div>
    <div id="@id"></div>
</div>
@code
{
    [Parameter] public string Json { get; set; }
    private string id { get; set; } = "Highchart" + Guid.NewGuid().ToString();
    protected override void OnParametersSet()
    {
        StateHasChanged();
        base.OnParametersSet();
    }
    IJSObjectReference jsmodule;
    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (!string.IsNullOrEmpty(Json))
        {
            jsmodule = await jsruntime.InvokeAsync<IJSObjectReference>("import", "/_content/Components/SharedComponents/HighChart.razor.js");
            await jsmodule.InvokeAsync<string>("loadHighchart", new object[] { id, Json });
        }
        await base.OnAfterRenderAsync(firstRender);
    }
} 

这里需要注意的重要事情是我们有两个嵌套的 div 标签:一个在外面,我们希望 Blazor 跟踪;一个在里面,Highcharts 将添加内容。

我们在配置的 JSON 中传递一个 JSON 参数,然后调用我们的 JavaScript 函数。我们在 OnAfterRenderAsync 方法中运行我们的 JavaScript 互操作,因为否则它可能会抛出异常,正如您可能从第四章理解基本 Blazor 组件中回忆的那样。

现在,唯一剩下的事情就是使用组件,它看起来像这样:

@rendermode InteractiveServer @page "/HighChartTest"
<HighChart Json="@chartjson">
</HighChart>
@code {
    string chartjson = @" {
    chart: { type: 'pie'},
    series: [{
        data: [{
            name: 'Does not look like Pacman',
            color:'black',
            y: 20,
        }, {
            name: 'Looks like Pacman',
            color:'yellow',
            y: 80
        }]
    }]
}";
} 

此测试代码将显示一个看起来像图 10.1的饼图:

一个黄色和黑色的饼图 描述自动生成

图 10.1:图表示例

我们已经了解了如何使 JavaScript 库与 Blazor 一起工作,所以如果需要,这是一个选项。

正如之前提到的,组件供应商正在投资 Blazor,因此他们可能已经拥有了我们所需要的,所以我们可能不需要在创建自己的组件库上投入时间。

WebAssembly 中的 JavaScript 互操作

本章中提到的所有内容都将非常适合 Blazor Server 和 Blazor WebAssembly。

但在 Blazor WebAssembly 中,我们直接访问 JSRuntime(因为所有代码都在浏览器内部运行)。直接访问将给我们带来巨大的性能提升。对于大多数应用程序,我们只进行一到两次 JavaScript 调用。性能实际上不会成为问题。但是,有些应用程序更侧重于 JavaScript,因此直接使用 JSRuntime 会更有益。

我们已经通过使用 IJSInProcessRuntimeIJSUnmarshalledRuntime 直接访问了 JSRuntime。但是,从 .NET 7 开始,这两个都已成为过时,我们现在有了更简洁的语法。

在 GitHub 仓库中,我向 SharedComponents 项目添加了一些文件,如果您想尝试代码。

我们将首先探讨如何在 .NET 中调用 JavaScript。请注意,由于我们的项目在服务器端进行预渲染,这些代码示例将无法工作(因为它们在服务器上运行时不会工作)。这些示例必须在仅使用 WebAssembly 的项目中运行或禁用预渲染。它们包含在 GitHub 上的源代码中供参考。

为了能够使用这些功能,我们需要在项目文件中启用它们,通过启用 AllowUnsafeBlocks

<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> 

.NET 到 JavaScript

为了展示差异,以下示例是本章前面提到的相同的 ShowAlert 函数。

Razor 文件看起来像这样:

@page "/nettojswasm"
@using System.Runtime.InteropServices.JavaScript
<h3>This is a demo how to call JavaScript from .NET</h3>
<button @onclick="ShowAlert">Show Alert</button>
@code {
    protected async void ShowAlert()
    {
        ShowAlert("Hello from .NET");
    }
    protected override async Task OnInitializedAsync()
    {
        await JSHost.ImportAsync("nettojs", "../JSInteropSamples/NetToJS.razor.js");
    }
} 

我们使用 JSHost 来导入 JavaScript 并将其命名为 "nettojs"。源生成器生成调用 JavaScript 的实现,并且为了确保它能识别出它应该做什么,我们需要在代码后添加一些代码。我们将在第十七章检查源生成器中更深入地探讨源生成器。代码后看起来像这样:

using System.Runtime.InteropServices.JavaScript;
namespace BlazorWebAssembly.Client.JSInteropSamples;
public partial class NetToJS
{
    [JSImport("showAlert", "nettojs")]
    internal static partial string ShowAlert(string message);
} 

JavaScript 文件看起来像这样:

export function showAlert(message) {
    return alert(message);
} 

我们在方法上添加一个 JSImport 属性,它将自动映射到 JavaScript 调用。

我认为这是一个更优雅的实现,并且速度更快。

接下来,我们将探讨如何从 JavaScript 调用 .NET。

JavaScript 到 .NET

当从 JavaScript 调用一个 .NET 方法时,有一个新的属性可以实现这一功能,称为 JSExport

Razor 文件实现如下:

@page "/jstostaticnetwasm"
@using System.Runtime.InteropServices.JavaScript
<h3>This is a demo how to call .NET from JavaScript</h3>
<button @onclick="ShowMessage">Show alert with message</button>
@code {
    protected override async Task OnInitializedAsync()
    {
        await JSHost.ImportAsync("jstonet", "../JSInteropSamples/JSToStaticNET.razor.js");
    }
} 

在演示的 JSExport 部分中,调用 JSHost.ImportAsync 不是必需的,但我们需要它来调用 JavaScript,以便我们可以从 JavaScript 中进行 .NET 调用。

同样,这里我们需要在代码背后的类中有类似这样的方法:

using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
namespace BlazorWebAssembly.Client.JSInteropSamples;
[SupportedOSPlatform("browser")]
public partial class JSToStaticNET
{
    [JSExport]
    internal static string GetAMessageFromNET()
    {
        return "This is a message from .NET";
    }
    [JSImport("showMessage", "jstonet")]
    internal static partial void ShowMessage();
} 

在这里,我们使用 SupportedOSPlatform 属性来确保此代码只能在浏览器上运行。

此演示的 JavaScript 部分看起来像这样:

export async function setMessage() {
    const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
    var exports = await getAssemblyExports("BlazorWebAssembly.Client.dll");
    alert(exports.BlazorWebAssembly.Client.JSInteropSamples.JSToStaticNET.GetAMessageFromNET());
}
export async function showMessage() {
    await setMessage();
} 

我们从 .NET 调用 showMessage JavaScript 函数,然后它将调用 setMessage 函数。

setMessage 函数使用 globalThis 对象来访问 .NET 运行时并获取对 getAssemblyExports 方法的访问权限。

它将检索我们程序集的所有导出,然后运行该方法。.NET 方法将返回 "This is a message from .NET" 字符串,并在一个警告框中显示该字符串。

尽管我更喜欢不在我的 Blazor 应用程序中调用任何 JavaScript 代码,但我非常喜欢能够轻松地在 .NET 代码和 JavaScript 代码之间建立桥梁的能力。

摘要

本章向我们介绍了从 .NET 调用 JavaScript 以及从 JavaScript 调用 .NET 的方法。在大多数情况下,我们不需要进行 JavaScript 调用,而且可能性很大,Blazor 社区或组件供应商已经为我们解决了这个问题。

我们还探讨了如果需要,如何移植现有的库。

在下一章中,我们将继续探讨状态管理。

加入我们的 Discord 社区

加入我们的 Discord 空间,与作者和其他读者进行讨论:

packt.link/WebDevBlazor3e

二维码

第十一章:管理状态 – 第二部分

在本章中,我们继续探讨状态管理。大多数应用程序都以某种形式管理状态。

状态只是以某种方式持久化的信息。它可以是存储在数据库中的数据,会话状态,甚至存储在 URL 中的内容。

用户状态存储在内存中,无论是在网页浏览器还是在服务器上。它包含组件层次结构和最近渲染的 UI(渲染树)。它还包含组件实例中的值或字段和属性,以及依赖注入中服务实例存储的数据。

如果我们调用 JavaScript,我们设置的值也会存储在内存中。Blazor Server 依赖于电路(SignalR 连接)来保持用户状态,而 Blazor WebAssembly 依赖于浏览器的内存。但是,当我们混合这两种状态时,状态管理就会变得有些复杂。如果我们重新加载页面,电路和内存都会丢失。切换页面也是如此;如果没有更多的InteractiveServer组件在页面上,SignalR 连接将被终止,状态丢失。管理状态不是关于处理连接或连接问题,而是关于我们如何在重新加载网页的情况下保持数据。

在页面导航或会话之间保存状态可以改善用户体验,可能是销售与否的区别。想象一下重新加载页面,购物车中的所有项目都消失了;你很可能不会再在那里购物。

现在想象一下,一周或一个月后返回页面,所有那些东西仍然在那里。

本章将涵盖以下主题:

  • 在服务器端存储数据

  • 在 URL 中存储数据

  • 实现浏览器存储

  • 使用内存状态容器服务

  • 状态管理框架

  • 根级级联值

我们已经讨论过,甚至实现了一些这些内容。让我们利用这个机会回顾我们已经讨论过的事情,以及介绍一些新技术。

技术要求

确保你已经阅读了前面的章节,或者以Chapter10文件夹作为起点。

你可以在这个章节的最终结果源代码github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter11 中找到。

如果你使用 GitHub 上的代码跳转到本章,请确保你在Settings文件中添加了Auth0账户信息。你可以在第八章认证和授权中找到说明。

在服务器端存储数据

在服务器端存储数据有许多不同的方式。唯一需要记住的是,Blazor WebAssembly(或InteractiveWebAssembly)始终需要一个 API。Blazor Server(或InteractiveServer)不需要 API,因为我们可以直接访问服务器端资源。

我与许多开发人员就 API 或直接访问进行了讨论,这些都归结为你打算如何使用应用程序。如果你正在构建 Blazor Server 应用程序,并且没有兴趣迁移到 Blazor WebAssembly,我可能会选择直接访问,就像我们在MyBlog项目中做的那样。

尽管如此,我并不会在组件中直接进行数据库查询。我会将它们保留在 API 中,只是不是 Web API。正如我们所看到的,在第七章 创建 API中,将那些 API 函数公开在 API 中,并不需要很多步骤。我们始终可以从直接服务器访问开始,如果我们想的话,再转向 API。

当涉及到存储数据时,我们可以将其保存在 Blob 存储、键值存储、关系数据库、文档数据库、表存储中等。

可能性是无限的。如果.NET 可以与该技术通信,我们就能使用它。

在 URL 中存储数据

初看起来,这个选项可能听起来很可怕,但事实并非如此。在这种情况下,数据可以是博客帖子 ID 或页码,如果我们使用分页的话。通常,你想要在 URL 中保存的东西是你希望以后能够链接到的东西,例如在我们的例子中是博客帖子。

要从 URL 中读取参数,我们使用以下语法:

@page "/posts/{PageNumber:int}" 

URL 由posts后跟帖子的页码(用于浏览博客帖子)组成。

要找到那个特定的路由,PageNumber必须是一个整数;否则,路由将无法找到。

我们还需要一个同名的public参数:

[Parameter]
public int PageNumber{ get; set; } 

如果我们在 URL 中存储数据,我们需要确保使用OnParametersSetOnParametersSetAsync方法;否则,如果更改参数,数据将不会重新加载。如果参数更改,Blazor 不会再次运行OnInitializedAsync

这就是为什么我们的post.razor组件在OnParametersSet中加载基于 URL 参数变化的内容,并在OnInitializedAsync中加载不受参数影响的内容。

我们可以通过指定它们为可空类型来使用可选参数,如下所示:

@page "/post/{PageNumber:int?}" 

因此,这个路由将匹配“/post/”和“/post/42”等。

路由约束

当我们指定参数的类型时,这被称为路由约束。我们添加约束,以便只有在参数值可以转换为指定的类型时,匹配才会发生。

以下是一些可用的约束:

  • bool

  • datetime

  • decimal

  • float

  • guid

  • int

  • long

URL 元素将被转换为C#对象。因此,在将它们添加到 URL 时使用不变文化是很重要的。string不是列表的一部分,因为这已经是默认行为。

使用查询字符串

到目前为止,我们只讨论了在page指令中指定的路由,但我们也可以从查询字符串中读取数据。

NavigationManager为我们提供了访问 URI 的权限,因此通过使用此代码,我们可以访问查询字符串参数:

@inject NavigationManager Navigation
@code{
var query = new Uri(Navigation.Uri).Query;
} 

我们不会深入探讨这个问题,但现在我们知道,如果我们需要访问查询字符串参数,这是可能的。

我们也可以使用类似这样的属性来访问query参数:

[Parameter, SupplyParameterFromQuery(Name = "parameterName")]
public string ParameterFromQuery { get; set; } 

这种语法更易于使用。

数据在 URL 中存在并不意味着存储数据。如果我们导航到另一个页面,我们需要确保包含新的 URL;否则,它将会丢失。如果我们想存储不需要每次都包含在 URL 中的数据,我们可以使用浏览器存储。

实现浏览器存储

浏览器有多种不同的方式在网页浏览器中存储数据。它们根据我们使用的数据类型而有所不同。本地存储的范围限定在用户的浏览器窗口。即使用户重新加载页面或关闭网页浏览器,数据仍然会被保存。

数据也会在标签页之间共享。会话存储的范围限定在浏览器标签页;如果你重新加载该标签页,数据将被保存,但如果你关闭该标签页,数据将会丢失。SessionsStorage在某种程度上更安全使用,因为我们避免了由于多个标签页操作存储中的相同值而可能出现的 bug 风险。

为了能够访问浏览器存储,我们需要使用 JavaScript。幸运的是,我们不需要自己编写代码。

在.NET 5 中,微软引入了受保护浏览器存储,它使用 ASP.NET Core 中的数据保护,但在 WebAssembly 中不可用。然而,我们可以使用一个名为Blazored.LocalStorage的开源库,它既可以用于 Blazor Server,也可以用于 Blazor WebAssembly。

但我们在这里是为了学习新事物,对吧?

因此,让我们实现一个接口,这样我们就可以根据我们使用的托管模型使用两种版本。这个实现有一个问题。如果我们以AutoMode运行,不同托管模型之间的状态将不会共享。解决方案是在两种实现中都坚持使用Blazored.LocalStorage。但为了展示实现之间的差异,我们将在这个案例中同时进行。请注意,这是在用户的计算机上以明文形式存储的,所以请小心存储的内容。

创建一个接口

首先,我们需要一个可以读取和写入存储的接口:

  1. SharedComponents项目中,创建一个名为Interfaces的新文件夹。

  2. 在新文件夹中,创建一个名为IBrowserStorage.cs的新类。

  3. 将文件中的内容替换为以下代码:

    namespace SharedComponents.Interfaces;
    public interface IBrowserStorage
    {
        Task<T?> GetAsync<T>(string key);
        Task SetAsync(string key, object value);
        Task DeleteAsync(string key);
    } 
    

现在我们有一个包含getsetdelete方法的接口。

实现 Blazor Server(InteractiveServer)

对于 Blazor Server,我们将使用受保护浏览器存储:

  1. BlazorWebApp项目中,添加一个名为Services的新文件夹。

  2. 在新文件夹中,创建一个名为BlogProtectedBrowserStorage.cs的新类。

    (我意识到命名有些过度,但这将有助于我们区分 Blazor Server 和 Blazor WebAssembly 实现,因为我们很快将创建另一个实现。)

  3. 打开新文件并添加以下using语句:

    using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
    using SharedComponents.Interfaces; 
    
  4. 将类替换为以下内容:

    public class BlogProtectedBrowserStorage : IBrowserStorage
    {
        ProtectedSessionStorage Storage { get; set; }
        public BlogProtectedBrowserStorage(ProtectedSessionStorage storage)
        {
            Storage = storage;
        }
        public async Task DeleteAsync(string key)
        {
            await Storage.DeleteAsync(key);
        }
        public async Task<T?> GetAsync<T>(string key)
        {
            var value = await Storage.GetAsync<T>(key);
            return value.Success ? value.Value : default(T);
        }
        public async Task SetAsync(string key, object value)
        {
            await Storage.SetAsync(key, value);
        }
    } 
    

    BlogProtectedBrowserStorage 类实现了 IBrowserStorage 接口以用于受保护的浏览器存储。我们注入一个 ProtectedSessionStorage 实例并实现了 setgetdelete 方法。

  5. Program.cs 文件中,添加以下命名空间:

    using SharedComponents.Interfaces;
    using BlazorWebApp.Services; 
    
  6. AddInteractiveWebAssemblyComponents(); 结尾的行下方添加以下内容:

    builder.Services.AddScoped<IBrowserStorage,BlogProtectedBrowserStorage>(); 
    

我们正在配置 Blazor,当注入 IBrowserStorage 时返回 BlogProtectedBrowserStorage 实例。

这与我们对 API 所做的是相同的。根据平台注入不同的实现。

实现 WebAssembly(交互式 WebAssembly)

对于 Blazor WebAssembly,我们将使用 Blazored.SessionStorage

  1. BlazorWebApp.Client 项目中,添加对 Blazored.SessionStorageNuGet 引用。

  2. 添加一个名为 Services 的新文件夹。

  3. 在新文件夹中,创建一个名为 BlogBrowserStorage.cs 的新类。

  4. 打开新文件,并将内容替换为以下代码:

    using Blazored.SessionStorage;
    using SharedComponents.Interfaces;
    namespace BlazorWebApp.Client.Services;
    public class BlogBrowserStorage : IBrowserStorage
    {
        ISessionStorageService Storage { get; set; }
        public BlogBrowserStorage(ISessionStorageService storage)
        {
            Storage = storage;
        }
        public async Task DeleteAsync(string key)
        {
            await Storage.RemoveItemAsync(key);
        }
        public async Task<T?> GetAsync<T>(string key)
        {
            return await Storage.GetItemAsync<T>(key);
        }
        public async Task SetAsync(string key, object value)
        {
            await Storage.SetItemAsync(key, value);
        }
    } 
    

    ProtectedBrowserStorageBlazored.SessionStorage 的实现方式非常相似。方法名称不同,但参数相同。

  5. Program.cs 文件中,添加以下命名空间:

    using Blazored.SessionStorage;
    using SharedComponents.Interfaces;
    using BlazorWebApp.Client.Services; 
    
  6. await builder.Build().RunAsync(); 之上添加以下内容:

    builder.Services.AddBlazoredSessionStorage();
    builder.Services.AddScoped<IBrowserStorage, BlogBrowserStorage>(); 
    

AddBlazoredSessionStorage 扩展方法将所有内容连接起来,以便我们可以开始使用浏览器会话存储。

然后我们添加 IBrowserStorage 的配置,就像我们在服务器上所做的那样,但在这个情况下,当我们请求依赖注入 IBrowserStorage 时,我们返回 BlogBrowserStorage

实现共享代码

我们还需要实现一些调用我们刚刚创建的服务的代码:

  1. SharedComponents 项目中,打开 Pages/Admin/BlogPostEdit.razor 文件。我们将对该文件进行一些修改。

  2. 注入 IBrowserStorage

    @inject SharedComponents.Interfaces.IBrowserStorage _storage 
    
  3. 由于我们只能在执行动作(如点击)或 OnAfterRender 方法中运行 JavaScript 调用,让我们创建一个 OnAfterRenderMethod

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && string.IsNullOrEmpty(Id))
        {
            var saved = await _storage.GetAsync<BlogPost>("EditCurrentPost");
            if (saved != null)
            {
               Post = saved;
       StateHasChanged();
            }
        }
        await base.OnAfterRenderAsync(firstRender);
    } 
    

    当我们加载组件且 Idnull 时,这意味着我们正在编辑一个新文件,然后我们可以检查浏览器存储中是否有保存的文件。

    此实现只能在草稿中有一个文件,并且只保存新帖子。如果我们编辑现有的帖子,则不会保存这些更改。

  4. 我们需要将 UpdateHTML 方法改为异步。将方法修改如下:

    protected async Task UpdateHTMLAsync()
    {
        if (!string.IsNullOrEmpty(Post.Text))
        {
            markDownAsHTML = Markdig.Markdown.ToHtml(Post.Text, pipeline);
            if (string.IsNullOrEmpty(Post.Id))
            {
                await _storage.SetAsync("EditCurrentPost", Post);
            }
        }
    } 
    
  5. 如果博客帖子的 Idnull,我们将把帖子存储在浏览器存储中。确保将所有对 UpdateHTML 的引用更改为 UpdateHTMLAsync

    确保在 OnParametersSetAsync 方法中也等待调用,如下所示:

    await UpdateHTMLAsync(); 
    
  6. 此实现存在一个问题:我们目前正在预渲染我们的组件。当我们预渲染时,没有连接到网络浏览器。没有状态可以检索。简单来说,我们需要禁用此组件的预渲染功能以使其工作。所以,让我们这么做吧!

  7. @rendermode InteractiveServer更改为:

    @rendermode @(new InteractiveServerRenderMode(prerender: false)) 
    

我们完成了。现在是时候测试实现:

  1. 通过按Ctrl + F5键运行项目。

  2. 登录到网站(这样我们就可以访问管理工具)。

  3. 点击博客文章然后点击新建博客文章

  4. 在框中输入任何内容,一旦我们在文本区域中输入一些内容,它就会将文章保存到存储中。

  5. 点击博客文章(这样我们就从我们的博客文章中导航出去)。

  6. 点击新建博客文章,所有信息仍然保留。

  7. F12键以查看浏览器开发者工具。点击应用程序 | 会话存储 | https://localhost:portnumber

    你应该看到一个键为EditCurrentPost的文章,该文章的值应该是一个加密字符串,如图 11.1所示:

    图 11.1 – 加密的受保护浏览器存储

    图 11.1:加密的受保护浏览器存储

    让我们接下来测试 Blazor WebAssembly(InteractiveWebAssembly)。

  8. 再次打开EditPost.razor文件,将@rendermode @(new InteractiveServerRenderMode(prerender: false))更改为:

    @rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) 
    
  9. 你可能需要清理和重新构建你的项目才能使其工作。

  10. 登录到网站(这样我们就可以访问管理工具)。

  11. 点击博客文章然后新建博客文章。你可能注意到页面加载和组件显示之间存在延迟。这是 WebAssembly 的初始加载时间,用于启动一切。

  12. 在框中输入任何内容,一旦我们在文本区域中输入一些内容,它就会将文章保存到存储中。

  13. 点击博客文章(这样我们就从我们的博客文章中导航出去)。

  14. 点击新建博客文章,所有信息应该仍然保留在那里。

  15. F12键以查看浏览器开发者工具。点击应用程序 | 会话存储 | https://localhost:portnumber

你应该看到一个键为EditCurrentPost的文章,该文章的值应该是一个 JSON 字符串,如图 11.2所示。

如果我们更改存储中的数据,它也会在应用程序中更改,所以请记住,这是纯文本,最终用户可以操作数据:

图 11.2 – 未受保护的浏览器存储

图 11.2:未受保护的浏览器存储

现在,我们已经为 Blazor Server 实现了受保护的浏览器存储,为 Blazor WebAssembly 实现了会话存储。我们可以在需要的地方混合和匹配托管模型,这是.NET 8 的一个真正惊人的功能。

我们只剩下一种方法可以尝试,让我们让它变得最有趣。

使用内存状态容器服务

当涉及到内存状态容器时,我们只需使用依赖注入来保持服务实例在内存中的预定时间(作用域、单例或瞬态)。

第四章理解基本 Blazor 组件中,我们讨论了依赖注入的作用域如何与 Blazor Server 和 Blazor WebAssembly 不同。在本节中,对我们来说最大的区别是 Blazor WebAssembly 在浏览器内部运行,并且没有与服务器或其他用户的连接。

为了展示内存状态的工作方式,我们将做一些可能对博客来说有点过度的事情,但看到它会很酷。当我们编辑我们的博客帖子时,我们将实时更新连接到我们博客的所有网络浏览器(我确实说过过度)。

根据宿主的不同,我们可能需要以不同的方式实现这一点。让我们从 Blazor Server 开始。

在 Blazor Server 上实现实时更新

Blazor Server 的实现也可以用于 Blazor WebAssembly。由于 WebAssembly 在我们的浏览器中运行,它只会通知连接到该站点的用户,这将是您自己。但了解这一点可能很好,因为同样的东西在 Blazor Server 和 Blazor WebAssembly 中都可以工作:

  1. SharedComponents项目中,在Interfaces文件夹中,创建一个名为IBlogNotificationService.cs的接口。

  2. 添加以下代码:

    using Data.Models;
    namespace SharedComponents.Interfaces;
    public interface IBlogNotificationService
    {
        event Action<BlogPost>? BlogPostChanged;
        Task SendNotification(BlogPost post);
    } 
    

    我们有一个可以在博客帖子更新时订阅的动作,以及一个在我们更新帖子时可以调用的方法。

  3. BlazorWebServer项目的Services文件夹中,添加一个名为BlazorServerBlogNotificationService.cs的新类。

    给类起一个包含BlazorServer的名字可能看起来不必要,但它确保我们可以轻松地区分这些类。

    将内容替换为以下代码:

    using SharedComponents.Interfaces;
    using Data.Models;
    namespace BlazorServer.Services;
    public class BlazorServerBlogNotificationService : IBlogNotificationService
    {
        public event Action<BlogPost>? BlogPostChanged;
        public Task SendNotification(BlogPost post)
        {
            BlogPostChanged?.Invoke(post);
            return Task.CompletedTask;
        }
    } 
    

    这里的代码相当简单。如果我们调用SendNotification,它将检查是否有人正在监听BlogPostChanged动作,并决定是否触发该动作。

  4. Program.cs中添加依赖注入:

    builder.Services.AddSingleton<IBlogNotificationService, BlazorServerBlogNotificationService>(); 
    

    每当我们请求IBlogNotificationService类型的实例时,我们都会得到BlazorServerBlogNotificationService的一个实例。

    我们将这个依赖注入作为单例。我必须强调这一点。当使用 Blazor Server 时,这将是所有用户的相同实例,因此我们必须在使用Singleton时格外小心。

    在这种情况下,我们希望该服务通知我们博客的所有访客博客帖子已更改。

  5. SharedComponents项目中打开Post.razor

  6. 在页面顶部(或接近顶部)添加以下代码:

    @using SharedComponents.Interfaces
    @inject IBlogNotificationService _notificationService
    @implements IDisposable 
    

    我们为IBlogNotificationService添加了依赖注入,并且还需要实现IDisposable以防止任何内存泄漏。

    OnInitializedAsync方法的顶部添加以下代码:

    _notificationService.BlogPostChanged += PostChanged; 
    

    我们向事件添加了一个监听器,以便我们知道何时应该更新信息。

  7. 我们还需要PostChanged方法,所以添加以下代码:

    private async void PostChanged(BlogPost post)
    {
        if (BlogPost?.Id == post.Id)
        {
            BlogPost = post;
            await InvokeAsync(()=>this.StateHasChanged());
        }
    } 
    

    如果参数具有与我们当前查看的帖子相同的 ID,则将事件中的内容替换为帖子并调用StateHasChanged

    由于这发生在另一个线程上,我们需要使用InvokeAsync调用StateHasChanged,以确保它在 UI 线程上运行。

    我们还需要通过实现Dispose方法来停止监听更新。添加以下内容:

    void IDisposable.Dispose()
    {
        _notificationService.BlogPostChanged -= PostChanged;
    } 
    

    我们移除事件监听器以防止任何内存泄漏。

    Post组件目前是一个静态渲染组件。我们没有任何交互性,所以让我们启用它。

    将以下内容添加到组件中:

    @rendermode InteractiveServer 
    
  8. SharedComponents项目中,打开Pages/Admin/BlogPostEdit.Razor文件。

  9. 当我们修改我们的博客文章时,我们还需要发送一个通知。在文件顶部添加以下内容:

    @using SharedComponents.Interfaces
    @inject IBlogNotificationService _notificationService 
    

    我们添加一个命名空间并注入我们的通知服务。

  10. UpdateHTMLAsync方法中,在!string.IsNullOrEmpty(Post.Text) if语句下方添加以下内容:

    await _notificationService.SendNotification(Post); 
    

    每次我们更改内容时,现在都会发送一个通知,表明博客文章已更改。我确实意识到在保存帖子时这样做可能更有意义,但这会使演示更加酷。

    让我们从测试InteractiveServer开始。在BlogPortEditPage.razor中,将@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))更改为:

    @rendermode @(new InteractiveServerRenderMode(prerender: false)) 
    
  11. 通过按Ctrl + F5运行项目。

  12. 复制 URL 并在另一个网络浏览器中打开。现在我们应该有两个网络浏览器窗口打开,显示我们的博客。

  13. 在第一个窗口中,打开一个博客文章(哪个都行),然后在第二个窗口中登录并编辑同一篇博客文章。

当我们在第二个窗口中更改博客文章的文本时,更改应该会实时反映在第一个窗口中。

我总是很惊讶,没有使用 Blazor 实现的功能可能有点棘手,但现在只需要 10 步(不算测试),如果我们没有为下一步做准备,步骤会更少。

接下来,我们将为 Blazor WebAssembly 实现相同的功能,但 Blazor WebAssembly 是在用户的网络浏览器中运行的。与 Blazor Server 一样,它没有内置的实时通信。

实现 Blazor WebAssembly 的实时更新

我们已经有很多东西准备好了。我们只需要添加一个实时消息系统。由于 SignalR 既容易实现又很棒,让我们使用它。

我第一次使用 SignalR 时,我的第一个想法是,“等等,这不可能那么简单。我一定是忘记了什么,或者缺少了什么”。希望我们现在会有同样的体验。

让我们看看这今天是否仍然成立:

  1. BlazorWebApp项目中,添加一个名为Hubs的新文件夹。

  2. 在新文件夹中,创建一个名为BlogNotificationHub.cs的类。

  3. 将代码替换为以下内容:

    using Data.Models;
    using Microsoft.AspNetCore.SignalR;
    namespace BlazorWebApp.Hubs;
    public class BlogNotificationHub : Hub
    {
        public async Task SendNotification(BlogPost post)
        {
            await Clients.All.SendAsync("BlogPostChanged", post);
        }
    } 
    

    该类继承自Hub类。有一个名为SendNotification的方法。记住这个名字;我们很快就会回到它。

    我们调用Clients.All.SendAsync,这意味着我们将发送一个名为BlogPostChanged的消息,包含博客文章的内容。

    名称BlogPostChanged也很重要,所以请记住这一点。

  4. Program.cs文件中,添加以下内容:

    builder.Services.AddSignalR(); 
    

    这添加了 SignalR。由于这个项目是托管模型的混合,我们已经有权限访问 SignalR。

  5. 添加以下命名空间:

    using BlazorWebApp.Hubs; 
    
  6. app.MapRazorComponents<App>()之上添加:

    app.MapHub<BlogNotificationHub>("/BlogNotificationHub"); 
    

    在这里,我们配置BlogNotificationHub应该使用哪个 URL。在这种情况下,我们使用与 hub 名称相同的 URL。

    这里的 URL 也很重要。我们将在稍后使用它。

  7. BlazorWebApp.Client中添加对Microsoft.AspNetCore.SignalR.Client NuGet包的引用。

  8. Services文件夹中,创建一个名为BlazorWebAssemblyBlogNotificationService.cs的类。

    在此文件中,我们将实现 SignalR 通信。

  9. 添加以下命名空间:

    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.SignalR.Client;
    using Data.Models;
    using SharedComponents.Interfaces; 
    
  10. 添加此类:

    public class BlazorWebAssemblyBlogNotificationService : IBlogNotificationService, IAsyncDisposable
    {
        public BlazorWebAssemblyBlogNotificationService(NavigationManager navigationManager)
        {
            _hubConnection = new HubConnectionBuilder()
            .WithUrl(navigationManager.ToAbsoluteUri("/BlogNotificationHub"))
            .Build();
            _hubConnection.On<BlogPost>("BlogPostChanged", (post) =>
            {
                BlogPostChanged?.Invoke(post);
            });
            _hubConnection.StartAsync();
        }
        private readonly HubConnection _hubConnection;
        public event Action<BlogPost>? BlogPostChanged;
    
        public async Task SendNotification(BlogPost post)
        {
            await _hubConnection.SendAsync("SendNotification", post);
        }
        public async ValueTask DisposeAsync()
        {
            await _hubConnection.DisposeAsync();
        }
    } 
    

    这里发生了很多事情。这个类实现了IBlogNotificationServiceIAsyncDisposable

    在构造函数中,我们使用依赖注入来获取NavigationManager,这样我们就可以确定服务器的 URL。

    然后,我们配置到 hub 的连接。然后,我们指定 hub 的 URL;这应该与我们在步骤 7中指定的相同。

    现在,我们可以配置 hub 连接以监听事件。在这种情况下,我们监听BlogPostChanged事件,这与我们在步骤 3中指定的名称相同。当有人发送事件时,我们将指定的方法将运行。

    在这种情况下,该方法触发我们在IBlogNotificationService中定义的事件。然后,我们开始建立连接。由于构造函数不能是异步的,所以我们不会等待StartAsync方法。

    IBlogNotificationService还实现了SendNotification方法,我们在 hub 上触发具有相同名称的事件,这将导致 hub 向所有已连接客户端发送BlogPostChanged事件。

    我们最后要确保我们处理了 hub 连接。

  11. Program.cs文件中,我们需要配置依赖注入。在await builder.Build().RunAsync();之上,添加以下内容:

    builder.Services.AddSingleton<IBlogNotificationService, BlazorWebAssemblyBlogNotificationService>(); 
    
  12. 这里事情变得有点复杂,因为我们根据是否使用 InteractiveServer 或 InteractiveWebAssembly 有不同的实现。我们需要确保以相同的方式运行EditPostPost组件。在这种混合场景中,始终使用 SignalR 连接是一个更好的选择,因为这样我们可以使用相同的实现,无论托管模型如何。在SharedComponents项目中,打开Pages/Admin/BlogPostEdit.razor并将@rendermode @(new InteractiveServerRenderMode(prerender: false))更改为:

    @rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)). 
    

    你可能需要清理并重新构建解决方案才能使其工作。

  13. 打开Post.razor并执行相同操作;将@rendermode InteractiveServer更改为:

    @rendermode InteractiveWebAssembly. 
    

    现在,是时候进行测试了,通过按Ctrl + F5来运行项目。

  14. 复制 URL 并打开另一个网络浏览器。现在,我们应该有两个网络浏览器窗口打开,显示博客。

  15. 在第一个窗口中,打开一个博客文章(任何一篇都可以),在第二个窗口中,登录并编辑同一篇博客文章。

当我们在第二个窗口中更改博客文章的文本时,该更改应该实时反映在第一个窗口中。

13 个步骤(不包括测试)中,我们实现了服务器和客户端之间的实时通信,一个运行在网页浏览器内的 .NET 代码的 Blazor WebAssembly 客户端。

而且不需要 JavaScript!

状态管理框架

谈到 JavaScript,在 Angular、React 等等 JavaScript 框架的世界中,有一些我们可以用来管理状态的框架(例如 ReduxngRX)。Blazor 也是如此。非常简单,我们有一个可以通过方法更改的状态;如果状态发生变化,监听该变化的组件将会收到通知。

对于 Blazor,有大量类似的框架。我个人从未使用过框架,而是构建了一个 Singleton 服务并将我的组件连接到该服务(基本上就是这些框架所做的事情)。

如果你想要深入了解,可以查看 Fluxor 或 Blazor-State。在组件之间共享状态的另一种方法是称为根级级联值。

根级级联值

根级级联值是 .NET 8 中的一个新特性。这是在组件之间以及不同渲染模式之间共享状态的一个好方法。它将自动添加一个级联值;我们已经使用了这个特性,然后添加了 AddCascadingAuthenticationState(),它在后台使用根级级联值。

然而,这并不在 InteractiveServer 和 InteractiveWebAssembly 之间共享值,但它为我们提供了一种在不使用依赖注入的情况下在组件之间共享状态的方法。

真正美妙的是,如果值发生变化,它将自动更改参数并触发组件的重渲染。组件内部不需要特殊代码。但是订阅值变化确实有成本,所以使用根级级联值时要小心,不要使用太多。

使用方式可能看起来像这样:

@(Preferences?.DarkTheme)
@code {
    [CascadingParameter(Name = "Preferences")]
    public Preferences Preferences { get; set; }
} 

Program.cs 中:

builder.Services.AddCascadingValue<Preferences>(sp =>
{
    var preferences = new Preferences { DarkTheme = true };
    var source = new CascadingValueSource<Preferences>("Preferences", preferences, isFixed: false);
    return source;
}); 

可以通过在 CascadingValueSource 上调用 NotifyChangedAsync 方法来更新值。一个实现可能看起来像这样:

builder.Services.AddCascadingValue<Preferences>(sp =>
{
    var preferences = new Preferences { DarkTheme = true };
    var source = new CascadingValueSource<Preferences>("Preferences", preferences, isFixed: false);
    if (preferences is INotifyPropertyChanged changed)
        changed.PropertyChanged += (sender, args) => source.NotifyChangedAsync();
    return source;
}); 

在这里,我们使用 INotifyPropertyChanged 接口在更改属性时调用 NotifyChangedAsync。如果你想要进一步探索,你可以在 GitHub 上找到一个完整的示例。

摘要

在这一章中,我们学习了如何在我们的应用程序中处理状态,以及如何使用本地存储来存储数据,无论是加密的还是未加密的。我们探讨了不同的方法,并确保包括 SignalR 以便能够使用与服务器之间的实时通信。

几乎所有应用程序都需要以某种形式保存数据。这可能包括设置或偏好。本章中我们讨论的内容是最常见的,但我们也应该知道,有许多开源项目我们可以用来持久化状态。我个人更喜欢组件在需要时从数据库中加载数据,这样就可以自给自足,而不必依赖于状态来自其他地方或某处。这种方法在过去一直为我服务得很好。

在下一章中,我们将探讨调试。希望你还没有必要知道如何调试!

第十二章:调试代码

在本章中,我们将探讨调试。Blazor 的调试体验很好;希望你在本书前面的章节中没有遇到难题,不得不跳到这一章。

调试代码是解决错误、理解工作流程或查看特定值的一种极好方式。Blazor 有三种不同的调试代码方式,我们将逐一查看。

在本章中,我们将涵盖以下内容:

  • 让事情出问题

  • Blazor Server 调试

  • Blazor WebAssembly 调试

  • 在浏览器中调试 Blazor WebAssembly

  • 热重载

技术要求

确保你已经遵循了前面的章节,或者使用 Chapter11 文件夹作为起点。

你可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter12 找到本章末尾结果的源代码。

如果你使用 GitHub 中的代码跳转到本章,请确保你已经在设置文件中添加了 Auth0 账户信息。你可以在 第八章身份验证和授权 中找到说明。

要调试某个东西,我们首先应该让它出点问题!

让事情出问题

艾德加·W·迪杰斯特拉曾经说过,

“如果调试是移除软件错误的过程,那么编程就必须是放置它们的过程。”

在这个部分中,这绝对是真实的,因为我们将添加一个会抛出异常的页面:

  1. SharedComponents 项目中,在 Pages 文件夹中,创建一个新的 Razor 组件,命名为 ThrowException.razor

  2. 将文件内容替换为以下代码块:

    @page "/ThrowException"
    @rendermode @(new InteractiveServerRenderMode(prerender: false))
    <button @onclick="@(()=> {throw new Exception("Something is broken"); })">Throw an exception</button> 
    

这个页面显示了一个按钮,当你按下它时,它会抛出一个异常。

太好了!我们找到了我们应用程序的伊万·德拉戈(它想打败我们,但我们可能用一些花哨的调试技巧打败它)。

下一步是查看 Blazor Server 调试。

Blazor Server 调试

如果你以前调试过任何 .NET 应用程序,你会感到非常熟悉。别担心;如果你还没有,我们会带你了解。调试 Blazor Server 就像我们可能预期的那样,并且是我们将要涵盖的三种不同类型中最好的调试体验。

我通常将我的 Razor 页面放在共享库中,在构建我的项目时,我使用 Blazor Server 的原因有两个。首先,运行项目要快一些,其次,调试体验更好。

让我们试一试!

  1. F5 启动项目(这次带有调试)。

  2. 使用网络浏览器,导航到 https://localhost:端口号/throwexception(端口号可能不同)。

  3. F12 显示网络浏览器开发者工具。

  4. 在开发者工具中,点击 控制台

  5. 点击我们页面上的 抛出异常 按钮。

    到目前为止,Visual Studio 应该请求焦点,并且应该显示如图 图 12.1 所示的异常:

    图 12.1 – Visual Studio 中的异常

    图 12.1:Visual Studio 中的异常

  6. F5继续并切换回网页浏览器。我们现在应该能够在开发者工具中看到异常信息,如图图 12.2所示:图 12.2 – 网页浏览器中的异常

    图 12.2:网页浏览器中的异常

    正如我们在图 12.1图 12.2中看到的那样,我们在 Visual Studio 调试时以及在开发者工具中都会遇到异常。

    如果在生产应用程序中发生异常,这会使问题变得非常容易找到(但愿不会发生)——这个功能已经帮我们节省了很多时间。

    现在我们尝试一个断点:

  7. 在 Visual Studio 中,打开Pages/Home.razor

  8. LoadPosts方法的任何地方,通过点击最左边的边框(出现一个红色圆点)来设置断点。我们也可以通过按F9来添加断点。

  9. 返回网页浏览器并导航到https://localhost:portnumber/(端口号可能不同)。

Visual Studio 现在应该会触发断点,通过悬停在变量上,我们应该能够看到当前的值。

断点和异常调试都按预期工作。接下来,我们将看看如何调试 Blazor WebAssembly。

Blazor WebAssembly 调试

Blazor WebAssembly 当然也可以进行调试。有几件事情需要记住。像我们在博客中使用的InteractiveWebAssembly进行调试,将会与 Blazor Server 一样工作。断点和异常将按预期工作。然而,有一个选项可以将 Blazor WebAssembly 作为一个独立的应用程序运行。这会有一些不同。

为了能够玩转这个,我们需要添加另一个项目。

  1. 右键点击MyBlog 解决方案,选择添加新建项目…,然后选择Blazor WebAssembly 独立应用程序

  2. 将项目名称更改为BlazorWebAssemblyApp

  3. 保持默认值不变并点击创建

  4. 右键点击我们的BlazorWebAssemblyApp项目并选择设置为启动项目

  5. Pages文件夹中,打开Counter.razor并在currentCount++行上设置断点。

  6. 通过按F5来运行项目,看看会发生什么,断点被触发了。

这并不总是这样,我实际上对它工作得如此之好感到非常高兴。在.NET 的早期版本中,你必须点击另一个页面然后再回来才能触发断点。

Blazor WebAssembly 的调试是通过launchSettings.json文件中的以下代码行实现的:

"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 

但它在我们创建项目时已经为我们提供了,所以我们不需要手动添加。

现在我们来看看我们的异常会发生什么:

  1. 复制我们的ThrowException.razor文件并将其放入BlazorWebAssemblyApp/Pages文件夹。

  2. 在复制的文件中,删除@rendermode行。由于 WebAssembly 项目全是 WebAssembly,我们不需要指定渲染模式。

  3. 在网页浏览器中,导航到https://localhost:portnumber/throwexception

  4. 点击抛出异常按钮。

  5. 在 Visual Studio 中不会遇到未处理的异常。我们将在网页浏览器的开发工具中遇到异常,如图 12.3 所示:

图 12.3 – WebAssembly 错误

图 12.3:WebAssembly 错误

Blazor WebAssembly 的调试体验不如 Blazor Server 那么精致,但它已经足够精致,可以完成这项工作。

我们还剩下一种方法可以探索——在网页浏览器中进行调试。

在网页浏览器中调试 Blazor WebAssembly

Blazor WebAssembly 的第一次调试体验是能够在网页浏览器中进行调试:

  1. 在 Visual Studio 中,按 Ctrl + F5 启动项目(不进行调试)。

  2. 在网页浏览器中,按 Shift + Alt + D

    我们将获得一个错误消息,其中包含如何以调试模式启动网页浏览器的说明。

    我正在运行 Edge,因此启动 Edge 的方式可能如下所示:

    msedge --remote-debugging-port=9222 --user-data-dir="C:\Users\Jimmy\AppData\Local\Temp\blazor-edge-debug" --no-first-run https://localhost:5001/ 
    

    portuser-data-dir 的值将与上面的示例不同。从您的网页浏览器中复制命令。

  3. Win + R 并粘贴命令。

  4. 将会打开一个新的 Chrome 或 Edge 实例。在这个新实例中,按 Shift + Alt + D

  5. 我们现在应该看到一个包含来自我们项目的 C# 代码的选项卡。从这里,我们可以设置将被触发的断点并悬停在变量上。请确保在网页浏览器中只打开此选项卡(而不是多个选项卡)。

调试用户界面可以在图 12.4 中看到:

图 12.4 – 浏览器内调试的截图

图 12.4:浏览器内调试用户界面的截图

在浏览器中调试 C# 代码非常令人惊叹,但由于我们一直在 Visual Studio 中直接进行调试,我认为这种调试用途不大。

接下来,我们将探讨一些可能不属于调试但开发 Blazor 应用程序时很有用的内容。

热重载

在 Visual Studio 和 dotnet CLI 中,我们可以启用热重载。这意味着只要我们在应用程序中进行更改,我们的 Blazor 应用程序就会自动重新加载,并且(在大多数情况下)我们不会丢失状态。

要设置此功能,请执行以下操作:

  1. 在 Visual Studio 中,有一个小火焰图标。我们可以使用此按钮手动触发热重载

    只有当应用程序正在运行时(无论是否进行调试)才可点击。

  2. 选择文件保存时热重载选项。

  3. Ctrl + F5 启动项目。

  4. 在网页浏览器中,通过将 /counter 添加到 URL 来打开计数器页面。

  5. 修改 /Pages/Counter.razor 文件并点击保存

现在,我们的网页浏览器应该会重新加载,并且更改将显示出来。在撰写本文时,我的热重载在 Visual Studio 中运行时没有找到任何更改,但使用 dotnet watch 时它确实有效。

热重载确实节省了时间,而且非常神奇。无需重新编译项目、启动浏览器,只需在保存文件后几秒钟就能在浏览器中看到更改,这简直令人惊叹。然而,有些情况下我们的网站表现异常,这时我们就需要重新构建。因此,你需要记住,如果遇到无法解释的问题,你可能需要再次构建项目。

这也可以通过运行以下命令从命令行进行:

dotnet watch 

热重载随着每个版本的发布而越来越好。我通常运行 Visual Studio 的预览版以获得最佳体验,但这也可能有时有其缺点。

摘要

本章探讨了调试我们的 Blazor 应用程序的不同方法。总会有需要逐步检查代码以找到错误或查看发生了什么的时候。当这些时刻到来时,Visual Studio 提供了世界级的功能来帮助我们实现目标。

好处在于,无论是 Blazor 服务器还是 Blazor WebAssembly,调试 Blazor 应用程序都能按照预期从 Microsoft 产品中工作。我们得到的 C# 错误(在大多数情况下)都很容易理解和解决。

在下一章中,我们将探讨测试我们的 Blazor 组件。

第十三章:测试

在本章中,我们将探讨测试。为我们的项目编写测试将帮助我们快速开发。

我们可以运行测试以确保我们没有在最新的更改中破坏任何东西。此外,我们不必在测试组件上投入时间,因为所有这些都由测试完成。测试将提高产品的质量,因为我们知道之前工作正常的东西仍然按预期工作。

但为 UI 元素编写测试并不总是容易;最常见的方法是启动网站,使用点击按钮的工具,然后读取输出以确定是否正常工作。这种方法的优势在于我们可以测试我们的网站在不同的浏览器和设备上。缺点是通常需要花费大量时间来完成这些测试。我们需要启动网站,打开浏览器,验证测试,关闭浏览器,然后为下一个测试重复此过程。

我们也可以在 Blazor 中使用这种方法(就像任何 ASP.NET 网站一样),但与 Blazor 一起,我们在测试方面还有其他机会。

Steve Sanderson 为 Blazor 创建了一个测试框架的雏形,Microsoft MVP Egil Hansen 接手并继续了其开发。

Egil 的框架被称为 bUnit,并已成为 Blazor 社区中测试 Blazor 组件的行业标准。

本章涵盖了以下主题:

  • 什么是 bUnit?

  • 设置测试项目

  • 模拟 API

  • 编写测试

  • Blazm 扩展

技术要求

确保你已经阅读了前面的章节或使用 Chapter12 文件夹作为起点。

你可以在此章结果的源代码在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter13 找到。

如果你使用 GitHub 上的代码跳转到本章,请确保你已在设置文件中添加了 Auth0 账户信息。你可以在 第八章身份验证和授权 中找到说明。

什么是 bUnit?

如介绍中所述,一些测试会启动浏览器来测试页面/组件,但 bUnit 采用另一种方法。

bUnit 是专门为 Blazor 定制的。它可以使用 C# 或 Razor 语法定义和设置测试。它还可以模拟 JavaScript 互操作以及 Blazor 的身份验证和授权。为了使我们的组件更容易测试,有时我们需要从开始就考虑这些事情,或者对我们的代码进行一些小的修改。

bUnit 不依赖于浏览器,而是在内部渲染输出并将其暴露给我们,以便我们可以针对预定义的输出进行测试。这也是一个限制——我们不是在测试真实网站;我们是在测试组件,所以将其视为单元测试,而不是集成测试。

是时候让我们动手实践了,让我们创建一个测试项目。

设置测试项目

要能够运行测试,我们需要一个测试项目:

  1. 要安装 bUnit 模板,打开 PowerShell 并运行以下命令:

    dotnet new install bunit.template 
    
  2. 检查 bUnit 网页上的模板的最新版本:bunit.dev/

  3. 在 Visual Studio 中,右键单击 MyBlog 解决方案,然后选择 添加 | 新建项目

  4. 搜索 bUnit,在结果中选择 bUnit Test Project,然后点击 下一步。有时,找到模板需要一些时间,我们也可以将 项目类型 下拉菜单更改为 bUnit 来找到模板。我们可能需要重新启动 Visual Studio 才能找到它。

  5. 将项目命名为 MyBlog.Tests,保留位置不变,然后点击 下一步

  6. 选择 xUnit 作为单元测试框架和目标框架:.NET 8.0,然后点击 创建

太好了!我们现在有一个测试项目。在我们模拟 API 之前,让我们看看我们可用的不同方法,这样我们就可以了解 bUnit 是如何工作的。

MyBlog.Tests 中,我们应该有以下四个文件:

  • _Imports.razor 包含我们希望所有 Razor 文件都能访问的命名空间。

  • Counter.razor 是我们在 Blazor 模板中默认获得的相同 Counter 组件的副本。

  • CounterCSharpTest.cs 包含用 C#编写的测试。

  • CounterRazorTest.razor 包含用 Razor 编写的测试。

让我们从 CounterCSharpTest.cs 文件开始,它包含两个测试:一个检查计数器是否从 0 开始,另一个点击按钮并验证计数器现在是 1。这两个简单的测试对于测试 Counter 组件是有意义的。

CounterStartsAtZero 测试看起来像这样:

[Fact]
public void CounterStartsAtZero()
{
    // Arrange
var cut = RenderComponent<Counter>();
    // Assert that content of the paragraph shows counter
// at zero
    cut.Find("p").MarkupMatches("<p>Current count: 0</p>");
} 

让我们分解一下。Fact 属性告诉测试运行器这是一个 正常 测试,没有参数。我们也可以使用 Theory 属性告诉测试运行器 test 方法需要参数值,但在这个用例中我们不需要参数。

首先,我们安排测试。简单来说,我们设置进行测试所需的一切。Egil 使用 cut 作为组件的名称,这代表 待测试组件

在这种情况下,我们运行 RenderComponent 方法,并传入组件类型,即 Counter 组件。接下来,我们断言组件是否输出了正确的内容。我们使用 Find 方法找到第一个段落标签,然后验证 HTML 看起来像 <p>Current count: 0</p>

第二个测试稍微复杂一些,看起来像这样:

[Fact]
public void ClickingButtonIncrementsCounter()
{
    // Arrange
var cut = RenderComponent<Counter>();
    // Act - click button to increment counter
    cut.Find("button").Click();
    // Assert that the counter was incremented
    cut.Find("p").MarkupMatches("<p>Current count: 1</p>");
} 

与之前的测试一样,我们通过渲染我们的 Counter 组件开始安排。下一步是执行,我们点击按钮。我们寻找按钮,然后在 Counter 组件中点击按钮。只有一个按钮,所以在这种情况下,以这种方式寻找按钮是安全的。

然后是再次断言的时候了,我们以与上一个测试相同的方式检查标记,但这次我们寻找的是 1 而不是 0

另外还有一种替代方案,我们可以使用 Razor 语法编写测试。如果我们查看 CounterRazorTests.razor 文件,我们可以看到完全相同的测试,但语法不同:

 [Fact]
    public void CounterStartsAtZero()
    {
        // Arrange
var cut = Render(@<Counter />);
        // Assert that content of the paragraph shows counter at zero
        cut.Find("p").MarkupMatches(@<p>Current count: 0</p>);
    } 

实际上,只是我们渲染组件的方式不同。这做的是同样的事情,只是个人偏好的问题。我更喜欢使用 Razor 版本;它更容易阅读,并且在测试时添加参数到我们的组件也更简单。

现在,让我们运行测试并查看它们是否通过:

  1. 在 Visual Studio 中,通过使用 Ctrl + Q 搜索来调出 Test Explorer。我们也可以在 视图 | 测试资源管理器 中找到它。

  2. 在视图中点击 运行所有测试。测试资源管理器应该看起来像 图 13.1

图 13.1 – Visual Studio 测试资源管理器

图 13.1:Visual Studio 测试资源管理器

太棒了!现在,我们的第一个测试正在运行,并且希望它能通过。

接下来,我们将查看如何模拟 API。

模拟 API

测试我们的应用程序有不同的方法。API 测试超出了本书的范围,但我们仍然需要测试组件,这些组件依赖于 API。我们可以启动 API 并针对 API 进行测试,但在这个情况下,我们只对测试 Blazor 组件感兴趣。

然后,我们可以模拟 API 或创建一个不读取数据库但读取预定义数据集的 API 的假副本。这样,我们总能知道输出应该是什么。

幸运的是,我们为 API 创建的接口正是我们创建模拟 API 所需要的。

我们不会实现项目中所有测试的 100%,因此我们不需要模拟所有方法。请随意在章节末尾作为练习实现所有方法的测试。

我们有两种方法可以实现模拟 API。我们可以启动一个内存数据库,但为了保持简单,我们将选择另一种选项,在请求时生成帖子:

  1. MyBlog.Tests 项目中,将项目引用添加到 SharedComponentsBlazorWebApp 项目。

  2. 创建一个名为 BlogApiMock.cs 的新类。

  3. 添加以下命名空间:

    using Data.Models;
    using Data.Models.Interfaces;
    using System.Collections.Generic;
    using System.Threading.Tasks; 
    
  4. 实现 IBlogApi 接口;类应该看起来像这样:

    internal class BlogApiMock :IBlogApi
    {
    } 
    

    现在,我们将实现每个方法,以便我们可以获取数据。

  5. 对于 BlogPost,在类中添加以下代码:

    public async Task<BlogPost?> GetBlogPostAsync(string id)
    {
        BlogPost post = new()
        {
            Id = id,
            Text = $"This is a blog post no {id}",
            Title = $"Blogpost {id}",
            PublishDate = DateTime.Now,
            Category = await GetCategoryAsync("1"),
        };
        post.Tags.Add(await GetTagAsync("1"));
        post.Tags.Add(await GetTagAsync("2"));
        return post;
    }
    public Task<int> GetBlogPostCountAsync()
    {
        return Task.FromResult(10);
    }
    public async Task<List<BlogPost>?> GetBlogPostsAsync(int numberofposts, int startindex)
        {
            List<BlogPost> list = new();
            for (int a = 0; a < numberofposts; a++)
            {
                list.Add(await GetBlogPostAsync($"{startindex + a}"));
            }
            return list;
        } 
    

    当我们运行 GetBlogPostAsync 方法时,我们创建一个博客文章并填充我们可以稍后用于测试的预定义信息。对于获取博客文章列表也是同样的情况。

    我们还声称数据库中总共有 10 篇博客文章。

    对于类别,添加以下代码:

     public async Task<List<Category>?> GetCategoriesAsync()
        {
            List<Category> list = new();
            for (int a = 0; a < 10; a++)
            {
                list.Add(await GetCategoryAsync($"{a}"));
            }
            return list;
        }
        public Task<Category?> GetCategoryAsync(string id)
        {
            return Task.FromResult(new Category() { Id = id, Name = $"Category {id}" });
        } 
    

    这里,我们做同样的事情:我们创建名为 Category 后跟数字的类别。

  6. 对于注释,添加以下内容:

     public Task<List<Comment>> GetCommentsAsync(string blogPostId)
        {
            var comments= new List<Comment>
            {
                new Comment { BlogPostId = blogPostId, Date = DateTime.Now, Id = "Comment1", Name = "Rocket Raccoon", Text = "I really want that arm!" }
            };
            return Task.FromResult(comments);
        } 
    

    这里,我们创建一个评论。

    对于标签也是同样的情况;添加以下代码:

     public Task<Tag?> GetTagAsync(string id)
        {
            return Task.FromResult(new Tag() { Id = id, Name = $"Tag {id}" });
        }
        public async Task<List<Tag>?> GetTagsAsync()
        {
            List<Tag> list = new();
            for (int a = 0; a < 10; a++)
            {
                list.Add(await GetTagAsync($"{a}"));
            }
            return list;
        } 
    

    我们不会为 API 中的其他方法添加测试。但我们确实需要将它们添加到模拟类中,以满足接口:

     public Task<BlogPost?> SaveBlogPostAsync(BlogPost item)
        {
            return Task.FromResult(item);
        }
        public Task<Category?> SaveCategoryAsync(Category item)
        {
            return Task.FromResult(item);
        }
        public Task<Tag?> SaveTagAsync(Tag item)
        {
            return Task.FromResult(item);
        }     public Task<Comment?> SaveCommentAsync(Comment item)
        {
            return Task.FromResult(item);
        }
        public Task DeleteBlogPostAsync(string id)
        {
            return Task.CompletedTask;
        }
        public Task DeleteCategoryAsync(string id)
        {
            return Task.CompletedTask;
        }
        public Task DeleteTagAsync(string id)
        {
            return Task.CompletedTask;
        }
      public Task DeleteCommentAsync(string id)
      {
            return Task.CompletedTask;
      } 
    

现在我们有一个模拟 API,它反复执行相同的事情,这样我们就可以进行可靠的测试。

编写测试

是时候编写一些测试了。如我之前在本章中提到的,我们不会为整个网站创建测试;如果你愿意,我们可以留到稍后由你来完成。这只是为了让你了解如何编写测试:

  1. MyBlog.Tests项目中,创建一个名为Pages的新文件夹。这样做只是为了保持一定的结构(与我们要测试的项目相同的文件夹结构)。

  2. 选择Pages文件夹,创建一个名为HomeTest.razor的新 Razor 组件。

  3. _Imports文件中,添加以下命名空间:

    @using SharedComponents.Pages
    @using Data.Models.Interfaces
    @using SharedComponents.ReusableComponents 
    
  4. HomeTest.razor文件中,通过添加以下代码继承TestContext

    @inherits TestContext 
    
  5. 现在,我们将添加测试。添加以下代码:

    @code{
    [Fact(DisplayName ="Checks that the Home component shows 10 posts")]
        public void Shows10Blogposts()
        {
            // Act
    var cut = Render(@<Home />);
            // Assert that the content has 10 article tags (each representing a blogpost)
            Assert.Equal(10,cut.FindAll("article").Count());
        }
    } 
    

    我们给我们的测试起一个显示名称,这样我们就能理解它做什么。这个测试相当简单;我们知道从模拟 API 中有10篇博客文章。我们还知道每篇博客文章都在article标签内渲染。我们找到所有的article标签,并确保总共有10个。

    由于我们正在使用注入,我们需要配置依赖注入,这可以在构造函数中完成。

  6. 我们需要添加HomeTest方法:

    public HomeTest()
    {
          Services.AddScoped<IBlogApi, BlogApiMock>();
    } 
    

    当类创建时,此方法将运行,在这里我们声明如果组件请求BlogApi的实例,它将返回我们的模拟 API 的实例。

    这与 Blazor Server 中的方式相同,在那里我们返回一个直接与数据库通信的 API,以及与 Blazor WebAssembly 相同,在那里我们返回一个与 Web API 通信的 API 实例。

    在这种情况下,它将返回我们的模拟 API,该 API 返回易于测试的数据。现在,我们需要运行实际测试。

  7. 删除默认测试:

    Counter.razor 
    CounterCSharpTests.cs
    CounterRazorTests.cs 
    
  8. 在 Visual Studio 中,通过使用Ctrl + Q搜索来打开测试资源管理器。我们也可以在视图 | 测试资源管理器中找到它。

    运行我们的测试,看看是否得到绿色信号,如图图 13.2所示:

图 13.2:带有 IndexTest 的测试资源管理器

现在,我们有一个检查 10 篇帖子是否渲染的测试。

bUnit 是一个优秀的测试框架,而且它专门为 Blazor 编写,因此能够充分利用 Blazor 的强大功能,这使得它非常易于使用。

现在,我们对我们的博客有一个简单的测试,但 bUnit 支持更高级的功能,如身份验证。

身份验证

使用 bUnit,我们可以测试身份验证和授权。

然而,进行身份验证的不是组件本身。我们在第八章身份验证和授权中向App.razor添加了AuthorizeRouteView,所以在单个组件中测试这一点不会有任何区别。

但我们可以使用AuthorizeView,例如,我们已经在我们的博客中的LoginStatus组件中有了它,当未授权时显示登录链接,当授权时显示注销链接。请随意添加这些测试,就像我们在上一节中做的那样,或者作为参考。

我们可以使用AddTestAuthorization方法来授权我们的测试,如下所示:

 [Fact(DisplayName ="Checks if log in is showed")]
    public void ShouldShowLogin()
    {
        // Arrange
this.AddTestAuthorization();
        // Act
var cut = Render(@<LoginStatus />);

        // Assert that there is a link with the text Log in
        Assert.Equal("Log in",cut.Find("a").InnerHtml);
    } 

这种方法添加了TestAuthorization但并未授权。随后页面将显示一个带有文本登录的链接。为了测试用户是否已授权,我们只需将用户设置为已授权:

 [Fact(DisplayName ="Checks if logout is showed")]
    public void ShouldShowLogout()
    {
        // Arrange
var authContext = this.AddTestAuthorization();
        authContext.SetAuthorized("Testuser", AuthorizationState.Authorized);
        // Act
var cut = Render(@<LoginStatus />);

        // Assert that there is a link with the text Log out
        Assert.Equal("Log out",cut.Find("a").InnerHtml);
    } 

我们可以添加声明、角色以及更多内容。我们用于测试的用户与数据库中的用户或角色无关;授权由 bUnit 模拟。

认证和授权可能很难测试,但使用 bUnit 确实很简单。测试 JavaScript 可能有点困难,但 bUnit 也有相应的解决方案。

测试 JavaScript

bUnit 不支持测试 JavaScript,这是可以理解的。然而,我们可以自己测试互操作性。

在这本书中,我们使用了.NET 5 语法来编写 JavaScript。在我们的SharedComponents\ReusableComponents\BlogButton.razor组件中,我们进行 JavaScript 互操作以确认删除项。

JavaScript 调用看起来像这样:

jsmodule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "/_content/SharedComponents/ReusableComponents/BlogButton.razor.js");
return await jsmodule.InvokeAsync<bool>("showConfirm", ConfirmMessage); 

我们确保加载 JavaScript 模块,然后执行showConfirm方法。

在 bUnit 中,JavaScript 测试可以以两种模式进行——严格宽松。默认值是严格,因此我们需要指定每个模块和每个方法。

如果我们选择宽松,所有方法都将返回默认值。例如,对于布尔值,它将返回false

要测试前面的 JavaScript 调用,我们可以通过添加类似以下内容来实现:

var moduleInterop = this.JSInterop.SetupModule("/_content/SharedComponents/ReusableComponents/BlogButton.razor.js");
    var showconfirm = moduleInterop.Setup<bool>("showConfirm", "Are you sure?").SetResult(true); 

我们设置了一个与之前相同的路径到 JavaScript 的模块。然后,我们指定方法和任何参数。

最后,我们指定结果应该是什么。在这种情况下,我们返回true,这将返回 JavaScript,如果我们想删除项。我们还可以验证 JavaScript 方法是否被调用。在ItemList组件中测试此功能的完整示例如下:

@using Data.Models; @using SharedComponents.ReusableComponents;
@inherits TestContext
@code {
    [Fact(DisplayName = "Test if js method 'showConfirm' is called upon using JS interop")]
    public void ShouldShowConfirm()
    {
        // Arrange
var moduleInterop = this.JSInterop.SetupModule("/_content/SharedComponents/ReusableComponents/BlogButton.razor.js");
        moduleInterop.Setup<bool>("showConfirm", "Are you sure?").SetResult(true);
        var cut = Render(@<BlogButton OnClick="()=>{}" ConfirmMessage="Are you sure?"/>);
        // Act
var buttons = cut.FindAll("button");
        buttons.First().Click();
        // Assert
        JSInterop.VerifyInvoke("showConfirm");
}
} 

干得好!我们现在在我们的项目中有了测试。尽管我们没有涵盖所有组件,但我们应该有完成测试的所有构建块。

如果你想了解更多关于 bUnit 的信息,请查看以下链接:bunit.dev/docs/getting-started/index.html

他们的文档非常出色。

好知道也可以使用其他测试框架。我们结合使用 Playwright 测试和 bUnit,但测试的内容完全不同。你可以在playwright.dev/dotnet/docs/intro找到 Playwright。

在我们总结本章之前,我们还有一件事要讨论。

Blazm 扩展

在开发 Blazor 应用程序时,有些事情做起来有点繁琐。我们在整本书中做了很多这样的事情。我编写代码时容易拼错单词,在创建独立的 CSS 或 JavaScript 文件时,我有时会拼错文件名,甚至文件扩展名也会出错。所以,我想,有没有更好的方法来做这件事?

是的,有!

我开发了一个 Visual Studio 扩展,它将为 Visual Studio 添加一些非常棒的功能。

但我究竟为什么要等这么久才谈论这个呢!?好吧,首先学习“真正的”方法很重要,然后才走捷径。

你可以在这里查看扩展:marketplace.visualstudio.com/items?itemName=EngstromJimmy.BlazmExtension

它可以帮助我们添加代码后文件和隔离的 CSS 和 JavaScript 文件。它还可以帮助我们将命名空间移动到_imports文件,以及更多。但它还可以帮助我们生成测试,不是全部,但会在路上帮助我们。你还记得我们在第四章中使用的Alerts组件吗?我们可以右键单击该组件并选择Generate bUnit 测试,然后使用Razor 语法。它将生成代码到剪贴板,我们可以将其粘贴到我们想要的位置。它将自动给我们以下代码:

@inherits TestContext
@using Bunit
@using SharedComponents.ReusableComponents;
@code
    {
        [Fact]
        public void AlertTest()
        {
            //Arrange
            SharedComponents.ReusableComponents.Alert/AlertStyle style = default!;
            var cut = Render(@<Alert
                  Style="@style"
                  >
                  <ChildContent>
<b>ChildContent fragment</b></ChildContent>
                  </Alert>
);
            //Act
//Assert
        }
    } 

结果并不完美,正如我们所见,但它给了我们一个立足点。如果我们为Alert组件编写一个测试,它可能看起来像这样:

[Fact]
 public void AlertStyleTest()
 {
     //Arrange
     Alert.AlertStyle style = Alert.AlertStyle.Primary;
     var cut = Render(@<Alert Style="@style">
     <ChildContent>
         <b>ChildContent fragment</b>
     </ChildContent>
 </Alert>
 );
     //Act
//Assert
     cut.MarkupMatches("""<div class="alert alert-primary" role="alert"><b>ChildContent fragment</b></div>""");
 } 

我们不得不清理一些命名空间并添加一个断言。如果你问我,这很整洁,但话又说回来,我对这个话题确实有些偏见。我真的希望这个扩展能帮到你,如果你喜欢它,我非常希望你能给它一个五星好评。

摘要

在本章中,我们探讨了测试我们的应用程序。我们讨论了如何模拟 API 以进行可靠的测试。我们还涵盖了如何测试 JavaScript 互操作以及认证。

测试可以加快我们的开发速度,最重要的是,提高质量。结合 bUnit 和依赖注入,构建帮助我们测试组件的测试变得容易。

由于我们可以单独测试每个组件,所以我们不必登录,导航到我们网站上的特定位置,然后测试整个页面,正如许多其他测试框架所要求的那样。

现在,我们的网站包含可重用组件、认证、API、Blazor Server、Blazor WebAssembly、认证、共享代码、JavaScript 互操作、状态管理和测试。我们只剩下一件事要做:发布它!

在下一章,第十四章部署到生产环境,是时候发布了。

第十四章:部署到生产环境

在本章中,我们将探讨我们在将 Blazor 应用程序部署到生产环境时拥有的不同选项。由于选项很多,详细说明它们将是一本单独的书。

我们不会深入探讨,而是会涵盖我们需要考虑的不同事项,以便我们可以部署到任何提供商。

最后,部署是我们为了利用我们所构建的内容而需要做的事情。

在本章中,我们将涵盖以下内容:

  • 持续交付选项

  • 主机选项

技术要求

本章是关于一般部署的,因此我们不需要任何代码。

持续交付选项

当将任何内容部署到生产环境时,我们应该考虑确保移除不确定因素。例如,如果我们是从自己的机器上部署,我们如何知道它是最新版本?我们如何知道我们的队友最近没有解决问题,而我们分支中没有修复?坦白说,我们甚至不知道源控制中的版本是否与生产中的版本相同,或者生产中的版本是否存在于源控制中?你知道那句老话:“朋友不会让朋友右键点击并发布”(到生产环境中,那就是)?

这就是持续集成持续交付/部署CI/CD)出现的地方。我们确保有其他东西将部署到生产环境中。关于部署可以写整本书,所以我们不会深入这个主题。

GitHub Actions 和 Azure DevOps(或 Azure Pipelines)是微软的两个 CI/CD 产品。还有很多其他产品,如 Jenkins、TeamCity 和 GitLab——列表很长。如果我们目前使用的 CI/CD 系统支持部署 ASP.NET,它将能够处理 Blazor,因为最终,Blazor 只是一个 ASP.NET 网站。

如果我们有测试(我们应该有),我们还应该确保将测试设置为 CI/CD 管道的一部分。好事是,我们不需要添加任何特定的硬件来测试我们的组件;如果我们的 CI/CD 管道可以运行单元测试,它就会工作。

由于 Blazor 是 ASP.NET,我们没有任何阻止我们进一步进行网站自动化测试的理由。

还有一种叫做wasm-tools的东西,我们将在第十六章“深入 WebAssembly”中探讨。

主机选项

当涉及到托管 Blazor 时,有许多选项。任何可以托管 ASP.NET Core 站点的云服务都应该能够无任何问题地运行 Blazor。

我们需要考虑一些事情,让我们逐一探讨选项。

托管 Blazor Server/InteractiveServer

如果云提供商可以启用/禁用 WebSockets,我们希望启用它们,因为 SignalR 使用的就是该协议。

有时,云服务提供商可能支持.NET Core 3.x,但不支持.NET 8。但不用担心;通过确保以自包含模式发布我们的应用程序,我们确保部署还添加了运行项目所需的任何文件(这可能不是所有托管提供商都适用)。

这也是确保我们运行在期望的确切框架版本上的一个好方法。

运行交互式 WebAssembly

交互式 WebAssembly 使用.NET Core 后端(就像我们为博客所做的那样),我们托管了一个.NET Core 网站,因此托管 Blazor 服务器的规则同样适用。对于我们的博客,我们还添加了 SignalR,因此我们还需要启用 WebSocket。

托管 Blazor WebAssembly 独立版本

如果我们使用 Blazor WebAssembly 独立模板,我们不需要考虑.NET Core 托管。我们可以在 Azure Static Web Apps 或 GitHub Pages 上托管我们的应用程序。这是做 Blazor WebAssembly 独立站点的优点之一。

在 IIS 上托管

我们还可以在互联网信息服务器IIS)上托管我们的应用程序。安装托管包,如果机器上安装了 IIS,它还会确保包含 ASP.NET Core IIS 模块。

您需要确保在服务器上启用 WebSocket 协议。

我们目前在我们的网站上运行 IIS,并使用 Azure DevOps 来部署我们的网站。由于我们使用 Blazor 服务器,停机时间非常明显。一旦 Web 丢失了 SignalR 连接,网站将显示重新连接的消息。

对于我们使用的网站,在部署新版本时大约有 8 到 10 秒的停机时间,这相当快。

摘要

在本章中,我们讨论了为什么我们应该使用 CI/CD,因为它在确保应用程序质量方面有很大差异。我们查看了一些我们需要做的事情,以便在任何支持.NET 8 的云服务提供商上运行我们的 Blazor 应用程序。

部署可能是应用程序中最重要的一步。如果没有部署我们的应用程序,它就只是代码。在本章中提到的诸如 CI/CD、托管和部署等事项,我们现在已经准备好部署代码。

在下一章中,我们将更深入地探讨如何将现有网站移植,使用 Blazor 与其他技术结合,或者使用其他技术与 Blazor 结合。

第十五章:从现有网站迁移或与之结合

在本章中,我们将探讨如何结合不同的技术和框架使用 Blazor。

如果我们已经有了一个网站呢?

当涉及到从现有网站迁移时,有多种选择;第一个问题是,我们是否想要从现有网站迁移,还是想要将其与新技术结合?

微软有让技术共存的历史,这正是本章的主题。

我们如何在 Blazor 网站中使用 Angular 和 React,或者如何将 Blazor 引入现有的 Angular 和 React 网站?

在本章中,我们将涵盖以下内容:

  • 介绍 Web 组件

  • 探索自定义元素

  • 探索 Blazor 组件

  • 将 Blazor 添加到 Angular 网站

  • 将 Blazor 添加到 React 网站

  • 将 Blazor 添加到 MVC/Razor Pages

  • 将 Web 组件添加到 Blazor 网站

  • 从 Web 表单迁移

结合技术可以非常有用,要么是因为我们一次无法转换整个网站,要么是因为其他技术更适合我们想要实现的目标。

话虽如此,我更喜欢在我的网站上使用一种技术,而不是将 Blazor 与 Angular 或 React 混合。但在迁移期间或如果我们的团队是混合的,混合也有其好处。

混合技术是有成本的,我们将在本章中探讨这一点。

在撰写本章、回顾 Angular 和 React 的过程中,我必须抓住机会说,我有多么喜欢 Razor 语法。React 是带有 HTML 标签的 JavaScript,Angular 有模板,我觉得很棒,让人联想到 Razor 语法的样子。

然而,涉及到的内容很多:几乎 300 MB 的 Node.js 模块、npm、TypeScript 和 webpack。好吧,列表很长。

我喜欢使用 Blazor,因为我不需要处理我刚才提到的所有内容。在我看来,Blazor 在这三个选项中拥有最好的语法。

技术要求

本章是一个参考章节,并且与本书的其他章节没有任何关联。

您可以在github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter15找到本章示例的源代码。

介绍 Web 组件

要与 JavaScript 一起工作,无论是将 JavaScript 带入 Blazor 还是将 Blazor 带入 JavaScript,我们可以使用一种称为 Web 组件的技术。

Web 组件是一组 Web 平台 API,允许我们创建新的、自定义的、可重用的 HTML 标签。它们以封装的方式打包,我们可以像在 Blazor 中使用组件一样使用它们。

真正的好处是,我们可以在支持 HTML 的任何 JavaScript 库或框架中使用它们。

Web 组件建立在现有的 Web 标准(如 shadow DOM、ES 模块、HTML 模板和自定义元素)之上。

我们也将在 Blazor 中识别一些这些技术或它们的变体。Shadow DOM 与 Blazor 的渲染树相同,ES 模块是我们第十章中讨论的JavaScript 互操作中查看的 JavaScript 模块类型。

我们将在本章中探讨的技术是自定义元素

探索自定义元素

要将 Blazor 引入现有的 Angular 或 React 网站,我们使用一个名为CustomElements的功能。它是在.NET 6 中作为一个实验性功能引入的,并从.NET 7 开始成为框架的一部分。

理想的情况是在 Blazor 中创建网站的部分,而不必完全迁移到 Blazor。

为了使这个功能正常工作,我们需要有一个 ASP.NET 后端或手动确保_framework文件可用。这样我们就可以提供 Blazor 框架文件。

运行CustomElements有两种方式;我们可以将其作为 Blazor WebAssembly 或作为 Blazor Server 运行。由于我们正在将 Blazor 添加到像 React 或 Angular 这样的客户端框架中,最相关的方法是将它作为 Blazor WebAssembly 运行。因此,这些第一部分中的示例将是针对 Blazor WebAssembly 的。

在 GitHub 仓库中,有一个名为CustomElements的文件夹,其中包含项目的代码,我们将从本章中看到示例代码。

值得注意的是,由于组件是在客户端提供和使用的,因此没有任何东西阻止我们(或对我们造成伤害的人)反编译代码(如果我们使用 WebAssembly)。这是所有框架的客户端开发者一直在处理的事情,但再次提一下也是值得的。

探索 Blazor 组件

我们需要尝试的第一件事是一个 Blazor 组件。我在一个名为BlazorCustomElements的 Blazor WebAssembly 项目中创建了一个counter组件。

默认模板包含了很多东西,而 repo 项目被简化到最基本,因此很容易理解。

组件与我们之前在书中看到的不同,它是一个带有设置计数器应增加多少的参数的counter组件。它看起来像这样:

<h1>Blazor counter</h1>
<p role="status">Current count: @currentCount</p>
<p>Increment amount: @IncrementAmount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    [Parameter] public int IncrementAmount { get; set; } = 1;
    private void IncrementCount()
    {
        currentCount += IncrementAmount;
    }
} 

该项目还需要对 NuGet 包有一个引用:

Microsoft.AspNetCore.Components.CustomElements 

Program.cs中,我们需要像这样注册组件/自定义元素:

builder.RootComponents.RegisterCustomElement<Counter>("my-blazor-counter"); 

Blazor 项目就到这里了。

现在是时候使用我们的自定义元素了。

将 Blazor 添加到 Angular 网站

让我们看看如何将 Blazor 添加到现有的 Angular 网站中。这个演示基于 Visual Studio 中的 Angular 和 ASP.NET Core 模板。

该文件夹被命名为Angular

首先,我们需要对我们的 Blazor 库有一个引用。我将BlazorCustomElement项目作为引用添加到服务器项目中。

我们需要一个对Microsoft.AspNetCore.Components.WebAssembly.Server NuGet包的引用;这样我们就可以提供框架文件。

为了使我们的网站提供框架文件,我们需要将以下内容添加到Program.cs中:

app.UseBlazorFrameworkFiles(); 

默认情况下,当我们添加自定义元素时,Angular 会感到不满,因为它不识别该标签。为了解决这个问题,我们需要告诉 Angular 我们正在使用自定义元素。在angularproject.client/src/app/app.module.ts中添加以下内容:

import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 

确保替换掉已经包含对NgModule导入的行。

在同一文件的下方一点,添加以下内容:

 schemas: [
    CUSTOM_ELEMENTS_SCHEMA // Tells Angular we will have custom tags in our templates
  ] 

现在,Angular 对使用自定义元素表示满意。

接下来,是时候添加我们的组件了。在angularproject.client/src/app/app.component.html中,我们添加我们的自定义标签:

<my-blazor-counter increment-amount="10"></my-blazor-counter> 

在这种情况下,我们将increment-amount参数设置为10,每次点击时计数器将增加10

要使这一切都能正常工作,我们需要加载一些 JavaScript 脚本。在angularproject.client/src/index.html中,我们需要添加以下内容:

<script src="img/Microsoft.AspNetCore.Components.CustomElements.lib.module.js"></script>
<script src="img/blazor.webassembly.js"></script> 

我们还有最后一件事需要修复。当运行 Angular 项目时,它会启动一个开发者服务器。实际上,它会启动两个:一个用于 ASP.NET 后端,一个用于 Angular 前端。我们需要让 Angular 服务器将所有框架请求发送到 ASP.NET 后端。

在默认项目模板中,这已经为/weatherforecast路径完成了。将以下代码添加到angularproject.client/proxy.conf.js文件中:

 context: [
  "/weatherforecast",
  "/_framework",
  "/_content",
], 

我们告诉开发者服务器,如果请求是发送到weatherforecast_framework_content,我们希望将该请求重定向到 ASP.NET 后端。

现在我们有一个工作的 Angular/Blazor WebAssembly 混合应用。我第一次尝试时,真的对它有多么简单和直接感到惊讶。这使得在 Angular 网站上包含一些 Blazor 组件变得非常容易,因此您可以逐步将其转换为 Blazor,组件一个接一个。

接下来,我们将使用 React 网站执行相同的操作。

将 Blazor 添加到 React 网站

将 Blazor 添加到 React 网站与添加到 Angular 非常相似。这个演示基于 Visual Studio 中的 React 和 ASP.NET Core 模板。项目名为ReactProject

首先,我们需要对 Blazor 库有一个引用,我添加了BlazorCustomElement项目作为引用。

我们需要一个对Microsoft.AspNetCore.Components.WebAssembly.Server NuGet包的引用;这样我们就可以提供框架文件。

要使我们的网站能够提供框架文件,我们需要在Program.cs中添加以下内容:

app.UseBlazorFrameworkFiles(); 

接下来,是时候添加我们的组件了。在reactproject.client/src/App.tsx中,我们添加我们的自定义标签:

<my-blazor-counter increment-amount="10"></my-blazor-counter> 

在这种情况下,我们将increment-amount参数设置为10,每次点击时计数器将增加10

要使这一切都能正常工作,我们需要加载一些 JavaScript。在reactproject.client/index.html中,我们需要添加以下内容:

 <script src="img/Microsoft.AspNetCore.Components.CustomElements.lib.module.js"></script>
<script src="img/blazor.webassembly.js"></script> 

这些脚本将确保我们的组件加载。

我们还有最后一件事需要修复。当运行 React 项目时,它会启动一个开发者服务器。实际上,它会启动两个:一个用于 ASP.NET 后端,一个用于 React 前端。

我们需要让 React 服务器将所有框架请求发送到 ASP.NET 后端。在默认项目模板中,这已经为 /weatherforecast 路径完成了。

将以下代码添加到 reactproject.client/vite.config.ts 文件中:

'^/_framework': {
    target,
    secure: false
},
'^/_content': {
    target,
    secure: false
}, 

我们告诉开发者服务器,如果请求发送到 weatherforecast_framework_content,我们希望将那个请求重定向到 ASP.NET 后端。

现在我们有一个工作的 React/Blazor WebAssembly 混合。这非常类似于 Angular,我对它如何简单直接感到惊讶。这使得在 React 网站上包含一些 Blazor 组件变得非常容易,这样你可以逐步将它们转换为 Blazor。

接下来,我们将使用一个 Razor Pages 网站做同样的事情。

将 Blazor 添加到 MVC/Razor Pages

当我开始使用 Blazor 时,这正是我们想要解决的问题。我们有一个 MVC/Razor Pages 混合,是时候升级了。

我们通过实现引用了 Razor 组件的 Razor Pages 解决了这个问题。现在回想起来,这并不是一个很漂亮的解决方案,至少一开始不是,直到我们到达大多数代码都在 Blazor 中重写的点。

挑战在于,如果我们导航到一个包含 Blazor 组件(一个 Razor 组件)的页面,该页面会连接到服务器并建立 WebSocket。如果我们从 Blazor 页面导航到一个 MVC 页面,例如,我们会重新加载整个页面,脚本也会重新加载。新的连接被建立,而旧的连接在服务器上保持 3 分钟。

我们的用户不多,对我们来说,这种技术足以让我们完成迁移并推出网站的新的 Blazor 版本。

但我有好消息!

我们还可以使用相同的自定义元素在 Razor Pages 网站上运行。

让我们看看吧!

该项目被称为 RazorPagesProject

在之前的 Angular 和 React 示例中,那些技术是客户端的;因此,我们使用了 WebAssembly。Razor Pages 是服务器端的,尽管我们也可以在这里使用 WebAssembly,但这是一个很好的机会来看看如何使 自定义组件 使用 Blazor 服务器。

首先,我们需要引用我们的 Blazor 库。我添加了 BlazorCustomElement 项目作为引用。

然后,我们需要通过在 Program.cs 中添加以下代码来在我们的 Razor Pages 中启用 Blazor 服务器。

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(options =>
    {
        options.RootComponents.RegisterCustomElement<Counter>("my-counter");
    }); 

还有:

app.MapBlazorHub(); 

Pages/Shared/_Layout.cshtml 中,我们需要添加以下 JavaScript:

 <script src="img/Microsoft.AspNetCore.Components.CustomElements.lib.module.js"></script>
<script src="img/blazor.server.js"></script> 

在这种情况下,我们添加 Blazor 服务器端的脚本。

最后但同样重要的是,我们需要添加我们的组件。在 Pages/Index.cshtml 中,我们添加:

<my-counter increment-amount="10"></my-counter> 

我们完成了;自定义组件现在正在我们的 Razor Pages 网站内运行(当然,这是一个启用了 Razor Pages 的 ASP.NET 网站)。

最酷的部分是,只需进行一些更改,我们就可以将此实现切换为使用 WebAssembly 而不是 Blazor 服务器运行 Blazor 组件。

再次,我对这个项目印象深刻;它使得将现有网站迁移到 Blazor 变得非常简单。

接下来,我们将看看如何在我们的 Blazor 网站上使用 Angular 或 React 控件。

将 Web 组件添加到 Blazor 网站

我们已经探讨了如何将 Blazor 添加到现有的 Angular、React 以及 MVC/Razor Pages 网站。

但有时,你可能非常喜欢使用的那个完美的库可能没有 Blazor 对应版本。我们知道我们可以创建一个 JavaScript 互操作并自己构建它,但我们是否也可以从 Blazor 使用 Angular 和 React 库?

这里我们有两个选择;要么我们可以将我们的网站转换为 Angular/React 网站,并使用那些示例,要么我们可以将 JavaScript 库转换为 Web 组件,并在 Blazor 中使用它。

到目前为止,我们还没有使用 npm 或类似的东西,因为在大多数情况下,我们不需要它。但现在我们在混合技术,为此,npm 是最简单的方式。npm 不在本书的范围之内,所以我就不会深入介绍它了。

如何将 Angular/React 或其他任何东西转换为 Web 组件也不在本书的范围之内。

该项目被称为 BlazorProject

我们可以浏览这个网站上的一些 Web 组件:www.webcomponents.org/

我在 GitHub 上找到了一个 Markdown 编辑器。即使我们不会在我们的博客上实现它,如果你愿意,也可以随时回去实现。

我们可以在这里了解编辑器:

www.webcomponents.org/element/@github/markdown-toolbar-element

要获取所需的 JavaScript 文件,我们需要设置 npm。在项目文件夹(BlazorProject.Client)中,运行以下命令:

npm init
npm install –save @github/markdown-toolbar-element 

这将下载我们需要的 JavaScript。

接下来,将 BlazorProject\node_modules\@github\markdown-toolbar-element\ 文件夹复制到 wwwroot 文件夹(在服务器项目中),并将其包含在项目中。

现在,JavaScript 将可以从我们的项目中访问。

app.razor 中,我们需要添加对 JavaScript 的引用,并将其放在 Blazor JavaScript 下方:

<script type="module" src="img/index.js"></script> 

这个组件是一个 ES6 模块,所以我们将其类型设置为 "module"

现在,剩下的事情就是添加我们的组件。在演示项目中,我将它添加到了 MarkdownDemo 组件中。

首先,是组件:

<markdown-toolbar for="textarea" role="toolbar">
<md-bold class="btn btn-sm" tabindex="0">bold</md-bold>
<md-header class="btn btn-sm" tabindex="-1">header</md-header>
<md-italic class="btn btn-sm" tabindex="-1">italic</md-italic>
<md-quote class="btn btn-sm" tabindex="-1">quote</md-quote>
<md-code class="btn btn-sm" tabindex="-1">code</md-code>
<md-link class="btn btn-sm" tabindex="-1">link</md-link>
<md-image class="btn btn-sm" tabindex="-1">image</md-image>
<md-unordered-list class="btn btn-sm" tabindex="-1">unordered-list</md-unordered-list>
<md-ordered-list class="btn btn-sm" tabindex="-1">ordered-list</md-ordered-list>
<md-task-list class="btn btn-sm" tabindex="-1">task-list</md-task-list>
<md-mention class="btn btn-sm" tabindex="-1">mention</md-mention>
<md-ref class="btn btn-sm" tabindex="-1">ref</md-ref>
<md-strikethrough class="btn btn-sm" tabindex="-1">strikethrough</md-strikethrough>
</markdown-toolbar> 

然后是绑定到 C# 变量 markdown 的文本区域:

<textarea @bind="markdown" @bind:event="oninput" rows="6" class="mt-3 d-block width-full" id="textarea" contenteditable="false" spellcheck="false"></textarea>
@markdown
@code
{
    private string markdown = "Hello, **world**!";
} 

当我们编辑文本框时,C# 变量会立即改变,无论是通过使用工具栏还是输入一些文本。

我们已经将一个 Web 组件集成到我们的 Blazor 项目中,它绑定到一个 C# 变量。

这非常强大,为我们提供了将现有功能添加到我们的 Blazor 网站的新可能性。

现在我们知道了如何处理像 React 和 Angular 这样的 SPA 框架。但关于像 Web 表单这样的服务器框架怎么办?这就是我们接下来要看的。

从 Web 表单迁移

最后但同样重要的是,我们有 Web 表单

实际上,对于 Web 表单并没有一个好的升级路径;曾经有一个项目旨在将代码重用到 Blazor 迁移中,但现在它并没有被积极开发。

我们首先应该知道的是,Blazor 在许多方面与 Web 表单非常相似,因此学习 Blazor 的曲线几乎不存在,因为我们既有 Web 表单也有 Blazor 中的状态管理。

有一些迁移策略,你可能会使用另一个反向代理YARP)。然而,我的建议是将网站的一部分迁移到 Blazor,并让两个网站同时运行,直到我们达到功能完整的点。迁移到 Blazor 相对较快,最终我相信这将为您节省时间。

当我们将我们的网站从 MVC 迁移到 Blazor 时,我们意识到在某些情况下,将组件重写为 Blazor 比在 MVC 中解决问题要快。

由于后端代码与 Blazor 比 MVC 更相似,Web 表单的转换应该更快。

那么,我们应该怎么做呢?是升级还是继续使用 Web 表单?升级——你不会失望的!

摘要

在本章中,我们讨论了将 Blazor 添加到其他技术中,如 Angular、React 和 Razor Pages,使用 Web 组件。我们探讨了如何将 Web 组件添加到 Blazor 项目中,并在我们的 Blazor 应用程序中利用 JavaScript 库。

将现有网站升级到 Blazor 可能是一项大量工作。在我以前雇主那里,我们 4 年前就经历了这个过程。在我们的案例中,我们希望更新我们的 MVC 网站以使其更具交互性。我们选择了 Blazor,我认为这挽救了我们的项目并提高了我们的生产力,从而带来了更丰富的用户体验。

在下一章中,我们将更深入地探讨 Blazor WebAssembly。

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/WebDevBlazor3e

第十六章:深入了解 WebAssembly

在本章中,我们将更深入地探讨仅与 Blazor WebAssembly 相关的技术。

Blazor 中的大多数内容都可以应用于 Blazor Server 和 Blazor WebAssembly。然而,由于 Blazor WebAssembly 在浏览器中运行,我们可以做一些事情来优化代码并使用其他我们无法在服务器端使用的库。

我们还将探讨一些常见问题及其解决方法。

在本章中,我们将涵盖以下内容:

  • 探索 WebAssembly 模板

  • .NET WebAssembly 构建工具

  • AOT 编译

  • WebAssembly 单指令多数据SIMD

  • 压缩

  • 懒加载

  • 渐进式网络应用

  • 原生依赖

  • 常见问题

本章的一些部分是很好的跟随学习机会,而其他部分则是供参考的,以便你在需要时能够找到正确的信息。

技术要求

本章是一个参考章节,并且与本书的其他章节没有关联。您可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter16 找到本章结果的源代码。

探索 WebAssembly 模板

WebAssembly 模板看起来与我们在 第二章 中看到的模板略有不同,即创建您的第一个 Blazor 应用。在 Blazor Web App 模板中,我们的入口点是 app.razor 文件。它包含我们开始所需的 HTML 标签。WebAssembly 模板有一个 Index.html 文件。让我们创建一个项目,以便我们可以查看:

  1. 创建一个新的项目并使用 Blazor WebAssembly Standalone App 模板。

  2. 将项目命名为 BlazorWebAssembly

  3. 保持默认设置并按 创建

首先,在 wwwroot 文件夹中,我们有一个包含所有 CSS、JavaScript 等内容的 Index.html 文件。这与 Blazor Web App 模板中的 App.razor 文件内容相同。WebAssembly 项目中也有一个 app.razor 文件,但它包含的内容与 Routes.razor 文件相同。因此,如果我们同时使用这两个模板,可能会有些令人困惑。

让我们查看每个文件,但只关注特定于 WebAssembly 的内容。在 Index.html 中,我们有以下有趣的代码:

<div id="app">
    <svg class="loading-progress">
        <circle r="40%" cx="50%" cy="50%" />
        <circle r="40%" cx="50%" cy="50%" />
    </svg>
    <div class="loading-progress-text"></div>
</div> 

这是一个 div,内容是一个显示 WebAssembly 加载进度的进度条。在 css/app.css 文件中,我们有以下内容:

.loading-progress circle:last-child {
    stroke: #1b6ec2;
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
    transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text:after {
    content: var(--blazor-load-percentage-text, "Loading");
} 

这些只是加载进度的一些 CSS 类,但有趣的是,Blazor 将给我们两个 CSS 值,--blazor-load-percentage-text--blazor-load-percentage。这为我们提供了关于加载我们的 WebAssembly 应用剩余时间的指示。这是一种自定义我们的进度指示器的好方法。一旦加载完成,div 的内容将被 WebAssembly 应用替换。

如果我们查看 Program.cs 文件,当运行独立的 WebAssembly 时,它包含的内容会更多一些:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync(); 

在这里,我们设置我们的 WebAssembly 项目,并告诉 .NET 我们想在 HTML 标签中使用 id app 渲染 app.razor 组件。我们还告诉 .NET 将 HeadOutlet 作为 head 标签的最后一个子元素渲染。

运行项目并探索一下。项目中的组件与我们已经在 第四章 中查看过的相同,即 理解基本 Blazor 组件,所以那里没有新的内容。

当我们第一次启动项目时,加载我们的应用程序需要几秒钟。这是所有内容下载并启动的时候。下次我们的用户访问我们的网站时,大部分文件将被缓存,我们不需要再次下载它们。

.NET WebAssembly 构建工具

当涉及到更“高级”的场景时,我们需要安装额外的工具。有两种安装工具的方法。

在安装 Visual Studio(或使用 Visual Studio 安装程序添加)时,我们可以选择.NET WebAssembly 构建工具选项,或者在命令提示符(以管理员身份)中运行以下命令:

dotnet workload install wasm-tools 

.NET WebAssembly 构建工具基于 Emscripten,这是一个用于 Web 平台的编译工具链。

AOT 编译

默认情况下,在 Blazor WebAssembly 应用程序中,作为 WebAssembly 运行的只有运行时。其他所有内容都是普通的 .NET 程序集,通过浏览器使用 WebAssembly 实现的 .NET 中间语言IL)解释器运行。

当我开始尝试 Blazor 时,我对这一点并不太喜欢;使用 IL 而不是浏览器能够原生理解的代码运行一切感觉是浪费的。然后,我认为浏览器正在运行与我服务器上相同的代码。浏览器中的相同代码!这真是太令人惊讶了!

然而,我们有直接编译到 WebAssembly 的选项;这被称为即时编译AOT)。它有一个缺点:应用程序的下载大小会增加,但它的运行和加载速度会更快。

AOT 编译的应用程序通常比 IL 编译的应用程序大两倍。AOT 会将 .NET 代码直接编译成 WebAssembly。

AOT 不会修剪托管程序集,当使用原生 WebAssembly 时,需要更多的代码来表示高级 .NET IL 指令。这就是为什么其大小要大得多,并且它也难以通过 HTTP 压缩。

AOT 并非适用于所有人;大多数不使用 AOT 运行的应用程序都能正常工作。然而,对于计算密集型应用程序,使用 AOT 可以获得很多好处。

我的 ZX Spectrum 模拟器就是这样一款应用程序;它每秒运行多次迭代,对于这些应用程序使用 AOT 的性能提升是显著的。

要使用 AOT 编译我们的 Blazor WebAssembly 项目,我们在 csproj 文件中添加以下属性:

<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup> 

AOT 编译仅在应用程序发布时执行。编译可能需要很长时间(ZX Spectrum 模拟器需要七分钟),所以每次编译应用程序时不必等待这一点是非常好的。

然而,在发布模式下运行可能会出现问题,所以如果您想在发布模式下快速测试,请暂时禁用前面的设置。

不要忘记再次启用它;我在这个领域有一些经验。

WebAssembly 单指令多数据(SIMD)

.NET7 中的一个新特性是 SIMD,这是一种最近添加到 WebAssembly 的并行处理类型。

SIMD 是一种计算机架构,允许 CPU 同时对多个数据点执行相同的操作,从而提高某些类型任务的性能。SIMD 指令通常用于执行向量运算,其中单个指令同时应用于向量的多个元素。SIMD 对于图像和视频处理等需要快速处理大量数据的任务可能有益。

SIMD 默认启用。要禁用 SIMD,我们需要在项目文件中将其禁用,如下所示:

<PropertyGroup>
  <WasmEnableSIMD>false</WasmEnableSIMD>
</PropertyGroup> 

为了使 SIMD 工作,我们需要使用 AOT 编译。

这超出了本书的范围,但我想要提到它,以防这符合您的项目需求。

剪枝

默认情况下,发布 Blazor WebAssembly 应用程序时,会执行剪枝。这将删除不必要的文件,并通过这样做,减小应用程序的大小。

如果我们的应用程序使用反射,剪枝器可能难以确定可以删除和不能删除的内容。

对于大多数应用程序,剪枝是自动的,并且会正常工作。要了解更多关于剪枝选项的信息,您可以查看这里:learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options?pivots=dotnet-8-0

延迟加载

当使用 Blazor WebAssembly 时,一个挑战是下载大小。尽管这并不是一个大问题,在我看来,我们可以做一些事情来处理下载和加载时间。我们将在本章后面的常见问题部分回到这个问题。

当导航到 Blazor WebAssembly 应用程序时,会下载我们应用程序和来自.NET Framework 的所有 DLL。启动一切需要一些时间。我们可以通过使用延迟加载来按需加载一些 DLL,以解决这个问题。

假设我们的应用程序很大,其中有一个报告部分。报告可能不是每天都会使用,也不是每个人都会使用,因此从初始下载中删除该部分并在需要时加载它是有意义的。

要实现这一点,我们想要延迟加载的部分必须在一个单独的项目/DLL 中。在 Blazor WebAssembly 客户端项目的csproj文件中,通过添加以下代码来添加对 DLL 的引用:

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.dll" />
</ItemGroup> 
LazyAssemblyLoader.

LazyAssemblyLoader服务将进行 JS 互操作调用以下载程序集并将其加载到运行时。

我们确保在路由器(App.razor)中下载必要的程序集/DLL,这样我们就可以确保在导航到使用它们的组件之前下载它们:

@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger
<Router AppAssembly="@typeof(App).Assembly" 
    OnNavigateAsync="@OnNavigateAsync">
    ...
</Router>
@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
} 

我们需要注入 LazyAssemblyLoader;在 Blazor WebAssembly 项目中,它默认注册为单例。

您需要设置一个 OnNavigateAsync 事件,并在该方法中检查路径,确保加载所需的程序集。

此事件也可以通过执行类似以下操作来用于可路由组件:

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger
<Router AppAssembly="@typeof(App).Assembly" 
    AdditionalAssemblies="@lazyLoadedAssemblies" 
    OnNavigateAsync="@OnNavigateAsync">
    ...
</Router>
@code {
    private List<Assembly> lazyLoadedAssemblies = new();
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
                   lazyLoadedAssemblies.AddRange(assemblies);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
} 
"/fetchdata". The {LIST OF ASSEMBLIES}, which contains a list of assemblies we wish to load, can be "sample.dll".

这使得对于没有访问权限的用户不加载管理界面成为可能,例如。当然,当有需要时,我们可以触发下载额外的程序集(而不是等待用户点击应用程序的特定部分然后下载程序集)。

渐进式 Web 应用

Blazor 服务器和 Blazor WebAssembly 都可以创建 渐进式 Web 应用PWA),但 Blazor WebAssembly 更常见。PWA 使得我们可以下载我们的 Web 应用并将其作为手机或计算机上的应用程序运行。它们将使我们能够添加漂亮的图标,并在没有 URL 输入字段的情况下在网页浏览器中启动我们的网站,这样它就会更像一个应用程序。

在创建项目时,我们选择 渐进式 Web 应用。通过这样做,我们将获得一些配置和 JavaScript 来设置一切。

PWA 超出了本书的范围,但有一些很好的资源可以帮助我们入门。您可以在以下链接中找到更多信息:learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app?view=aspnetcore-8.0&tabs=visual-studio

原生依赖项

由于我们在运行 WebAssembly,我们可以在项目中使用用其他语言编写的 WebAssembly 程序集。这意味着我们可以在项目中直接使用任何原生依赖项。

一种方法是将 C 文件直接添加到我们的项目中。在存储库中的 Chapter16 文件夹中,你可以找到一个示例。

我添加了一个名为 Test.c 的文件,内容如下:

int fact(int n)
{
    if (n == 0) return 1;
    return n * fact(n - 1);
} 

在项目文件中,我添加了对该文件的引用:

<ItemGroup>
    <NativeFileReference Include="Test.c" />
</ItemGroup> 

Home.razor 文件中,我添加了以下代码:

@page "/"
@using System.Runtime.InteropServices
<PageTitle>Native C</PageTitle>
<h1>Native C Test</h1>
<p>
    @@fact(3) result: @fact(3)
</p>
@code {
    [DllImport("Test")]
    static extern int fact(int n);
} 

在我们的 C# 项目中,我们现在有一个可以从 Blazor 项目中调用的 C 文件。它被编译成 WebAssembly,然后我们可以引用那个 WebAssembly 文件(这会自动发生)。我们可以通过使用一个使用 C++ 库的库来更进一步。Skia 是一个用 C++ 编写的开源图形引擎。

更多信息请参阅:github.com/mono/SkiaSharp。我们可以通过添加 NuGet 包 SkiaSharp.Views.Blazor 将该库添加到 Blazor WebAssembly 应用程序中。

在存储库中的 Chapter16 文件夹中,您可以探索一个名为 SkiaSharpDemo 的项目。

Home.razor 文件中,我添加了以下代码:

<SKCanvasView OnPaintSurface="@OnPaintSurface" />
@code {
    private void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
        var canvas = e.Surface.Canvas;
        canvas.Clear(SKColors.White);
        using var paint = new SKPaint
        {
            Color = SKColors.Black,
            IsAntialias = true,
            TextSize = 24
        };
        canvas.DrawText("Raccoons are awesome!", 0, 24, paint);
    }
} 

页面将在画布上绘制 "Raccoons are awesome"

在这种情况下,我们正在使用一个使用 C++ 库的 C# 库。

我们甚至可以直接通过添加 对象文件.o)、归档文件.a)、位码.bc)和独立 WebAssembly 模块.wasm)来引用已经使用 Emscripten 构建的库。如果我们找到一个用另一种语言编写的库,我们可以将其编译成 WebAssembly,然后从我们的 Blazor 应用程序中使用它。这打开了无数的大门!

接下来,我们将探讨一些我遇到的一些常见问题。

常见问题

让我们从一开始就深入探讨这个问题。

关于 Blazor WebAssembly 最常见的评论是下载大小和加载时间。一个小项目的大小大约是 1 MB,但我相信问题是加载时间,而不是下载大小/时间,因为所有内容都已缓存,而且在世界上的大多数地方,我们都能访问高速互联网。

有几种解决这个问题的方法。

进度指示器

当谈到用户体验UX)时,我们可以给用户一种感知速度的感觉。

默认的 Blazor WebAssembly 模板有一个加载进度指示器,它给用户一些可以看的东西,而不是一个空白页面。它被构建得很容易使用 CSS 变量进行自定义。我们可以使用 --blazor-load-percentage--blazor-load-percentage-text 变量来自定义并创建我们的进度条。

它甚至不需要指示正在发生什么;Dragons Mania Legends 有像“缝纫迷你维京人”这样的评论,这显然不是正在发生的事情。所以根据我们正在构建的应用程序,显示某些内容比显示无内容更重要。

服务器端预渲染

在 Blazor 的早期版本中,我们必须自己做一些魔法才能使预渲染工作。但有了新的 Blazor Web App 模板,我们就可以直接获得这个功能。到目前为止,我们一直在使用 Blazor WebAssembly Standalone 模板讨论 Blazor WebAssembly 的功能;在本节中,我们使用的是 Blazor Web App 模板。更好的解决方案是将其作为 InteractiveAuto 运行;这样,我们就可以获得服务器快速加载的能力,然后无需等待即可获得 WebAssembly。

这是一种简单而有效的方法,可以为我们的网站添加 SEO。

有一个问题:在服务器上渲染时,它将加载数据,然后在 WebAssembly 加载时再次加载。

有一种方法可以绕过这个问题,我们将在下一节中探讨。

预加载和持久化状态

如果我们可以避免,我们不希望我们的组件调用数据库两次。

如果你运行 BlazorPrerender 示例并转到 天气 页面,你应该能够看到它加载两次,因为数据是随机的,每次我们请求它时都会生成。

这是在使用 InteractiveServerInteractiveWebAssemblyInteractiveAuto 时看到的行为。页面首先在服务器上渲染。然后,SignalR 或 WebAssembly 被连接并再次加载页面。

这个示例的源代码是 BlazorPrerender 项目。

当我们通过 WebAssembly 传递有关登录用户的信息时,我们使用了这种技术。

在 Blazor 的早期版本中,我们必须添加一个名为persist-component-state的组件,但在.NET 8 中,这个组件默认添加了。这个组件将在服务器上渲染时渲染组件的保存状态,并且当 SignalR 或 WebAssembly 接管时,状态已经存在。

在客户端项目和我们要持久化的组件中(例如示例中的Weather.razor),我们注入一个PersistanceComponentState,并使组件实现Idisposable

@inject PersistentComponentState ApplicationState
@implements IDisposable 

我们添加了一个PersistingComponentStateSubscription组件,该组件将数据保存到应用程序状态中:

private PersistingComponentStateSubscription _subscription; 

OnInitializedAsync中,我们注册监听组件想要持久化数据时运行的代码:

_subscription = ApplicationState.RegisterOnPersisting(PersistState); 

当我们加载数据时,我们首先确保检查应用程序状态。如果数据不可用,我们可以继续并发出一个HTTP请求:

if (ApplicationState.TryTakeFromJson<WeatherForecast[]>("weatherdata", out var stored))
{
    forecasts = stored;
}
else
{
    await Task.Delay(500);
    var startDate = DateOnly.FromDateTime(DateTime.Now);
    var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
    forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToArray();
} 

它指的是一个将数据持久化到应用程序状态的方法:

private Task PersistState()
{
    ApplicationState.PersistAsJson("weatherdata", forecasts);
    return Task.CompletedTask;
}
public void Dispose()
{
    _subscription.Dispose();
} 

服务器将首先渲染内容,当服务器完成时,它将响应整个页面,包括一个Base64编码的 JSON 字符串,数据看起来像这样:

<!--Blazor:{"type":"server","prerenderId":"d0b382c5fa7d4b65a8002157a8b6a1 a2","key":{"locationHash":"F2AAEE86A5A9C5406A2EF4551C02A263059448AC:0","formattedComponentKey":""},"sequence":0,"descriptor":"CfDJ8EzYgDK6\u002BdZLqM2gwGUPDtNbwNLH7VoJxc6/d6CZ4gHE0LtdIMqSoBfSh8OHGynUVW5DKNVBSG4cZBgETzOixExgSkzmqvPY7I58TMjl4XliAJ ae5d2fmVTS7\u002ByDOooQOqVN41jgj\u002BthTcmHEkBng1MukO5/28AsARyCKVXGxlw3cu9ohFo6b38BprF63EPjo7zQqNYRQT2k xkxn9TiFzTga//RyoyQKIwvEkb044SW\u002Bj9tHP1bBt3B8rpE5EATAvbtKEu7yjwUFGb3xsDHvJ6jGAtQOKOXQhKoWM5pp8 z0RMKkxMfeyuQUubu7i48qPSPvvWCnoym79o64FsTlataWG9JeO8V1X9ihTQppyw/
jkc0RHp9Si49UgCVlEuPWMXTjVSVj7gBizQRc7eT0t2v30NwpBrYHvQS0t\u002BgssPyT\u002BTQWCfEcEc7iMboA/oCSqcAJRTWCcGbWroCIKchU1mdTJj48vAuMKKu5tw6Yqo61V\u002BM4wTR7XJ1ffk0KCQ7lKCqNr2ffNRz1RxjbQX8oVU4s="}--> 

由于我们将所有放入应用程序状态的内容都存储为 JSON,因此非常重要,不要包含任何我们没有考虑显示的敏感数据。当然,这对于所有调用都是正确的,因为我们正在使用 JSON 发送数据。

我们还可以在InteractiveServerInteractiveWebAssemblyInteractiveAuto上使用PersistentComponentState。我通常在我的网站上关闭预渲染,但如果你的网站需要 SEO,这会非常棒。

现在,我们了解了一些常见问题和它们的解决方法。

摘要

在本章中,我们探讨了 Blazor WebAssembly 中的一些特定内容。大部分情况下,我们可以在 Blazor Server 和 Blazor WebAssembly 中重用组件,并且我们可以通过使用本章学到的知识来加速 WebAssembly。

我们还研究了原生依赖项,这为重用其他库和混合语言打开了可能性。如果我们不需要支持这两种场景,我们可以充分利用 WebAssembly。

在下一章中,我们将探讨源生成器

第十七章:检查源生成器

在本章中,我们将探讨编写生成代码的代码。尽管本章与 Blazor 开发没有直接关系,但我们将发现它仍然与 Blazor 有关。

源生成器是一个单独的主题,但我想介绍一下,因为它们被 Blazor 使用,而且坦白说,这是我最喜欢的功能之一。

我就是这样的人,如果我知道我需要反复重复这 10 分钟,我会花一整天的时间编写源代码来节省这 10 分钟。重复性任务从来不是我的最爱。

在本章中,我们将涵盖以下内容:

  • 源生成器是什么

  • 如何开始使用源生成器

  • 社区项目

本章的想法是让你将其作为参考,以便你可以自己实现一个新项目。

技术要求

本章是一个参考章节,并且与本书的其他章节没有任何关联。

你可以在 github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter17 找到本章结果的源代码。

源生成器是什么

在许多情况下,我们会发现自己反复编写相同类型的代码。在过去,我使用 T4 模板生成代码,甚至编写了 存储过程 和可以帮助我生成代码的应用程序。源生成器 是 .NET 编译器平台(Roslyn)SDK 的一部分。

生成器为我们提供了访问表示当前正在编译的所有用户代码的编译对象的权限。从那里,我们可以检查对象,并基于此编写额外的代码。

好吧,这听起来很复杂,如果我说编写源生成器很容易,那我就撒谎了,但它可以立即为我们节省大量时间。所以,让我们稍微分解一下。

当我们编译代码时,编译器会执行以下步骤:

  1. 编译过程开始运行。

  2. 源生成器分析代码。

  3. 源生成器生成新的代码。

  4. 编译过程继续。

步骤 2步骤 3 是源生成器所做的事情。

在 Blazor 中,源生成器一直被使用;这是一个将 .razor 文件转换为 C# 代码的源生成器。

我们可以通过向我们的 .csproj 文件中添加以下内容来查看 Blazor 生成的代码:

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 

添加此代码将在 obj 文件夹中为 razor 组件生成输出文件。

我们可以在以下位置找到它们:\obj\Debug\net8.0\generated\Microsoft.NET.Sdk.Razor.SourceGenerators\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator

我们可以通过使用以下方式选择文件的输出位置:

<CompilerGeneratedFilesOutputPath>THEPATH</CompilerGeneratedFilesOutputPath> 

你可以将 THEPATH 替换为你希望文件输出的路径。

在那个文件夹中,我们可以找到一个名为 Pages_Counter_razor.g.cs 的文件,它是计数器组件的 C# 表示形式。

Microsoft.NET.Sdk.Razor.SourceGenerators-generator 当然是一个非常高级的源生成器。

让我们考虑一个场景:在工作中,我们为服务创建服务和接口。这些接口的唯一用途是用于测试目的,就像我们在整本书中构建存储库的方式一样。

在这种情况下,向服务添加方法意味着我们需要将方法添加到类和接口中。我们试图通过将接口和类放在同一个文件中来简化这个过程。然而,我们仍然忘记了接口,提交了代码,直到构建完成并生成了 NuGet 包才注意到错误。

我们发现了一个名为InterfaceGenerator的源生成器;向我们的类添加一个属性将为我们生成接口。

让我们看看这个例子:

public class SampleService
{
    public double Multiply(double x, double y)
    {
        return x * y;
    }
    public int NiceNumber => 42;
} 

这是一个简单的服务类(来自InterfaceGenerator的 GitHub 页面)。向代码添加一个属性将自动生成一个接口,我们可以添加对该接口的引用:

[GenerateAutoInterface]
public class SampleService: ISampleService
… 

生成的接口将始终是最新的。这个示例是一个极好的例子,说明了源代码生成器如何节省时间和消除痛点。

源生成器很强大;我们可以访问一个语法树,我们可以查询它。我们可以遍历所有类,找到具有特定属性或实现接口的类,例如,然后基于这些信息生成代码。

存在一些限制。我们无法知道源生成器将按什么顺序运行,因此我们无法根据生成的代码生成代码。我们只能添加代码,而不能修改代码。

下一个部分将探讨我们如何构建我们的源生成器。

如何开始使用源生成器

是时候看看我们如何构建我们的源代码生成器了。Chapter17文件夹是我们讨论的完成示例。说明将不会是逐步指南。

要创建一个源代码生成器,我们需要一个针对.NET Standard 2.0的类库。我们还需要在该库中添加对 NuGet 包Microsoft.CodeAnalysis.CSharpMicrosoft.CodeAnalysis.Analyzers的引用。我们还需要确保我们的.csproj文件有<LangVersion>latest</LangVersion>

要创建一个源代码生成器,我们需要创建一个具有两个功能的类:

  • 它需要具有[Generator]属性。

  • 它需要实现ISourceGenerator

模板代码应该看起来像这样:

using Microsoft.CodeAnalysis;
namespace SourceGenerator;
[Generator]
public class HelloSourceGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        // Code generation goes here
    }
    public void Initialize(GeneratorInitializationContext context)
    {
        // No initialization required for this one
    }
} 

Initialize方法中,我们添加可能需要的任何初始化;而在Execute方法中,我们编写生成的代码。

我们现在正在构建的生成器当然是一个愚蠢的例子,但它也展示了源生成器的一些强大功能。

Execute方法中,我们添加以下代码:

 // Build up the source code
        string source = """
namespace BlazorWebAssemblyApp;
public class GeneratedService
    {
        public string GetHello()
        {
            return "Hello from generated code";
        }
    }
""";
        // Add the source code to the compilation
        context.AddSource($"GeneratedService.g.cs", source); 

它将源变量中的代码保存为 GeneratedService.g.cs。我们还在此文件中使用原始字符串字面量 – 这是 .NET7 中我最兴奋的功能。通过添加三个双引号,我们不需要转义字符串;我们可以在字符串内部自由添加更多双引号。如果您想转义超过三个双引号,您可以在开头和结尾添加更多。

要将源生成器添加到我们的项目中,我们可以像这样添加项目:

 <ItemGroup>
    <ProjectReference 
        Include="..\SourceGenerator\SourceGenerator.csproj" 
        OutputItemType="Analyzer"
        ReferenceOutputAssembly="false"/>
  </ItemGroup> 

当我们编译我们的项目时,GeneratedService 将被生成,我们可以使用这段代码。

现在,我们可以注入服务并在我们的组件中使用它:

@page "/"
@inject GeneratedService service
<h1>@service.GetHello()</h1> 

不要忘记将其添加到 Program.cs 中:

builder.Services.AddScoped<GeneratedService>(); 

上述示例并不是在现实场景中实际使用的方式,但我想要展示的是,开始使用它并不复杂。

有时 Visual Studio 编辑器不会识别这些生成的文件,我们会在代码编辑器中看到一些红色波浪线。这是因为源生成器的顺序(没有保证的顺序)会导致这些问题,尤其是在将源生成器与其他也生成的类(如 .razor 文件)结合使用时。

在下一节中,我们将探讨我们可以在项目中使用的源生成器。

社区项目

源生成器自 .NET5/6 以来一直存在,我们可以使用许多社区/开源项目在我们的项目中。让我们在接下来的章节中探索它们。

InterfaceGenerator

我们已经讨论了 InterfaceGenerator。无需重复编写相同的内容即可生成接口,这将节省时间并帮助您避免问题,尤其是如果您只使用接口进行测试。

我们可以在这里找到它:

github.com/daver32/InterfaceGenerator

Blazorators

David Pine,以及许多贡献者,构建了 Blazorators,它可以将 TypeScript 定义文件转换为可用于任何 Blazor 项目的 JavaScript 互操作代码。Blazorators 在编写 JavaScript 互操作时消除了许多痛点。

在这里查看他的项目:

github.com/IEvangelist/blazorators

C# 源生成器

Amadeusz Sadowski,以及许多贡献者,制作了一个令人印象深刻的列表,列出了更多关于源生成器的信息以及一些杰出的项目。您可以在以下位置找到这个出色的资源:

github.com/amis92/csharp-source-generators

Roslyn SDK 示例

微软已经向他们的 Roslyn SDK 仓库添加了一些示例。这是一个深入了解源生成器的良好开端。您可以在以下位置找到这些示例:

github.com/dotnet/roslyn-sdk/tree/main/samples/CSharp/SourceGenerators

Microsoft Learn

Microsoft Learn 是学习任何与 C# 相关内容的绝佳资源,源生成器也不例外。

如果你像我一样认为源生成器是自切片面包以来最好的东西,我建议你深入研究 Microsoft Learn 上的文档:

learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview

摘要

在本章中,我们探讨了编写代码以节省时间和减少重复性任务的代码。

Blazor 使用源生成器将 razor 代码转换为 C# 代码,因此,间接地,我们一直在使用它们。

在下一章中,我们将通过访问 .NET MAUI 来了解 Blazor 混合模式。

第十八章:访问 .NET MAUI

到目前为止,我们已经讨论了 Blazor WebAssembly 和 Blazor 服务器,但第三个选项是什么?

在本章中,我们将访问 .NET MAUI,微软的新跨平台开发平台。

本章不会深入探讨 .NET MAUI,因为那可以是一本完整的书。

在本章中,我们将涵盖以下内容:

  • 什么是 .NET MAUI?

  • 创建新项目

  • 查看模板

  • 为 Android 开发

  • 为 iOS 开发

  • 为 macOS 开发

  • 为 Windows 开发

  • 为 Tizen 开发

本章的目的是让你将其作为参考,以便你能够独立实现一个新项目。

技术要求

本章是参考章节,并且与本书的其他章节没有任何关联。

你可以在github.com/PacktPublishing/Web-Development-with-Blazor-Third-Edition/tree/main/Chapter18找到本章的源代码。

什么是 .NET MAUI?

我们将从一点历史开始。

Xamarin 是一家成立于 2011 年 5 月的软件公司,由创建 Mono 的工程师创立,Mono 是 .NET Framework 的免费和开源版本。微软于 2016 年收购了该公司,现在它是 .NET 开发平台的重要组成部分,为使用 C# 和 .NET 构建原生跨平台移动应用提供工具和服务。Xamarin 的技术允许开发者使用单个共享代码库编写原生 iOS、Android 和 Windows 应用程序,这使得为多个平台开发和维护应用程序变得更加容易。

.NET 多平台应用程序用户界面MAUI)是微软的新框架,它是 Xamarin.Forms 的进化。

这是一种创建一个 UI,部署到许多不同的平台,并在每个平台上获得原生控件的方法。.NET MAUI 还可以托管 Blazor,这被称为 Blazor 混合。这样,我们可以在 .NET MAUI 应用程序内部渲染 Blazor 内容,使用与为网络构建相同的控件和代码。使用 Blazor 混合渲染的控件是网络控件,因此我们不会获得原生控件。然而,我们可以混合原生和 Blazor 混合内容。

多年前,我参加了一个与一群顾问的会议。我工作的公司想要投资一个应用程序,我们转向瑞典的一家大型咨询公司,以获得关于我们如何进行的帮助。

一周后,我们再次开会,他们展示了他们的发现。他们的建议是原生开发,不使用任何跨平台框架。

他们进行了一系列的争论,但有两点给我留下了深刻的印象,如下:

  • 原生应用看起来更好,并给用户带来“真实”的设备体验。

  • 共享代码(跨平台)意味着如果一个平台有错误,同样的错误现在在所有平台上都有。

由于 .NET MAUI(以前称为 Xamarin.Forms)使用原生控件,用户无法知道在开发原生应用程序和使用 .NET MAUI 开发之间的区别。最终,它将看起来和感觉像原生应用程序。对于 Blazor 混合来说并非如此,它使用 Web 控件。因此,对于第一个论点有一些合理的论据。现在,我们必须问自己,原生外观和感觉有多重要?看看我 iPhone 上的应用程序,没有多少应用程序看起来相同,所以我争辩说,只要您坚持良好的 UX,这并不那么重要。第二个论点让我非常生气。他们是不是试图说服我们共享代码很糟糕?是的,他们是。在平台之间共享代码非常棒;您只需编写一次代码,修复一次错误,并在所有平台上修复它。

.NET MAUI 给我们提供了两种选择。我们可以使用原生 UI 和 C# 代码,或者使用 Blazor 混合来获取 Web 控件。

创建新项目

要开发跨平台应用程序,我们必须在 Visual Studio 中安装跨平台工具。

如果您还没有这样做,请打开 Visual Studio 安装程序并选择 .NET 多平台应用程序 UI 开发工作流程。

.NET MAUI 有几个模板:.NET MAUI 应用程序.NET MAUI Blazor 混合应用程序.NET MAUI 类库

.NET MAUI 应用程序

.NET MAUI 应用程序模板使用 XAML 创建应用程序。

XAML 也用于 Windows 表现基金会WPF)和通用 Windows 平台UWP)。每个 XAML 版本都有细微的差别,但如果您之前使用过 WPF 或 UWP,它们应该感觉熟悉。

XAML 被转换为原生元素。这样,如果我们的应用程序在 Windows 上运行,它将具有 Windows 应用程序的外观和感觉。如果我们运行在 iOS 设备上,它将看起来和感觉像原生 iOS 应用程序。

如果我们想使用我们的 C# 技能创建跨平台应用程序,这可能是我们最好的选择。使用这种方法,我们将获得原生感觉,而无需在 Kotlin 或 Swift 中编写原生代码。

.NET MAUI 类库

.NET MAUI 类库用于在应用程序之间共享内容、类和功能。

.NET MAUI Blazor 混合应用程序

由于这是一本关于 Blazor 的书,我们将重点关注 .NET MAUI Blazor 混合 App 模板。这是一个将 Blazor 应用程序嵌入到原生壳中的模板。

对于 .NET MAUI Blazor 应用程序项目,我们至少需要:

  • Android 7.0 (API 24) 或更高版本

  • iOS 14 或更高版本

  • macOS 11 或更高版本,使用 Mac Catalyst

.NET MAUI Blazor 混合应用程序项目使用 BlazorWebView 来渲染 Blazor 内容。它与 Blazor 服务器不同,不运行 WebAssembly;它只是我们托管 Blazor 应用程序的第三个选项。

让我们开始一个新的项目并深入了解:

  1. 在 Visual Studio 中,创建一个新的 .NET MAUI Blazor 混合应用程序 项目。

  2. 将项目命名为 BlazorHybridApp 并确保您选择了 .NET 8

  3. 在 Visual Studio 顶部,选择 Windows 机器 并运行项目。

就这些。我们现在有了我们的第一个跨平台 Blazor 混合应用!

计算机的截图  自动生成的描述

图 18.1:在 Windows 上运行的.NET MAUI 应用

我们可能需要在我们的机器上启用开发者模式。如果有提示我们这样做,只需按照说明重新运行应用程序。太好了!我们现在有一个项目了。在下一节中,我们将查看模板的样子。

查看模板

当运行项目时,我们应该能够识别 UI。它就是相同的Hello, world!页面,相同的计数器,以及相同的天气预报。

如果我们查看Components/Pages文件夹,我们会找到 Razor 组件,如果我们打开Counter.razor文件,我们会找到一个看起来像这样的熟悉组件:

@page "/counter"
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
} 

要创建 Blazor 混合应用,添加这样的组件就足够开始了解了,但让我们更深入地了解一下。模板是带有一些添加的 Blazor 启动代码的.NET MAUI 应用。

为了理解正在发生的事情,我们将从Platforms文件夹开始。在Platforms文件夹中,我们将为每个我们可以为 Android、iOS、Mac Catalyst、Tizen 和 Windows 开发的平台找到不同的文件夹。

这是每个平台的开端,它们有一些不同的实现,但最终,它们都指向位于项目根目录的MauiProgram文件。

MauiProgram类设置了所有内容,比如字体和依赖注入:

namespace BlazorHybridApp;
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });
        builder.Services.AddMauiBlazorWebView();
#if DEBUG
        builder.Services.AddBlazorWebViewDeveloperTools();
        builder.Logging.AddDebug();
#endif
        builder.Services.AddSingleton<WeatherForecastService>();
        return builder.Build();
    }
} 

文件中的关键是UseMauiApp<App>,这给我们一些关于接下来会发生什么的线索。下一步是加载App.xaml

App.xaml文件包含许多用于样式的资源。Blazor 的魔法从App.xaml.cs开始:

namespace BlazorHybridApp;
public partial class App : Application
{
    public App()
    {
        InitializeComponent();
        MainPage = new MainPage();
    }
} 

它将应用程序MainPage设置为MainPage类的实例。在MainPage.xaml中,我们已经达到了应用程序中的第一个 Blazor 引用,即BlazorWebView

<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView> 

在这种情况下,我们指的是位于wwwroot文件夹中的index.html,并设置根组件(类似于我们在 Blazor Server 和 Blazor WebAssembly 中的Program.cs中做的)。

在这里,我们还可以添加 XAML 组件,这使得混合 XAML 和 Blazor 组件成为可能。尽管实现看起来不同,但我们应该熟悉这些概念。

index.html几乎与 Blazor WebAssembly 中的相同:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>BlazorHybridApp</title>
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/app.css" rel="stylesheet" />
<link href="BlazorHybridApp.styles.css" rel="stylesheet" />
</head>
<body>
<div class="status-bar-safe-area"></div>
<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="img/blazor.webview.js" autostart="false"></script>
</body>
</html> 

值得注意的是唯一的不同之处是与其他的 JavaScript 不同(Blazor Server 和 Blazor WebAssembly 实现)。从这一点开始,应用程序现在运行的是纯 Blazor。

正如我们在MainPage.xaml中看到的,我们正在加载一个名为Routes的 Razor 文件。这个名字来自 Blazor Web App 模板,看起来像这样:

<Router AppAssembly="@typeof(MauiProgram).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router> 

这是我们找到路由器的地方,我们在这里配置 Razor 组件的位置,并处理找不到的请求。

我们不会深入探讨 Blazor 部分,因为路由器之后的一切都与其他任何 Blazor 托管模型(Blazor Server 和 Blazor WebAssembly)相同。有一个MainLayoutNavMenu以及每个功能的组件(Hello, world!CounterWeather)。

使用 Blazor Server 和 Blazor WebAssembly,我们需要调用 JavaScript 来访问本地资源,如蓝牙、电池和手电筒等。Blazor Hybrid 添加了直接访问本地资源的能力。我们可以通过类似以下代码来访问手电筒(因为我们都喜欢发光的东西):

try
{
    if (FlashlightSwitch.IsToggled)
        await Flashlight.Default.TurnOnAsync();
    else
await Flashlight.Default.TurnOffAsync();
}
catch (FeatureNotSupportedException ex)
{
    // Handle not supported on device exception
}
catch (PermissionException ex)
{
    // Handle permission exception
}
catch (Exception ex)
{
    // Unable to turn on/off flashlight
} 

如果我们运行 Blazor Server 或 Blazor WebAssembly 应用程序,此代码将无法工作。如果我们仍然想在 .NET MAUI 和 Blazor 网页应用程序之间共享组件,我们可以使用依赖注入来实现,就像我们在书中已经做过的几次一样,一个是网页实现,一个是移动实现。

接下来,我们将让我们的神奇应用在 Android 上运行。

针对 Android 的开发

在开发 Android 时有两个选项。我们可以在模拟器物理设备上运行我们的应用程序。

要发布我们的应用程序,我们需要一个 Google 开发者许可证,但在开发和测试时不需要。

在模拟器中运行

我们首先需要安装一个模拟器来在 Android 模拟器上运行我们的应用程序:

  1. 在 Visual Studio 中,打开工具 | Android | Android 设备管理器

  2. 点击新建按钮并配置一个新设备(默认设置应该可以):

图片

图 18.2:Android 设备配置

  1. 点击创建以下载设备镜像并配置它。

  2. 在 Visual Studio 顶部选择新创建的模拟器并运行项目。启动模拟器将需要几分钟时间。在开发时,请确保不要关闭模拟器以加快部署时间。

为了让模拟器运行得更快,我们可以根据所使用的处理器启用硬件加速。

要启用硬件加速,请参阅官方文档:learn.microsoft.com/en-us/xamarin/android/get-started/installation/android-emulator/hardware-acceleration?pivots=windows

太好了!我们现在已经在 Android 模拟器中运行了我们的应用程序:

图片

图 18.3:在 Android 模拟器中运行的应用程序

接下来,我们将在物理设备上运行应用程序。

在物理设备上运行

如果我们想在物理设备上尝试我们的应用程序,我们需要在我们的 Android 设备上做一些事情。这可能会因设备而异。

首先,我们需要确保手机已解锁为开发者模式:

  1. 前往设置屏幕。

  2. 选择关于手机

  3. 点击构建号七次,直到出现您现在是一名开发者

其次,我们需要启用 USB 调试:

  1. 前往设置屏幕。

  2. 选择开发者选项

  3. 打开 USB 调试 选项。

  4. 一些设备还需要启用 通过 USB 安装

现在我们已经准备好在物理设备上尝试我们的应用程序了。

  1. 使用 USB 线缆将您的设备连接到计算机。

  2. 在 Visual Studio 顶部的菜单中,点击 Android 本地设备 下的箭头,并选择您的设备。

  3. 运行,Visual Studio 将将应用程序部署到设备上。

我们现在应该在我们的设备上运行我们的应用程序。

在另一台设备上运行代码是一种非凡的感觉。多年来,我为 Windows 8 和 Windows Phone 开发了超过 100 个应用程序。然而,时至今日,看到我的应用程序部署到另一台物理设备上,我仍然有同样的感觉。

接下来,我们将查看我们为 iOS 开发有哪些选项。

开发 iOS 应用

苹果不允许在非苹果计算机上编译 iOS 代码。还有像 MacinCloud 和 MacStadium 这样的云选项,但在这本书中我们不会讨论这些选项。

这意味着我们必须拥有一台 Mac(用于使用模拟器)或者拥有一个苹果开发者许可证(用于使用热重启)。

为了使我们的 iOS 设备能够工作,我们需要将其设置为开发者模式:

  1. 打开您 iPhone 的 设置 应用。

  2. 向下滚动一点,找到 隐私和安全,然后点击它。

  3. 寻找名为 开发者模式 的选项。如果您找不到,您可能需要将手机连接到 Xcode。它因操作系统版本而异,但可以向 Google 或 Bing 求助。有许多资源可以帮助您解决您版本的问题。

  4. 应该有一个切换开关;将其翻转以启用开发者模式。

  5. 您的 iOS 设备可能会提醒您,这可能会使您的设备的安全性降低。不用担心,只需点击 重启 以继续。

  6. 一旦您的设备重新启动,解锁它。您会看到一个提示,询问您是否确定要启用开发者模式。继续点击 开启,如果需要输入密码,请输入。

热重启

要在我们的物理设备上测试我们的应用程序,我们可以使用热重启。热重启功能仅设计用于我们在开发过程中测试应用程序,我们将无法发布应用程序。

首先,我们需要安装 iTunes。如果您没有 iTunes,您可以从 Windows 商店安装。

在 Visual Studio 的顶部菜单中,如果我们选择 iOS 本地设备,我们会看到一个友好的向导,它会精确地告诉我们需要做什么。第一步是提供信息性的,并允许我们安装 iTunes。

接下来,是时候输入我们的 App Store Connect API 密钥信息了。为了能够提供这些信息,我们需要有一个苹果开发者账户。截至写作时,这需要花费 99 美元。

关于如何找到这些信息,有非常优秀的说明。

您将看到以下屏幕:

计算机的截图 自动生成描述

图 18.4:Apple Connect API 密钥信息屏幕

然后,您需要采取以下步骤:

  1. 您可以通过访问appstoreconnect.apple.com/access/api创建一个新的密钥。

  2. 点击请求 API 密钥然后生成 API 密钥

  3. 输入名称Visual Studio并选择访问开发者

  4. 将不同的值复制到 Visual Studio 中,下载 API 密钥,并将文件作为私钥路径选择。

  5. 接下来,选择一个团队,我们就准备好了。

  6. 运行应用程序并查看它在您的 iPhone 上运行:

图 18.5:在 iPhone 上运行的应用程序

接下来,我们将探讨如何设置模拟器。

模拟器

模拟器在 Mac 上运行应用程序,但结果显示在 PC 上。模拟器与仿真器不同。仿真器在机器上(在我们的案例中,是 PC)上运行代码。模拟器在原生操作系统(macOS)上运行,模仿 iPad 或 iPhone。

要使模拟器工作,我们需要在同一网络上有苹果电脑。Visual Studio 将帮助我们设置一切。我们必须安装 Xcode。在您的 Mac 上,从 App Store 安装 Xcode,启动它以同意许可协议并选择您想要为哪些设备开发。

我们还需要打开 Mac 的远程访问。我们可以通过以下方式做到这一点:

  1. 在 Mac 上,按cmd + space调用 Spotlight,搜索远程登录,然后打开共享系统偏好设置

  2. 启用远程登录选项以允许 Visual Studio 连接到 Mac。

  3. 设置仅限这些用户的访问权限,并确保您的用户包含在列表或组中。

我们现在已经在 Mac 上准备好了所有东西。在 PC 上的 Visual Studio 中,我们现在可以配对我们的 Mac:

  1. 选择工具 | iOS | 与 Mac 配对

  2. 按照向导中的说明(与上面相同)。

  3. 从列表中选择 Mac 并点击连接。Visual Studio 现在可以帮助您安装开始所需的东西。Mac 安装所有这些可能需要一段时间,所以如果不起作用,模拟器可能还没有安装。

  4. 在 Visual Studio 顶部的下拉菜单中,我们可以选择iOS 模拟器,然后选择一个设备来运行我们的应用程序。

计算机屏幕截图  自动生成描述

图 18.6:在 Visual Studio 中的设备选择

  1. 运行应用程序,模拟器将启动。如果我们在 iPad Mini 上运行它,应用程序将看起来像这样:

图 18.7:在 iPad 模拟器中运行的应用程序

我们现在有两种在 iOS 设备上运行和测试应用程序的方法。我们还可以直接将 iPhone 连接到 Mac,并通过 Wi-Fi 运行应用程序。有关通过 Wi-Fi 调试的更多信息,请参阅官方文档:learn.microsoft.com/en-us/xamarin/ios/deploy-test/wireless-deployment

接下来,我们将为 macOS 构建一个应用程序。

macOS 开发

我们没有从 Windows 机器运行或部署到 macOS 的选项。要在 Mac 上运行我们的应用程序,请按照以下步骤操作:

  1. 在 Mac 上,使用 VS Code 打开我们的项目。

  2. 在撰写本文时,将 .NET MAUI 工具安装在 VS Code 中仍然是一个预览版本,并且微软宣布 VS for Mac 已停止开发。这是跟踪在 Mac 上安装工具的最佳来源:learn.microsoft.com/en-us/dotnet/maui/get-started/installation?view=net-maui-8.0&tabs=visual-studio-code。请遵循链接中的说明。

  3. 运行项目,我们的应用将显示出来:

图 18.8:在 macOS 上运行的应用

在这种情况下,我们是在同一平台上运行应用程序,没有仿真器或模拟器,这比在单独的设备上运行要简单得多。

接下来,我们将在 Windows 上运行我们的应用程序。

为 Windows 开发

在 Windows 上运行应用程序是我们之前在 .NET MAUI Blazor Hybrid App 部分的 步骤 3 中所做的事情。再次强调,执行以下步骤:

  1. 将下拉菜单更改为 Windows Machine 并运行项目。我们可以在本章开头的 图 18.1 中看到结果。

与 macOS 一样,我们在同一平台上运行应用程序,没有仿真器或模拟器,这比在单独的设备上运行要简单得多。

接下来,我们将看看 Tizen。

为 Tizen 开发

Tizen 是一个主要针对电视和手表的操作系统。我的三星 Gear S3 运行的是 Tizen。Tizen 由三星管理,而不是微软。其他制造商能够接入这个平台的能力,正好显示了 .NET MAUI 平台是多么出色。

在撰写本文时,Tizen 的体验略有滞后。由于这不是一个官方平台,并且由于工具的状态,我决定不包括指南。

但 Tizen 正在开发工具,所以如果你想将你的应用转移到运行 Tizen 的电视上,你应该了解一下。

摘要

在本章中,我们探讨了使用 Blazor Hybrid 进行跨平台开发。我之前在本章中提到过这一点,但再次提一下,在手机或非计算机设备上运行代码是一件非常有趣的事情。这种感受是无法比拟的。即使你并不打算为移动设备开发,也值得一试。

使用 .NET MAUI,我们可以利用我们现有的 C# 知识,也许更重要的是,我们的 Blazor 知识来创建移动应用程序。

第十九章:从哪里开始

这本书即将结束,我想让你记住我们在 Blazor 预览阶段以来在生产环境中遇到的一些事情。我们还将讨论从这里开始的方向。

在本章中,我们将涵盖以下主题:

  • 运行 Blazor 的生产经验总结

  • 下一步

运行 Blazor 的生产经验总结

由于 Blazor 处于预览阶段,我们一直在生产环境中运行 Blazor 服务器。在大多数情况下,一切运行良好,没有问题。偶尔,我们遇到了一些问题,我将在本节中与您分享我们的经验总结。

我们将查看以下内容:

  • 解决内存问题

  • 解决并发问题

  • 解决错误

  • 旧浏览器

这些是我们遇到的一些问题,我们已经以对我们有效的方式解决了它们。

解决内存问题

我们最新的升级增加了许多用户,随之而来的是服务器负载的增加。服务器管理内存相当好,但在这个版本中,后端系统有点慢,所以用户会按 F5 重新加载页面。然后,电路会断开,并创建一个新的电路。旧的电路会等待用户再次连接到服务器,默认时间为 3 分钟。

然后,用户将有一个新的电路,并且永远不会再次连接到旧的电路,但在这三分钟内,用户的会话状态仍然会占用内存。这可能对大多数应用程序来说不是问题,但我们正在将大量数据加载到内存中——数据、渲染树以及与之相关的所有内容都将保留在内存中。

那么,我们能从中学到什么呢?Blazor 是一个单页应用程序。重新加载页面就像重启应用程序一样,这意味着我们应该始终确保添加从页面内部更新数据的能力(如果这对应用程序有意义)。我们也可以像在 第十一章管理状态 – 第二部分 中所做的那样,在数据变化时更新数据。

在我们这个案例中,我们给服务器增加了更多内存,并确保在用户界面中有重新加载按钮,这些按钮可以刷新数据而不重新加载整个页面。最终目标是添加实时更新,当数据发生变化时,持续更新用户界面。

如果增加服务器的内存不是一个选择,我们可以尝试将垃圾回收从服务器改为桌面。.NET 垃圾回收有两个模式:

  • 工作站模式针对在通常没有很多内存的工作站上运行进行了优化。它每秒运行垃圾回收多次。

  • 服务器模式针对通常有很多内存的服务器进行了优化,优先考虑速度,这意味着它每 2 秒只会运行垃圾回收器一次。

垃圾回收器的模式可以在项目文件或 runtimeconfig.json 文件中通过更改 ServerGarbageCollection 节点来设置:

<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup> 

虽然增加内存可能是一个更好的主意。

我们也注意到了处理我们的数据库上下文的重要性。请确保使用 IDbContextFactory 创建数据上下文的实例,并在我们完成时,通过使用 Using 关键字来释放它。

然后,数据上下文将只短暂可用,然后被释放,快速释放内存。

解决并发问题

我们经常遇到数据上下文已经被使用,无法从两个不同的线程访问数据库的问题。

这可以通过使用 IDbContextFactory 并在我们完成使用数据上下文后释放它来解决。

在非 Blazor 网站中,同时加载多个组件永远不会是问题(因为网页一次只做一件事),所以 Blazor 能够同时做很多事情,这是我们设计架构时需要考虑的。

解决错误

Blazor 通常会给出易于理解的错误,但在一些罕见的情况下,我们确实会遇到难以解决的问题。我们可以通过在 Startup.cs 中添加以下选项来向我们的电路(对于 Blazor Server)添加详细的错误:

services.AddServerSideBlazor().AddCircuitOptions(options => { options.DetailedErrors = true; }); 

通过这样做,我们将获得更详细的错误。然而,我不建议在生产环境中使用详细的错误。话虽如此,我们在生产中的内部应用程序中打开了此设置,因为内部用户已经接受了培训并了解如何处理它。这使得我们更容易帮助用户,并且错误消息仅在网页浏览器的开发者工具中可见,而不是在用户界面上可见。

旧浏览器

一些客户在旧系统上运行旧浏览器,尽管 Blazor 支持所有主流浏览器,但这种支持不包括真正旧的浏览器。我们最终帮助这些客户升级到 Edge 或 Chrome,仅仅是因为我们认为他们不应该使用不再接收安全补丁的浏览器浏览网页。

即使是我们家里的电视也能运行 Blazor WebAssembly,所以旧浏览器可能不是大问题,但在考虑浏览器支持时,这值得思考。我们需要/想要支持哪些浏览器?

下一步

到这个时候,我们已经知道了 Blazor Server 和 Blazor WebAssembly 之间的区别,也知道何时选择什么,选择其中一个并不是真的那么重要。我们知道如何创建可重用组件,制作 API,管理状态,等等。但接下来我们该怎么办?下一步是什么?

社区

Blazor 社区不如其他框架那么大,但发展迅速。许多人通过博客或视频与社区分享内容。YouTube 和 PluralSight 有很多教程和课程。Twitch 上有越来越多的 Blazor 内容,但在庞大的内容目录中并不总是容易找到。

有许多值得提及的资源:

  • 吉米·恩斯特罗姆 – 如果我没有把我自己列入我的名单,我就不是一个大大的 Blazor 爱好者。我谈论 Blazor,时不时地来点双关语。当我们直播时,我们使用 CodingAfterWork(见下文)。我的博客有很多 Blazor 内容,还有更多即将到来:engstromjimmy.com/. X: @EngstromJimmy.

  • 我们编写的Blazm组件库可以在blazm.net/找到。虽然有许多更好的网格组件,但这也展示了网格组件既简单又复杂。

  • 工作后编码在我们的播客和直播中有很多关于 Blazor 的内容;请在我们的社交媒体上关注我们:codingafterwork.com/FindUs.

  • 丹尼尔·罗思是 Blazor 的产品经理。他非常值得聆听,曾作为嘉宾出现在我们的播客中。在 YouTube 上搜索他。X: @danroth27.

  • 史蒂夫·桑德森是 Blazor 的发明者;他绝对值得关注。他在演讲中继续做一些开创性的工作;在 YouTube 上搜索他。确保观看他在 NDC 奥斯陆的演讲,他在那里首次展示了 Blazor。X: @stevensanderson.

  • Awesome-Blazor有一个包含大量 Blazor 相关链接和资源的巨大列表,可以在github.com/AdrienTorris/awesome-blazor找到。

  • 杰夫·弗里茨在 Twitch 上分享 Blazor 知识(以及其他内容):www.twitch.tv/csharpfritz. X: @csharpfritz.

  • 克里斯·圣蒂是一位同行作者,他为 Blazor 制作了许多真正令人惊叹的包。他在博客上有很多内容:chrissainty.com/. X: @chris_sainty.

  • 卡尔·富兰克林BlazorTrain.com/上制作了许多 Blazor 视频。X: @carlfranklin.

  • 约翰·希尔顿有很多 Blazor 内容。你可以在jonhilton.net/找到他。X: @jonhilt.

  • 帕特里克·戈德在他的 YouTube 频道上有许多精彩内容:www.youtube.com/@PatrickGod. X: @_PatrickGod.

  • 大卫·派恩是一位同行作者,也是 Blazorators 的创造者,可以在github.com/IEvangelist/blazorators找到他。X: @davidpine7.

  • 彼得·莫里斯是 Fluxor 的创造者,是一位值得关注的伟大人物。X: @MrPeterLMorris

  • 迈克尔·华盛顿是一位同行作者,我们可以在adefwebserver.com/找到他。X: @ADefWebserver.

  • Ed Charbeneau 总是能提供优质的内容。请务必关注他。edcharbeneau.com/ www.twitch.tv/edcharbeneauwww.youtube.com/edwardcharbeneauwww.twitch.tv/codeitlivewww.youtube.com/@telerik。X: @EdCharbeneau

  • Eric Johansson 是 Twitch 上的常客,展示他的项目并将他的 .NET Framework 应用现代化到一个更现代的平台 www.twitch.tv/thindal X: @EricJohansson

  • Egil Hansen 是 bUnit 的创造者。我们可以在以下链接找到他:egilhansen.com/about/。X: @egilhansen

  • Sam Basu 是关注 .NET MAUI 内容时的一个优秀人选。www.twitch.tv/codeitlivewww.youtube.com/@telerik。X: @samidip

  • Junichi Sakamoto 制作了大量的出色 Blazor 库,从连接游戏手柄到翻译和预渲染,应有尽有。你可以在以下链接找到他的项目:github.com/jsakamoto。X: @jsakamoto

  • Blazor University 提供了大量的培训材料,是学习更多知识的绝佳资源:blazor-university.com/

  • Gerald Versluis 在他的 YouTube 频道上提供了大量与各种 .NET 相关的内容:youtube.com/GeraldVersluis。X: @jfversluis

  • Maddy Montaquilla 的视频非常值得观看;在 YouTube 上搜索她来观看她的视频。X: @maddymontaquila

  • James Montemagno 拥有一个内容丰富的 YouTube 频道,其中包含大量的 .NET MAUI 内容:www.youtube.com/JamesMontemagno。X: @JamesMontemagno

  • Daniel Hindrikes 在这个 YouTube 频道上提供了一些关于 .NET MAUI 的优质内容:www.youtube.com/@DanielHindrikes。X: @hindrikes

组件

大多数第三方组件供应商,如 Progress Telerik、DevExpress、Syncfusion、Radzen、ComponentOne 以及更多,都投资了 Blazor。有些需要付费,有些是免费的。还有很多开源组件库供我们使用。

这个问题经常出现:我是 Blazor 的初学者。我应该使用哪个第三方供应商? 我的建议是在投资库(无论是金钱还是时间)之前,先尝试弄清楚你需要什么。

许多供应商可以完成我们所需要的一切,但在某些情况下,使应用程序工作可能需要更多的努力。我们开始自己开发一个网格组件,过了一段时间后,我们决定将其开源。

这就是 Blazm 的诞生。我们有一些特殊的要求(并不复杂),但它们要求我们反复编写大量代码,以便在第三方供应商组件中使其工作。

通过编写我们的组件,我们学到了很多,这实际上是非常容易做到的。我的建议并不总是要编写自己的组件。专注于你试图解决的真正业务问题会更好。

对于我们来说,构建一个相当高级的网格组件教会了我们很多关于 Blazor 内部工作原理的知识。

想想你需要什么,尝试不同的供应商以查看哪个最适合你。也许一开始自己构建组件会更好,这样你可以更多地了解 Blazor。

但始终关注你的代码。如果你重复相同的代码,将其封装在组件中。始终思考:这能成为一个可重用的组件吗?

我们目前使用一个组件供应商,但我们把所有组件都封装在我们自己的一个组件中。这样,设置默认值和添加适合我们的逻辑就变得很容易,就像我们在整本书中学到的那样。

摘要

在本章中,我们检查了在生产环境中运行 Blazor 时遇到的一些挑战,并讨论了下一步该怎么做。

在整本书中,我们学习了 Blazor 的工作原理以及如何创建基本和高级组件。我们实现了使用认证和授权的安全功能。我们创建并使用了连接到“数据库”的 API。

我们进行了 JavaScript 调用和实时更新。我们调试了我们的应用程序并测试了我们的代码,最后但同样重要的是,我们探讨了如何部署到生产环境。

现在,我们已经准备好将所有这些知识应用到下一个冒险,另一个应用程序中。我希望你在阅读这本书的过程中和我写作这本书一样感到乐趣。成为 Blazor 社区的一员非常有趣,我们每天都在学习新事物。

感谢您阅读这本书。请保持联系。我很乐意了解您构建的东西!

欢迎加入 Blazor 社区!

加入我们的社区 Discord

加入我们的社区 Discord 空间,与作者和其他读者进行讨论:

packt.link/WebDevBlazor3e

posted @ 2025-10-22 10:25  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报