Blazor-WebAssembly-示例第二版-全-
Blazor WebAssembly 示例第二版(全)
原文:
zh.annas-archive.org/md5/7f5240a36c2eea475fe48291c475fedd译者:飞龙
前言
Blazor WebAssembly 是一个框架,它使您能够构建使用 C# 而不是 JavaScript 的单页网页应用。它建立在流行的、健壮的 ASP.NET 框架之上。Blazor WebAssembly 不需要插件或附加组件来在浏览器中使用 C#。它只要求浏览器支持 WebAssembly,而所有现代浏览器都支持。
在本书中,您将完成实际项目,这些项目将教会您 Blazor WebAssembly 框架的基础知识。每一章都包含一个独立的项目,并提供详细的逐步说明。每个项目都旨在突出 Blazor WebAssembly 的一些或多个重要概念。
在本书结束时,您将拥有构建简单独立网页应用和具有 SQL Server 后端的主机网页应用的经验。
这是本书的第二版。在本版中,我们增加了有关调试、部署到 Microsoft Azure 以及使用 Microsoft Azure Active Directory 保护应用程序的章节。此外,本书中的所有项目都已更新,以使用 Blazor WebAssembly 框架的最新版本。
本书面向对象
本书面向那些厌倦了不断涌现的新 JavaScript 框架,并希望利用他们在 .NET 和 C# 方面的经验来构建可在任何地方运行的网页应用的经验丰富的网页开发者。
本书面向任何希望通过强调实践而非理论来快速学习 Blazor WebAssembly 的人。它使用完整、逐步的示例项目,这些项目易于跟随,以教授您使用 Blazor WebAssembly 框架开发网页应用所需的概念。
您不需要成为专业开发者就能从本书的项目中受益,但您需要一些 C# 和 HTML 的经验。
本书涵盖内容
第一章,Blazor WebAssembly 简介,介绍了 Blazor WebAssembly 框架。它解释了使用 Blazor 框架的好处,并描述了三种托管模型之间的区别:Blazor Server、Blazor Hybrid 和 Blazor WebAssembly。在强调使用 Blazor WebAssembly 框架的优势后,讨论了 WebAssembly 的目标和支持选项。最后,它指导您完成设置计算机以完成本书中项目的步骤。在本章结束时,您将能够继续阅读本书的任何其他章节。
第二章,构建您的第一个 Blazor WebAssembly 应用程序,通过创建一个简单的项目介绍了 Razor 组件。本章分为两个部分。第一部分解释了 Razor 组件、路由、Razor 语法以及在开发应用程序时如何使用热重载。第二部分逐步引导您使用 Microsoft 提供的 Blazor WebAssembly App 项目模板创建您的第一个 Blazor WebAssembly 应用程序。到本章结束时,您将能够创建一个演示 Blazor WebAssembly 项目。
第三章,调试和部署 Blazor WebAssembly 应用程序,通过创建一个简单的项目向您展示了如何调试和部署 Blazor WebAssembly 应用程序。本章分为两个部分。第一部分解释了调试、日志记录、异常处理、使用即时编译(AOT)以及将应用程序部署到 Microsoft Azure。第二部分逐步引导您调试和部署 Blazor WebAssembly 应用程序。到本章结束时,您将能够调试一个简单的 Blazor WebAssembly 应用程序并将其部署到 Microsoft Azure。
第四章,使用模板组件构建模态对话框,通过创建模态对话框组件向您介绍了模板组件。本章分为两个部分。第一部分解释了RenderFragment参数、EventCallback参数和 CSS 隔离。第二部分逐步引导您创建模态对话框组件并将其移动到您自己的自定义 Razor 类库中。到本章结束时,您将能够创建一个模态对话框组件并通过 Razor 类库与多个项目共享。
第五章,使用 JavaScript 互操作性(JS Interop)构建本地存储服务,通过创建本地存储服务向您展示了如何在 Blazor WebAssembly 中使用 JavaScript。本章分为两个部分。第一部分解释了为什么您偶尔还需要使用 JavaScript 以及如何从.NET 中调用 JavaScript 函数。为了完整性,它还涵盖了如何从 JavaScript 中调用.NET 方法。最后,它介绍了项目中使用的 Web Storage API。在第二部分中,它逐步引导您创建和测试一个写入和读取浏览器本地存储的服务。到本章结束时,您将能够通过使用 JS Interop 从 Blazor WebAssembly 应用程序中调用 JavaScript 函数来创建本地存储服务。
第六章,构建作为渐进式 Web 应用程序(PWA)的天气应用程序,通过创建一个简单的天气 Web 应用程序,为您介绍了渐进式 Web 应用程序。本章分为两个部分。第一部分解释了什么是 PWA 以及如何创建它。它涵盖了清单文件和各种类型的服务工作者。还描述了如何使用项目所需的 CacheStorage API、地理位置 API 和 OpenWeather One Call API。第二部分逐步引导您创建一个 5 天的天气预报应用程序,并将其转换为 PWA,通过添加标志、清单文件和服务工作者。最后,它展示了如何安装和卸载 PWA。到本章结束时,您将能够通过添加标志、清单文件和服务工作者将 Blazor WebAssembly 应用程序转换为 PWA。
第七章,使用应用程序状态构建购物车,通过创建购物车 Web 应用程序,解释了如何使用应用程序状态。本章分为两个部分。第一部分解释了应用程序状态和依赖注入。第二部分逐步引导您创建购物车应用程序。为了在您的应用程序中维护状态,您将创建一个服务,并将其注册到 DI 容器中,然后注入到您的组件中。到本章结束时,您将能够使用依赖注入在 Blazor WebAssembly 应用程序内维护应用程序状态。
第八章,使用事件构建看板板,通过创建看板板 Web 应用程序,为您介绍了事件处理。本章分为两个部分。第一部分讨论了事件处理、属性展开和任意参数。第二部分逐步引导您创建一个使用DragEventArgs类来启用您在投放区域之间拖放任务的看板板应用程序。到本章结束时,您将能够处理您的 Blazor WebAssembly 应用程序中的事件,并且会舒适地使用属性展开和任意参数。
第九章,上传和读取 Excel 文件,解释了如何上传各种类型的文件以及如何使用Open XML SDK读取 Excel 文件。本章分为两个部分。第一部分解释了上传一个或多个文件和调整图像大小。它还解释了如何使用虚拟化来渲染数据以及如何从 Excel 文件中读取数据。第二部分逐步引导您创建一个可以上传和读取 Excel 文件并使用虚拟化渲染 Excel 文件数据的应用程序。到本章结束时,您将能够将文件上传到 Blazor WebAssembly 应用程序中,使用 Open XML SDK 读取 Excel 文件,并使用虚拟化渲染数据。
第十章,使用 Azure Active Directory 保护 Blazor WebAssembly 应用程序,通过创建一个简单的应用程序来显示声明的内容,教您如何通过创建一个简单的应用程序来保护 Blazor WebAssembly 应用程序。本章分为两个部分。第一部分解释了身份验证和授权之间的区别。它还教您如何处理身份验证以及如何使用Authorize属性和AuthorizeView组件。第二部分逐步引导您将应用程序添加到 Azure AD,并使用它进行身份验证和授权。到本章结束时,您将能够使用 Azure AD 保护 Blazor WebAssembly 应用程序。
第十一章,使用 ASP.NET Web API 构建任务管理器,通过创建任务管理器 Web 应用程序,为您介绍了托管 Blazor WebAssembly 应用程序。这是第一章节使用 SQL Server。它分为两个部分。第一部分描述了托管 Blazor WebAssembly 应用程序的组件。它还解释了如何使用HttpClient服务和各种 JSON 辅助方法来操作数据。最后一部分逐步引导您创建一个将数据存储在 SQL Server 数据库中的任务管理器应用程序。您将使用 Entity Framework 创建一个 API 控制器和操作。到本章结束时,您将能够创建一个使用 ASP.NET Web API 更新 SQL Server 数据库数据的托管 Blazor WebAssembly 应用程序。
第十二章,使用 EditForm 组件构建支出跟踪器,通过创建支出跟踪器 Web 应用程序,教您如何使用EditForm组件。本章使用 SQL Server。它分为两个部分。第一部分介绍了EditForm组件、内置输入组件和内置验证组件。它还解释了如何使用导航锁定来防止用户在保存编辑之前导航到另一个页面。最后一部分逐步引导您创建一个使用EditForm组件和一些内置组件来添加和编辑存储在 SQL Server 数据库中的支出的支出跟踪器应用程序。到本章结束时,您将能够使用EditForm组件与内置组件一起输入和验证存储在 SQL Server 数据库中的数据。
为了充分利用本书
我们建议您阅读本书的前两章,以了解如何设置您的计算机以及如何使用 Blazor WebAssembly 项目模板。之后,您可以按任何顺序完成剩余的章节。随着您在书中的进展,每个章节的项目变得越来越复杂。最后两章需要 SQL Server 数据库来完成项目。第三章和第十章需要 Microsoft Azure 订阅。
您将需要本书中涵盖的以下软件/硬件:
-
Visual Studio 2022 Community Edition
-
SQL Server 2022 Express Edition
-
Microsoft Azure
如果您使用的是本书的数字版,我们建议您自己输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
本书假设您是一位经验丰富的 Web 开发者。您应该有一些 C#和 HTML 的经验。
有些项目使用 JavaScript 和 CSS,但所有代码都提供。此外,还有两个项目使用 Entity Framework,但同样,所有代码都提供。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还提供其他丰富的书籍和视频的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
代码实战
本书的相关代码实战视频可以在(packt.link/CodeinAction)查看。
下载彩色图像
我们还提供包含本书中使用的截图/图表的彩色 PDF 文件。您可以从这里下载:packt.link/Q27px。
使用的约定
本书使用了几个文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“将DeleteProduct方法添加到@code块中。”
代码块设置如下:
private void DeleteProduct(Product product)
{
cart.Remove(product);
total -= product.Price;
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
public class CartService : ICartService
{
**public** **IList<Product> Cart {** **get****;** **private****set****; }**
**public****int** **Total {** **get****;** **set****; }**
public event Action OnChange;
}
任何命令行输入或输出都按以下方式编写:
Add-Migration Init
Update-Database
粗体: 表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“从构建菜单中选择构建解决方案选项。”
警告或重要提示看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送电子邮件至 customercare@packtpub.com。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《Blazor WebAssembly By Example,第二版》,我们很乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在移动中阅读,但无法携带您的印刷书籍到任何地方吗?您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取好处:
- 扫描二维码或访问以下链接

packt.link/free-ebook/9781803241852
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他好处发送到您的电子邮件。
第一章:Blazor WebAssembly 简介
Blazor WebAssembly 是微软为在 .NET Framework 上构建交互式 Web 应用程序而推出的新 单页应用程序(SPA)框架。由于它基于 .NET Framework,Blazor WebAssembly 允许您在客户端和服务器上运行 C# 代码。因此,我们不再被迫在客户端编写 JavaScript,现在我们可以到处使用 C#。
Blazor 火热非凡!
在客户端运行 C#。
再见,JavaScript!
在本章中,我们将解释使用 Blazor 框架的好处。我们将介绍三种不同的 Blazor 主机模型,并讨论每个模型的优缺点。此外,我们还将讨论 WebAssembly 的目标,并分享其支持情况。最后,我们将指导您设置计算机以完成本书中的项目。
在本章中,我们将涵盖以下主题:
-
使用 Blazor 框架的好处
-
主机模型:
-
Blazor Server
-
Blazor Hybrid
-
Blazor WebAssembly
-
-
Blazor 主机模型之间的区别
-
什么是 WebAssembly?
-
设置您的 PC
使用 Blazor 框架的好处
使用 Blazor 框架提供了几个好处。首先,它是一个基于微软强大且成熟的 .NET Framework 的免费和开源框架。此外,它是一个使用 Razor 语法并可以使用微软卓越的工具开发的 SPA 框架。最后,微软正在积极支持和更新 Blazor 框架。让我们在接下来的部分中详细检查这些好处。
.NET Framework
Blazor 框架建立在 .NET Framework 之上。因此,熟悉 .NET Framework 的人可以快速使用 Blazor 框架变得高效。Blazor 框架利用了 .NET Framework 的强大生态系统,包括 .NET 库和 NuGet 包。此外,由于客户端和服务器都可以用 C# 编写代码,因此客户端和服务器可以共享代码和库。例如,客户端和服务器可以共享用于数据验证的应用程序逻辑。
开源
Blazor 框架是开源的。由于 Blazor 是 ASP.NET 框架的一个功能,因此 Blazor 的所有源代码都可在 GitHub 上找到,作为 dotnet/aspnetcore 仓库的一部分,该仓库由 .NET Foundation 管理。.NET Foundation 是一个独立的、非盈利的组织,旨在支持围绕 .NET 平台的创新、商业友好型开源生态系统。.NET 平台拥有一个强大的社区,超过 3,700 家公司贡献了超过 100,000 次的贡献。
由于 .NET Framework 是免费的,这意味着 Blazor 也是免费的。使用 Blazor,包括商业用途,没有任何费用或许可费用。
SPA 框架
Blazor 框架是一个单页应用(SPA)框架。正如其名所示,SPA 是由单个页面组成的网页应用。应用会动态重写页面中已更改的区域,而不是在每次 UI 更新时加载全新的页面。目标是实现更快的转换,使网页应用感觉更像原生应用。
当页面渲染时,Blazor 会创建一个渲染树,这是一个页面组件的图。它就像浏览器创建的 文档对象模型(DOM)。然而,它是一个虚拟 DOM。UI 的更新应用于虚拟 DOM,并且只有 DOM 和虚拟 DOM 之间的差异会被浏览器渲染。
Razor 语法
Razor 是用于使用 C# 创建动态网页的 ASP.NET 视图引擎。Razor 是一种将 HTML 标记与 C# 代码结合的语法,旨在提高开发者的生产力。它允许开发者在同一文件中使用 HTML 标记和 C#。
Blazor 网页应用使用 Razor 组件 构建。Razor 组件是包含 C# 代码、标记和其它 Razor 组件的可重用 UI 元素。Razor 组件可以说是 Blazor 框架的基石。有关 Razor 组件的更多信息,请参阅 第二章,构建您的第一个 Blazor WebAssembly 应用程序。
重要提示
Razor Pages 和 MVC 也使用 Razor 语法。与渲染整个页面的 Razor Pages 和 MVC 不同,Razor 组件只渲染 DOM 变更。区分它们的一种简单方法是,Razor 组件使用 RAZOR 文件扩展名,而 MVC 和 Razor Pages 使用 CSHTML 文件扩展名。
Blazor 框架的名称有一个有趣的起源故事。术语 Blazor 是由单词 browser 和单词 razor 组合而成的。
精彩的工具集
您可以使用 Microsoft Visual Studio 或 Microsoft Visual Studio Code 来开发 Blazor 应用程序。Microsoft Visual Studio 是一个 集成开发环境(IDE),而 Microsoft Visual Studio Code 是一个轻量级但功能强大的编辑器。它们都是构建企业应用的绝佳工具。作为额外的好处,这两个工具都有免费版本。
由微软支持
虽然 Blazor 框架是开源的,但它由微软维护。他们继续对 Blazor 的未来进行大量投资。以下列表包括微软正在积极开发并添加到 Blazor 中的功能:
-
热重载改进
-
提前编译(AOT)编译性能改进
-
认证改进
-
额外的内置组件
-
多线程
使用 Blazor 框架开发 Web 应用程序有许多相关的好处。由于它建立在 .NET 框架之上,它使开发者能够使用他们已经掌握的技能,如 C#,以及他们已经熟悉的工具,如 Visual Studio。此外,由于它是一个 SPA 框架,Blazor Web 应用对用户来说感觉就像原生应用程序。最后,微软正在对 Blazor 的未来进行大量投资。
主机模型
正如我们之前提到的,Razor 组件是 Blazor 应用程序的基本构建块。这些 Razor 组件托管的位置取决于主机模型。
Blazor 有三种不同的主机模型:
-
Blazor Server
-
Blazor Hybrid
-
Blazor WebAssembly
微软发布的第一个主机模型是 Blazor Server。在此主机模型中,Razor 组件在服务器上执行。微软发布的第二个主机模型,也是本书的主题,是 Blazor WebAssembly。在此主机模型中,Razor 组件使用 WebAssembly 在浏览器上执行。最新的主机模型是 Blazor Hybrid。Blazor Hybrid 允许您通过在嵌入的 Web View 控件中托管 Razor 组件来构建原生客户端应用程序。
每种主机模型都有其自身的优缺点。然而,它们都依赖于相同的底层架构。因此,可以独立于主机模型编写和测试代码。
主机模型之间的主要区别在于代码的执行位置、延迟、安全性、有效负载大小和离线支持。所有主机模型共同的一点是它们都能以接近原生速度执行。
Blazor Server
正如我们刚才提到的,Blazor Server 主机模型是微软发布的第一个主机模型。它于 2019 年 9 月作为 .NET Core 3 版本的一部分发布。
以下图示说明了 Blazor Server 主机模型:

图 1.1:Blazor Server
在此主机模型中,Web 应用在服务器上执行,并且只有 UI 更新发送到客户端浏览器。浏览器被视为瘦客户端,所有处理都在服务器上完成。因此,此模型需要与服务器保持持续连接。当使用 Blazor Server 时,UI 更新、事件处理和 JavaScript 调用都是通过 ASP.NET Core SignalR 连接来处理的。
重要提示
SignalR 是一个软件库,允许 Web 服务器向浏览器推送实时通知。Blazor Server 使用它将 UI 更新发送到浏览器。
Blazor Server 的优势
与使用 Blazor WebAssembly 相比,使用 Blazor Server 有一些优势。然而,关键优势是所有操作都在服务器上完成。由于 Web 应用在服务器上运行,它可以访问服务器上的所有内容。因此,安全和数据访问得到了简化。此外,由于所有操作都在服务器上完成,包含 Web 应用代码的程序集(DLL)也保留在服务器上。
使用 Blazor Server 的另一个优势是它可以在不支持 WebAssembly 的瘦客户端和旧浏览器(如 Internet Explorer)上运行。
最后,使用 Blazor Server 的网络应用在首次使用时的初始加载时间可以远小于使用 Blazor WebAssembly 的网络应用,因为需要下载的文件要少得多。
Blazor Server 的缺点
与 Blazor WebAssembly 相比,Blazor Server 托管模型有几个缺点。最大的缺点是浏览器必须保持与服务器的一致连接。由于没有离线支持,每次用户交互都需要网络往返。由于所有这些往返,Blazor Server 网络应用的延迟比 Blazor WebAssembly 网络应用高,可能会感觉响应缓慢。此外,网络中断可能导致客户端意外断开连接。
提示
延迟是 UI 操作与 UI 更新时间之间的时间。
使用 Blazor Server 的另一个缺点是它依赖于 SignalR 进行每个 UI 更新。微软对 SignalR 的支持正在改善,但扩展可能具有挑战性。当打开到服务器的并发连接太多时,连接耗尽可能会阻止其他客户端建立新的连接。
最后,Blazor Server 网络应用必须从 ASP.NET Core 服务器上提供服务。
Blazor Hybrid
Blazor Hybrid 托管模型是微软最近发布的托管模型。它于 2021 年 11 月作为 .NET 6 发布的一部分发布。
下面的图示说明了 Blazor Hybrid 托管模型:

图 1.2:Blazor Hybrid
在此模型中,Razor 组件在嵌入的 Web View 控件中运行。Blazor Hybrid 应用包括 Windows Forms、WPF 和 .NET MAUI 应用。通过使用 Blazor Hybrid 托管模型,您的应用可以完全访问您选择的目标设备的原生功能。
Blazor Hybrid 的优势
与 Blazor WebAssembly 相比,使用此模型的优势在于它不需要 WebAssembly。此外,由于组件的 C# 代码在宿主进程中执行,Blazor Hybrid 应用可以访问设备的原生功能。
Blazor Hybrid 的缺点
使用 Blazor Hybrid 的主要缺点是它们在原生应用中的 Web View 组件中托管。因此,开发者必须知道如何开发他们想要针对的每种类型的原生客户端应用。另一个缺点是它们通常需要一个服务器来交付应用。相比之下,Blazor WebAssembly 应用可以作为一组静态文件下载。
Blazor WebAssembly
Blazor WebAssembly 托管模型是本书的主题。
Blazor WebAssembly 3.2.0 于 2020 年 5 月发布。.NET 5 中的 Blazor WebAssembly 是作为 .NET 5.0 发布的一部分在 2020 年 11 月发布的。ASP.NET Core Blazor 是作为 .NET 6.0 发布的一部分在 2021 年 11 月发布的,它是一个 长期支持(LTS)版本。Blazor WebAssembly 的最新版本是作为 .NET 7 发布的一部分在 2022 年 11 月发布的。本书将使用 .NET 7 中的 Blazor WebAssembly 用于所有项目。
提示
LTS 版本在初始发布后至少由微软支持 3 年。.NET 7 中的 Blazor WebAssembly 是一个当前版本,而不是 LTS 版本。当前版本将获得 18 个月的免费支持和补丁。我们建议,如果你正在使用 Blazor WebAssembly 开发一个新项目,你应该使用最新的版本。
以下图表说明了 Blazor WebAssembly 托管模型:

图 1.3:Blazor WebAssembly
在这种托管模型中,Web 应用在浏览器上执行。为了使 Web 应用和 .NET 运行时在浏览器上运行,浏览器必须支持 WebAssembly。WebAssembly 是所有现代浏览器(包括移动浏览器)支持的网络标准。虽然 Blazor WebAssembly 本身不需要服务器,但 Web 应用可能需要服务器进行数据访问和身份验证。
在过去,在浏览器上运行 C# 代码的唯一方法是使用插件,例如 Silverlight。Silverlight 是微软提供的一个免费浏览器插件。它非常受欢迎,直到苹果决定禁止在 iOS 上使用任何浏览器插件。由于苹果的决定,微软放弃了 Silverlight。
重要提示
Blazor 不依赖于插件或重新编译代码到其他语言。相反,它基于开放的 Web 标准,并受到所有现代浏览器(包括移动浏览器)的支持。
Blazor WebAssembly 的优势
Blazor WebAssembly 有许多优势。首先,由于它在浏览器上运行,它依赖于客户端资源而不是服务器资源。因此,处理工作被卸载到客户端。此外,与 Blazor Server 不同,由于每个 UI 交互都需要往返服务器,因此没有延迟。
Blazor WebAssembly 可以用来创建 渐进式 Web 应用(PWA)。PWA 是看起来和感觉像原生应用的网络应用。它们提供离线功能、后台活动、原生 API 层和推送通知。它们甚至可以列在各种应用商店中。通过将你的 Blazor WebAssembly 应用配置为 PWA,你的应用可以通过单一代码库在任何设备上触达任何人。有关创建 PWA 的更多信息,请参阅 第六章,作为渐进式 Web 应用(PWA)构建天气应用。
最后,Blazor WebAssembly 网页应用程序不依赖于 ASP.NET Core 服务器。实际上,您可以通过内容分发网络(CDN)部署 Blazor WebAssembly 网页应用程序。
Blazor WebAssembly 的缺点
公平地说,使用 Blazor WebAssembly 时存在一些缺点,应该予以考虑。首先,在使用 Blazor WebAssembly 时,.NET 运行时、dotnet.wasm 文件以及您的程序集需要下载到浏览器中才能使您的网页应用程序工作。因此,第一次运行 Blazor WebAssembly 应用程序通常比相同的 Blazor Server 应用程序初始加载时间更长。然而,您可以使用一些策略来加快初始加载时间,例如将一些程序集的加载推迟到需要时。此外,这仅是初始加载时的问题,因为应用程序的后续运行将访问来自本地缓存中的文件。
Blazor WebAssembly 网页应用程序的另一个缺点是它们的强大程度取决于运行的浏览器。因此,不支持瘦客户端。Blazor WebAssembly 只能在支持 WebAssembly 的浏览器上运行。幸运的是,由于世界 Wide Web Consortium(W3C)与苹果、谷歌、微软和 Mozilla 的工程师之间的大量协调,所有现代浏览器都支持 WebAssembly。
托管模型差异
下表显示了三种模型之间的差异:
| Blazor WebAssembly | Blazor Hybrid | Blazor Server | |
|---|---|---|---|
| 原生执行速度 | X | X | X |
| 在客户端执行 | X | X | |
| 在服务器上执行 | X | ||
| 初始加载后低延迟 | X | X | |
| 快速初始加载时间 | X | ||
| 离线支持 | X | X | |
| 不需要服务器 | X | ||
| 需要持续连接到服务器 | X | ||
| 可构建 PWA | X | ||
| 将程序集发送到客户端 | X | X | |
| 程序集保留在服务器上 | X | ||
| 可访问原生客户端功能 | X | ||
| 需要 WebAssembly | X | ||
| 需要 SignalR | X | ||
| 可在瘦客户端上运行 | X |
表 1.1:托管模型差异
Blazor 框架提供了三种不同的托管模型,分别是 Blazor Server、Blazor Hybrid 和 Blazor WebAssembly。Blazor Server 网页应用程序在服务器上运行,并使用 SignalR 将 HTML 传输到浏览器。Blazor Hybrid 网页应用程序在原生应用程序的 Web View 控件中运行。Blazor WebAssembly 网页应用程序直接在浏览器中使用 WebAssembly 运行。它们各自都有其优缺点。然而,如果您想创建交互性强、响应速度快、类似原生的离线可用的网页应用程序,我们推荐使用 Blazor WebAssembly。接下来,我们将学习更多关于 WebAssembly 的知识。
什么是 WebAssembly?
WebAssembly 是一种二进制指令格式,它允许用高级语言(如 C#)编写的代码以接近原生的速度在浏览器上运行。要在网页浏览器中运行 .NET 二进制文件,它使用的是编译为 WebAssembly 的 .NET 运行时版本。你可以将其视为在浏览器中执行原生编译的代码。
WebAssembly 是由 W3C 社区组开发的开放标准。它最初于 2015 年宣布,第一个支持它的浏览器于 2017 年发布。
WebAssembly 目标
当 WebAssembly 最初开发时,该项目有四个主要设计目标。以下是 WebAssembly 的原始目标列表:
-
快速且高效
-
安全
-
开放
-
不要破坏网络
WebAssembly 快速且高效。它旨在允许开发者用任何语言编写代码,然后编译为在浏览器中运行。由于代码是编译的,因此它运行速度快,性能接近原生速度。
WebAssembly 是安全的。它不允许直接与浏览器的 DOM 交互。相反,它在自己的内存安全、沙箱执行环境中运行。您必须使用 JavaScript 互操作来与 DOM 交互。第五章中的项目,使用 JavaScript 互操作构建本地存储服务(JS 互操作),将教会您如何使用 JavaScript 互操作。
WebAssembly 是开放的。尽管它是一种低级汇编语言,但可以手动编辑和调试。
WebAssembly 没有破坏网络。它是一种旨在与其他网络技术协同工作的网络标准。此外,WebAssembly 模块可以访问与 JavaScript 可访问的相同 Web API。
总体而言,WebAssembly 能够实现所有原始目标,并且迅速获得了所有现代浏览器的支持。
WebAssembly 支持
如前所述,WebAssembly 在所有现代浏览器上运行,包括移动浏览器。从下表可以看出,所有最流行浏览器的当前版本都与 WebAssembly 兼容:
| 浏览器 | 版本 |
|---|---|
| 微软 Edge | 当前 |
| Mozilla Firefox,包括 Android | 当前 |
| Google Chrome,包括 Android | 当前 |
| Safari,包括 iOS | 当前 |
| Opera,包括 Android | 当前 |
| 微软 Internet Explorer | 不支持 |
表 1.2:WebAssembly 浏览器兼容性
重要提示
自 2022 年 6 月 15 日起,微软不再支持微软 Internet Explorer。它不支持 WebAssembly,并且永远不会支持 WebAssembly。
WebAssembly 是一种网络标准,允许开发者在浏览器中运行用任何语言编写的代码。它被所有现代浏览器支持。
现在我们已经讨论了使用 Blazor 框架的好处,并比较了各种托管模型,现在是时候开始使用 Blazor WebAssembly 框架进行开发了。然而,在我们开始之前,您需要设置您的电脑。
设置您的电脑
对于本书中的项目,我们使用 Microsoft Visual Studio Community 2022、.NET 7、Microsoft SQL Server 2022 Express Edition 和 Microsoft Azure。
所有项目都是使用 Microsoft Visual Studio Community 2022 (64 位) – 当前版本 17.4.2 以及 ASP.NET 和 Web 开发工作负载构建的。如果您需要安装 Microsoft Visual Studio Community 2022,请遵循本章后面 安装 Microsoft Visual Studio Community Edition 部分的说明。
提示
尽管我们使用的是 Microsoft Visual Studio Community 2022,但本书中的任何版本的 Microsoft Visual Studio 2022 都可以用来完成项目。Microsoft Visual Studio Code 也可以使用。然而,所有截图均来自 Microsoft Visual Studio Community 2022。
.NET 7 中的 Blazor WebAssembly 需要 .NET 7.0。要确定计算机上运行的 .NET 版本,请打开 命令提示符 并输入以下命令:
dotnet –-version
如果您的计算机未运行 .NET 7.0 或更高版本,请遵循本章后面 安装 .NET 7.0 部分的说明。
第三章 和 第十章 使用 Microsoft Azure。第三章 使用 Microsoft Azure 发布 Blazor WebAssembly 应用程序,而 第十章 使用 Microsoft Azure Active Directory 保护 Blazor WebAssembly 应用程序。
本书最后两个项目使用 Microsoft SQL Server 2022 Express Edition 作为后端数据库。如果您需要安装 Microsoft SQL Server Express Edition,请遵循本章后面 安装 Microsoft SQL Server Express 部分的说明。
提示
尽管我们使用的是 Microsoft SQL Server 2022 Express Edition,但任何年份或版本的 SQL Server 都可以用来完成本书中的项目。
安装 Microsoft Visual Studio Community Edition
Microsoft Visual Studio Community Edition 是 Microsoft Visual Studio 的免费版。要安装 Microsoft Visual Studio Community Edition,请执行以下步骤:
- 从
visualstudio.microsoft.com下载 Visual Studio 安装程序。

图 1.4:下载 Visual Studio 选择器
-
下载完成后,运行安装程序以完成安装。
-
在安装过程的第一个步骤中,Visual Studio 安装程序将检查系统中现有的 Visual Studio 版本。一旦安装程序完成对已安装版本的检查,它将打开以下安装对话框:

图 1.5:Visual Studio 安装程序
- 选择 ASP.NET 和 Web 开发 工作负载,然后点击 安装 按钮完成安装。
安装 .NET 7.0
要安装 .NET 7.0,请执行以下步骤:
-
从
dotnet.microsoft.com/download/dotnet/7.0下载 .NET 7.0 安装程序。我们使用的是 Windows x64 SDK 安装程序。 -
下载完成后,运行安装程序以完成 .NET 7.0 在您计算机上的安装。
-
打开命令提示符并输入以下命令以验证您的计算机现在正在运行 .NET 7.0:
dotnet –-version以下截图来自运行 .NET 7.0 的计算机:
![图形用户界面,文本,应用程序,描述自动生成]()
图 1.6: .NET 版本
安装 Microsoft SQL Server Express
Microsoft SQL Server Express 是 Microsoft SQL Server 的免费版。要安装 Microsoft SQL Server Express,请执行以下操作:
-
从
www.microsoft.com/en-us/sql-server/sql-server-downloads下载 SQL Server Express 的 Microsoft SQL Server 安装程序。 -
下载完成后,运行 SQL Server 安装程序。
-
选择基本安装类型:

图 1.7: SQL Server 安装程序
-
点击接受按钮以接受 Microsoft SQL Server 许可条款。
-
点击安装按钮完成安装。
以下截图显示了 SQL Server Express 安装成功后出现的对话框:
![图片]()
图 1.8: SQL Server Express 版本
创建 Microsoft Azure 账户
Microsoft Azure 是微软的云平台,它提供超过 200 个产品和云服务。您可以使用它来运行和管理您选择工具和框架的应用程序。
如果您还没有 Microsoft Azure 账户,您可以创建一个免费账户。每个免费账户都附带 200 美元的信用额度,以帮助您开始使用,并提供超过 55+ 的免费服务。

图 1.9: Microsoft Azure
要创建一个免费的 Microsoft Azure 账户,请执行以下操作:
-
导航到 Microsoft Azure 页面,
azure.microsoft.com/。 -
点击免费账户按钮。
-
点击开始免费按钮。
-
完成协议并点击注册按钮。
要完成本书中的所有项目,您需要一个代码编辑器,例如 Microsoft Visual Studio Community 2022、.NET 7.0、Microsoft Azure 和 Microsoft SQL Server。在本章中,我们向您展示了如何安装 Visual Studio 2022 社区版、.NET 7.0 和 SQL Server 2022 Express 版本。我们还向您展示了如何开通免费的 Microsoft Azure 账户。
摘要
完成本章后,您应该了解使用 Blazor WebAssembly 相比其他 Blazor 托管模型的好处,并准备好完成本书中的项目。
在本章中,我们介绍了 Blazor 框架。Blazor 框架建立在 .NET Framework 之上,允许网络开发者在网络应用程序的客户机和服务器端使用 C#。
之后,我们比较了 Blazor WebAssembly 与 Blazor Server 和 Blazor Hybrid。所有三种托管模型都用于托管 Razor 组件。它们各自都有其优势和劣势。我们更喜欢 Blazor WebAssembly。
在本章的最后部分,我们解释了如何使用 Microsoft Visual Studio Community Edition、.NET 7.0 和 Microsoft SQL Server Express 设置您的计算机,以及如何打开 Microsoft Azure 账户,所有这些都是在完成本书中的项目所必需的。
现在您的计算机已设置好以完成本书中的项目,是时候开始行动了。在下一章中,您将创建您的第一个 Blazor WebAssembly 网络应用程序。
问题
以下问题供您思考:
-
以下哪种托管模型需要持续连接到服务器:Blazor WebAssembly、Blazor Server 或 Blazor Hybrid?
-
使用 Blazor WebAssembly 是否意味着你永远不再需要编写 JavaScript?
-
Blazor WebAssembly 是否需要在浏览器上安装任何插件?
-
使用 Blazor WebAssembly 开发需要多少钱?
进一步阅读
以下资源提供了关于本章主题的更多信息:
-
关于 Blazor 的更多信息,请参阅
blazor.net。 -
关于 .NET 基金会的更多信息,请参阅
dotnetfoundation.org。 -
关于 GitHub 上的 ASP.NET 仓库的更多信息,请参阅
github.com/dotnet/aspnetcore。 -
关于 WebAssembly 的一般信息,请参阅
webassembly.org。 -
关于 WebAssembly 与浏览器的兼容性信息,请参阅
caniuse.com/?search=wasm。 -
关于 Razor Pages 的更多信息,请参阅
learn.microsoft.com/en-us/aspnet/core/razor-pages。
加入我们的 Discord 社区
加入我们的 Discord 社区空间,与作者和其他读者进行讨论:

第二章:构建您的第一个 Blazor WebAssembly 应用程序
Razor 组件是 Blazor WebAssembly 应用程序的构建块。Razor 组件是一块用户界面,它可以被共享、嵌套和重用。Razor 组件是普通的 C#类,可以放置在项目的任何位置。
Razor 组件。
Blazor 应用程序的构建块。
可嵌套的魔法。
在本章中,我们将学习关于 Razor 组件的内容。我们将学习如何使用它们,如何应用参数,以及如何创建它们。我们还将熟悉它们的生命周期和结构。我们将学习如何使用@page指令来定义路由,以及如何使用Razor 语法将 C#代码与 HTML 标记组合。最后,我们将介绍热重载体验。
本章中的 Blazor WebAssembly 项目将通过使用 Microsoft 提供的Blazor WebAssembly App 项目模板来创建。创建项目后,我们将检查它以进一步熟悉 Razor 组件。我们将学习如何使用它们,如何添加参数,如何应用路由,如何使用 Razor 语法,以及如何将 Razor 标记和代码分离到不同的文件中。在编辑代码时,我们将使用Hot Reload来自动更新浏览器。
在本章中,我们将涵盖以下主题:
-
Razor 组件
-
路由
-
Razor 语法
-
热重载
-
创建 demo WebAssembly 项目
创建 Demo Blazor WebAssembly 项目的技术要求
要完成此项目,您需要在您的 PC 上安装 Microsoft Visual Studio 2022。有关如何安装 Microsoft Visual Studio 2022 免费社区版的说明,请参阅第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter02。
代码在行动视频在此处可用:packt.link/Ch2。
Razor 组件
Blazor WebAssembly 是一个以组件为驱动的框架。Razor 组件是 Blazor WebAssembly 应用程序的基本构建块。它们是使用 C#、HTML 和 Razor 语法组合实现的类。当 Web 应用程序加载时,这些类会以正常的.NET 程序集(DLLs)的形式下载到浏览器中。
重要提示
在这本书中,术语“Razor 组件”和“组件”是可互换使用的。
使用组件
使用 HTML 元素语法将一个组件添加到另一个组件中。标记看起来像一个 HTML 标签,其中标签的名称是组件类型。
在本章稍后我们将创建的Demo项目的Pages/Index.razor文件中的以下标记将渲染一个SurveyPrompt实例:
<SurveyPrompt Title="How is Blazor working for you?" />
前面的SurveyPrompt元素包含一个名为Title的属性参数。
参数
组件参数用于使组件动态化。参数是组件的公共属性,用 Parameter 属性或 CascadingParameter 属性装饰。参数可以是简单类型、复杂类型、函数、RenderFragments 或事件回调。
以下名为 Hello 的组件的代码包括一个名为 Text 的参数:
Hello.razor
<h1>Hello @Text!</h1>
@code {
[Parameter] public string? Text { get; set; }
}
要使用 Hello 组件,请在另一个组件中包含以下 HTML 语法:
<Hello Text="World" />
在前面的示例中,Hello 组件的 Text 属性是 Text 参数的来源。此截图显示了使用组件的结果:

图 2.1:Hello 组件
参数的 get 和 set 访问器不得包含自定义逻辑。它们仅作为从父组件流向子组件的信息通道。此外,如前所述,它们必须是公共的。
重要提示
如果子组件包含导致父组件重新渲染的参数,应用程序将进入无限循环。
必需参数
您可以通过使用 EditorRequired 属性来装饰参数,指定编辑器需要该参数。在 Hello2 组件的以下版本中,Text 参数是必需的:
Hello2.razor
<h1>Hello @Text!</h1>
@code {
[Parameter]
[EditorRequired]
public string? Text { get; set; }
}
如果我们尝试在一个组件中使用 Hello2 并不包括 Text 属性,Visual Studio 将显示以下警告:

图 2.2:缺少参数警告
前面的警告不会阻止应用程序构建,并且在运行时也不强制执行。它仅由编辑器使用。
重要提示
使用 EditorRequired 属性装饰参数不能保证在运行时参数将有值。
查询字符串
组件还可以从查询字符串接收参数。查询字符串用于将值分配给指定的参数。为了指示参数可以来自查询字符串,我们用 SupplyParameterFromQuery 属性装饰参数。
在以下示例中,Increment 参数已被 SupplyParameterFromQuery 属性装饰:
[Parameter]
[SupplyParameterFromQuery]
public int? Increment { get; set; }
这是将 Increment 的值设置为 5 的代码:
localhost:7097/counter?increment=5
在前面的示例中,问号之后的所有内容都是查询字符串。查询字符串不区分大小写。此外,前面的示例假设我们在本地端口 7097 上运行我们的应用程序。由于所使用的端口会因应用程序而异,因此我们将在其余示例中省略端口。
提示
applicationUrl 在 Properties/launchSettings.json 文件中定义。每次我们使用 Microsoft 项目模板创建新的 Blazor 项目时,applicationUrl 将随机引用不同的端口。
查询字符串提供的参数限于以下类型、以下类型的数组及其可空变体:
-
bool -
DateTime -
decimal -
double -
float -
Guid -
int -
long -
string
它们也可以由前面类型组成的数组。
组件命名
Razor 组件的名称必须使用标题格式。因此,hello 不是一个有效的 Razor 组件名称,因为 h 没有被大写。此外,Razor 组件使用 RAZOR 扩展名,而不是 Razor Pages 使用的 CSHTML 扩展名。
重要提示
Razor 组件必须以大写字母开头。
组件生命周期
Razor 组件继承自 ComponentBase 类。ComponentBase 类包含异步和同步方法,用于管理组件的生命周期。在本书中,我们将使用方法的异步版本,因为它们执行时不会阻塞其他操作。组件生命周期中方法的调用顺序如下:
-
SetParametersAsync:此方法设置组件父级在渲染树中提供的参数。
-
OnInitializedAsync:此方法在参数设置完成后,组件成功初始化后调用。
-
OnParametersSetAsync:此方法在组件初始化后以及每次组件重新渲染时调用。当父组件重新渲染且至少有一个参数已更改时,组件将重新渲染。此外,当调用组件的 StateHasChanged 方法时,组件也会重新渲染。
-
OnAfterRenderAsync:此方法在组件完成渲染后调用。此方法用于与 JavaScript 一起使用,因为 JavaScript 需要在执行任何工作之前渲染 Document Object Model (DOM) 元素。
组件结构
下面的图示展示了我们将在此章节中创建的 Demo 项目的 Counter 组件的代码:

图 2.3:组件结构
上述示例中的代码分为三个部分:
-
指令
-
标记
-
代码块
每个部分都有不同的用途。
指令
指令用于添加特殊功能,例如路由、布局和依赖注入。文件级指令在 Razor 中定义,并且不能定义自己的指令。Razor 指令以 @ 符号开头。
在上述示例中,只使用了单个指令——@page 指令。@page 指令用于路由。在此示例中,以下 URL 将将用户路由到 Counter 组件:
/counter
一个典型的页面可以在页面的顶部包含许多指令。此外,许多页面有多个 @page 指令。
大多数 Razor 指令都可以在 Blazor WebAssembly 应用程序中使用。以下是按字母顺序排列的 Blazor 中使用的 Razor 指令:
-
@attribute: 这个指令向组件添加类级属性。以下示例添加了[Authorize]属性:@attribute [Authorize] -
@code: 这个指令向组件添加类成员。在示例中,它用于区分代码块。 -
@implements: 这个指令为指定的类实现接口。 -
@inherits: 这个指令提供了对视图继承的类的完全控制。 -
@inject: 这个指令用于依赖注入。它使组件能够从依赖注入容器中注入服务到视图中。以下示例将定义在Program.cs文件中的 HttpClient 注入到组件中:@inject HttpClient Http -
@layout: 这个指令用于指定包含@page指令的 Razor 组件的布局。 -
@namespace: 这个指令设置组件的命名空间。只有当你不想使用组件的默认命名空间时,才需要使用此指令。默认命名空间基于组件的位置。 -
@page: 这个指令用于路由。 -
@preservewhitespace: 这个指令用于保留渲染的标记中的空白。如果设置为true,则保留空白。默认值为false。 -
@using: 这个指令控制作用域内的组件。
标记
标记是带有 Razor 语法标记的 HTML。Razor 语法可以用来渲染文本,并允许 C# 作为标记的一部分被包含。我们将在本章后面更详细地介绍 Razor 语法。
代码块
代码块包含页面的逻辑。它以 @code 指令开始。按照惯例,代码块位于页面底部。它是唯一一个不在页面顶部放置的文件级指令。
代码块是我们向组件添加 C# 字段、属性和方法的地方。在本章的后面部分,我们将把代码块移动到一个单独的后台代码文件中。
Razor 组件是 Blazor WebAssembly 应用程序的基本构建块。由于它们仅仅是 HTML 标记和 C# 代码的组合,因此它们易于使用。它们通过指令、标记和代码块进行结构化。组件具有明确的生命周期。它们可以嵌套并利用不同类型的参数来使它们动态化。在下一节中,我们将解释如何使用路由在组件之间进行导航。
路由
在 Blazor WebAssembly 中,路由在客户端处理,而不是在服务器端。当你通过浏览器导航时,Blazor 会拦截该导航并渲染与匹配路由的组件。
URL 是相对于在 wwwroot/index.html 文件中指定的基本路径解析的。基本路径使用以下语法在 head 元素中指定:
<base href="/" />
与您可能使用过的其他框架不同,路由不是从其文件位置推断出来的。例如,在 Demo 项目中,Counter 组件位于 /Pages/Counter 文件夹中,但它使用以下路由:
/counter
这是 Counter 组件使用的 @page 指令:
@page "/counter"
路由参数
路由参数可以用来填充组件的参数。组件和路由的参数必须具有相同的名称,但它们不区分大小写。
您可以为组件提供多个 @page 指令。以下 RoutingExample 组件演示了如何包含多个 @page 参数:
RoutingExample.razor
@page "/routing"
@page "/routing/{text}"
<h1>Blazor WebAssembly is @Text!</h1>
@code {
[Parameter] public string? Text { get; set; }
protected override void OnInitialized()
{
Text = Text ?? "fantastic";
}
}
在前面的代码中,第一个 @page 指令允许在无参数的情况下导航到组件,而第二个 @page 指令包含一个路由参数。如果提供了 text 的值,它将被分配给组件的 Text 属性。如果组件的 Text 属性为 null,则将其设置为 fantastic。
以下 URL 将将用户路由到 RoutingExample 组件:
/routing
以下 URL 也将将用户路由到 RoutingExample 组件,但这次 Text 参数将由路由设置:
/routing/amazing
此截图显示了使用指示路由的结果:

图 2.4:RoutingExample 组件
重要提示
路由参数不区分大小写。
可选路由参数
Blazor 支持可选路由参数。在以下 RoutingExample 组件版本中,Text 属性是可选的:
RoutingExample.razor
@page "/routing/{text?}"
<h1>Blazor WebAssembly is @Text!</h1>
@code {
[Parameter] public string? Text { get; set; }
protected override void OnInitialized()
{
Text = Text ?? "fantastic";
}
}
RoutingExample 组件的这个版本与原始版本的区别在于,两个 @page 指令已被合并,并且 text 路由参数已被更改为可空类型。通过使用可选路由参数,我们可以减少应用程序所需的 @page 指令数量。
提示
如果需要组件使用不同的可选参数值导航到自身,您应该在 OnParametersSet 事件中设置该值,而不是在 OnInitialized 事件中。
通配符路由参数
通配符路由参数用于捕获跨越多个文件夹边界的路径。此类路由参数是 string 类型,并且只能放置在 URL 的末尾。通配符路由参数用星号表示。
这是一个使用通配符路由参数的示例组件:
CatchAll.razor
@page "/error/{*path}"
@page "/warning/{*path}"
<h1>Catch All</h1>
Route: @Path
@code {
[Parameter] public string? Path { get; set; }
}
对于 /error/type/3 URL,前面的代码将设置 Path 参数的值为 type/3,如下所示图所示:

图 2.5:通配符路由参数示例
路由约束
路由约束用于强制路由参数的数据类型。要定义约束,请在参数后添加冒号和约束类型。在以下示例中,路由期望一个名为Increment且类型为int的路由参数:
@page "/counter/{increment:int}"
对于路由约束,以下类型及其可空变体都受到支持:
-
bool -
datetime -
decimal -
double -
float -
guid -
int -
long重要提示
路由约束使用不变文化,并且不支持本地化。例如,日期仅以 MM-dd-yyyy 或 yyyy-MM-dd 的形式有效,布尔值必须是
true或false。
以下类型目前不支持作为约束:
-
正则表达式
-
枚举
-
自定义约束
路由约束支持可选参数。在以下示例中,名为increment且类型为int的路由参数是可选的:
@page "/counter/{increment:int?}"
路由在客户端处理。每个可路由组件可以包含一个或多个路由。我们可以使用路由参数和通配符路由参数来定义路由。路由约束用于确保路由参数是所需的数据类型。Razor 组件使用 Razor 语法将 HTML 与 C#代码无缝合并,这是我们将在下一节中看到的。
Razor 语法
Razor 语法由 HTML、Razor 标记和 C#组成。从 Razor 组件渲染 HTML 与从 HTML 文件渲染 HTML 相同。Razor 语法使用内联表达式和控制结构来渲染动态值。
内联表达式
内联表达式以@符号开头,后跟变量或函数名。这是一个内联表达式的示例:
<h1>Blazor is @Text!</h1>
在前面的示例中,Blazor 将@符号后面的文本解释为属性名或方法名。
控制结构
控制结构也以@符号开头。花括号内的内容将被评估并渲染到输出中。这是一个来自我们将在本章后面创建的Demo项目中的FetchData组件的if语句示例:
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
条件语句
Razor 语法包括以下类型的条件语句:
-
if语句 -
switch语句
这是一个if语句的示例:
@if (DateTime.Now.DayOfWeek.ToString() != "Friday")
{
<p>Today is not Friday.</p>
}
else if (DateTime.Now.Day != 13)
{
<p>Today is not the 13th.</p>
}
else
{
<p>Today is Friday the 13th.</p>
}
前面的代码使用if语句检查当前星期几是否为星期五以及/或当前月份是否为 13 日。结果渲染相应的p元素。
这是一个switch语句的示例:
@switch (value)
{
case 1:
<p>The value is 1!</p>
break;
case 42:
<p>Your number is 42!</p>
break;
default:
<p>Your number was not 1 or 42.</p>
break;
}
@code {
private int value = 2;
}
前面的switch语句将value变量与1和42进行比较。结果渲染相应的p元素。
循环
Razor 语法提供了以下类型的循环:
-
for循环
-
foreach循环
-
while循环
-
do while循环
下面的每个示例都循环遍历 WeatherForecast 项的数组,以显示数组中每个项的 Summary 属性。
这是一个 for 循环的示例:
@for (var i = 0; i < forecasts.Count(); i++)
{
<div>@forecasts[i].Summary</div>
}
@code {
private WeatherForecast[] forecasts;
}
这是一个 foreach 循环的示例:
@foreach (var forecast in forecasts)
{
<div>@forecast.Summary</div>
}
@code {
private WeatherForecast[] forecasts;
}
这是一个 while 循环的示例:
@while (i < forecasts.Count())
{
<div>@forecasts[i].Summary</div>
i++;
}
@code {
private WeatherForecast[] forecasts;
private int i = 0;
}
这是一个 do while 循环的示例:
@do
{
<div>@forecasts[i].Summary</div>
i++;
} while (i < forecasts.Count());
@code {
private WeatherForecast[] forecasts;
private int i = 0;
}
所有的前述循环示例都渲染相同的输出。使用 Razor 语法循环遍历集合有许多方法。
如果你已经了解 C#,Razor 语法很容易学习。它包括内联表达式和控制结构,如条件语句和循环。通过使用 Hot Reload,我们可以在浏览器中立即编辑代码并查看结果。
热重载
Hot Reload 允许开发者在无需重新构建或刷新应用程序的情况下编辑正在运行的应用程序的标记和 C# 代码。同时,它还保持应用程序的状态。
你可以使用或不需要调试器来使用 Hot Reload。要触发 Hot Reload,你可以使用工具栏上的 热重载 下拉按钮或按 Alt+F10。
这是从工具栏访问的 热重载 下拉按钮:

图 2.6:热重载下拉按钮
如你所见,从 热重载 下拉按钮中,你可以设置 热重载 在你保存文件时自动触发。通过菜单上的 设置 选项还有更多设置可用。热重载 支持对组件的大部分更改,包括样式表。但是,有时更改可能需要重启应用程序。
这是一个需要重启的一些活动的列表:
-
添加新的局部函数
-
添加新的 lambda 表达式
-
添加新的字段
-
更改参数的名称
-
添加 await 操作符
如果需要重启,将显示以下对话框:

图 2.7:热重载警告对话框
如果你勾选了 当无法应用更新时始终重新构建 复选框,Visual Studio 将在 Hot Reload 无法自动应用更改时自动重新构建和重新加载应用程序。此外,此对话框将不再显示,直到在 设置 中更改此设置或关闭解决方案。
重要提示
如果启用了原生代码调试,则 Hot Reload 将无法工作。此外,你可以在项目的 Properties/launchSettings.json 文件中将 hotReloadEnabled 设置为 false 来在项目级别禁用它。
热重载使你更有效率,因为你不需要每次更新时都停止并重新启动你的应用程序。
创建演示 Blazor WebAssembly 项目
本章将要构建的 Blazor WebAssembly 应用程序是一个简单的三页应用程序。每一页都将用于演示 Razor 组件的一个或多个功能。
这是我们完成的Demo项目的截图:

图 2.8:Demo项目的首页
此项目的构建时间大约为 60 分钟。
项目概述
我们正在创建的Demo项目是基于Blazor WebAssembly App项目模板提供的示例项目之一。在用模板创建项目后,我们将检查示例项目中的文件,并更新一些文件以展示如何使用 Razor 组件。为了提升开发体验,我们将启用热重载。最后,我们将一个组件的代码块分离到一个单独的文件中,以展示如何使用代码后技术将标记与代码分离。
开始使用项目
Visual Studio 附带了许多项目模板。我们将使用Blazor WebAssembly App项目模板来创建我们的第一个 Blazor WebAssembly 项目。由于此项目模板可以用来创建许多不同类型的 Blazor 项目,因此必须精确遵循这些说明:
-
打开 Microsoft Visual Studio 2022。
-
点击创建新项目按钮。
-
在搜索模板(Alt+S)文本框中输入
Blazor并按Enter键。以下截图显示了我们将要使用的Blazor WebAssembly App项目模板:
![图形用户界面,文本,应用程序,聊天或文本消息 自动生成的描述]()
图 2.9:Blazor WebAssembly App 项目模板
-
选择Blazor WebAssembly App项目模板并点击下一步按钮。
-
在项目名称文本框中输入
Demo并点击下一步按钮。这是配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 2.10:配置新项目的对话框
提示
在前面的示例中,我们将
Demo项目放置在E:\Blazor文件夹中。然而,这个项目的位置并不重要。 -
将框架版本选择为.NET 7.0。
-
将身份验证类型选择为无。
-
勾选配置为 HTTPS复选框。
-
取消勾选ASP.NET Core 承载复选框。
-
取消勾选渐进式 Web 应用程序复选框。
-
勾选不使用顶级语句复选框。
这是创建我们新的 Blazor WebAssembly 应用程序时使用的对话框截图:
图 2.11:Blazor WebAssembly App 对话框的附加信息
-
点击创建按钮。
您已创建
Demo项目。
运行Demo项目
一旦项目创建完成,你需要运行它来了解它所做的工作。Demo 项目包含三个页面:首页、计数器和获取数据:
-
从调试菜单中选择不调试启动(Ctrl+F5)选项来运行
Demo项目。重要提示
如果这是你的第一个 Web 应用程序,Visual Studio 将要求设置 Web 应用的证书。你应该信任该证书。
这是
Demo项目的 首页 的截图:![图 2.10 – 首页]()
图 2.12:首页
首页分为两个部分。导航菜单位于页面左侧,主体位于页面右侧。首页的主体包含一些静态文本和一个指向调查的链接。
-
点击导航菜单上的 计数器 选项,导航到 计数器 页面。
这是
Demo项目的 计数器 页面的截图:![图形用户界面,文本,应用程序,聊天或文本消息 描述自动生成]()
图 2.13:计数器页面
计数器 页面的主体包含 当前计数 和一个 点击我 按钮。每次点击 计数器 页面上的按钮,当前计数 就会增加。
重要提示
由于
Demo项目是一个单页应用程序(SPA),只有页面变化的部分会被更新。 -
点击导航菜单上的 获取数据 选项,导航到 获取数据 页面。
这是
Demo项目的 获取数据 页面的截图:![图形用户界面 描述自动生成,置信度中等]()
图 2.14:获取数据页面
获取数据 页面的主体包含一个表格,显示 2022 年 1 月第二周的虚构天气预报。正如你将看到的,表格中显示的数据只是来自
wwwroot\sample-data\weather.json文件的静态数据。
检查 Demo 项目的结构
现在让我们回到 Visual Studio 来检查 Demo 项目中的文件。
下图显示了项目的文件结构:

图 2.15:Demo 项目的文件结构
项目包含相当多的文件,其中一些被分到了自己的文件夹中。让我们来检查它们。
属性文件夹
Properties 文件夹包含 launchSettings.json 文件。此文件包含为每个配置文件定义的各种设置。如本章前面所述,applicationUrl 在此文件中定义。此外,可以通过将 hotReloadEnabled 设置为 false 来禁用 Hot Reload。
提示
launchSettings.json 文件中的设置仅适用于你的本地开发机器。
wwwroot 文件夹
wwwroot 文件夹是应用程序的 Web 根目录。只有此文件夹中的文件是可通过 Web 地址访问的。wwwroot 文件夹包含一系列 层叠样式表(CSS)、一个示例数据文件、图标文件、字体和 index.html 文件。在本书的后续部分,除了这些类型的文件外,我们还将使用此文件夹来存储公共静态资源,如图片和 JavaScript 文件。
index.html 文件是 Web 应用的根页面。每当请求页面时,index.html 页面的内容都会在响应中渲染并返回。index.html 文件的 head 元素包含对 css 文件夹中每个 CSS 文件的链接,并指定用于 Web 应用的基本路径。index.html 文件的 body 元素包含两个 div 元素和对 blazor.webassembly.js 文件的引用。
这是 index.html 文件 head 元素中的代码:
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width,
initial-scale=1.0,
maximum-scale=1.0,
user-scalable=no" />
<title>Demo</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css"
rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="Demo.styles.css" rel="stylesheet" />
</head>
base 元素用于指示使用 @path 指令表示的 URL 的基本路径。在 Demo 项目中,href 属性指向应用程序的根目录。index.html 文件中需要 base 元素。
index.html 引用了三个不同的样式表。bootstrap.min.css 文件用于 Bootstrap 5.1,位于 /css/bootstrap 文件夹中。app.css 文件位于 /css 文件夹中,它包含应用于 Demo 项目的全局样式。最后,Demo.styles.css 文件用于将定义在组件级别的任何 CSS 文件捆绑到一个文件中。这样做是为了实现 CSS 隔离。捆绑的 CSS 文件在构建时在 obj 文件夹中创建。
提示
Demo 项目的 Demo.styles.css 的副本位于 …\Demo\Demo\obj\Debug\net7.0\scopedcss\bundle。
这是 index.html 文件 body 元素中的代码:
<body>
<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 id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">X</a>
</div>
<script src="img/blazor.webassembly.js"></script>
</body>
在前面的代码中,高亮的 div 元素加载了 App 组件。如您所见,App 组件包含一个加载进度指示器。由于 Demo 项目非常简单,您在运行应用程序时可能没有注意到它,因为它加载得非常快。这是加载进度指示器的图片:

图 2.16:加载进度指示器
您可以通过更新高亮的 div 为以下内容来移除加载进度指示器:
<div id="app">
</div>
此外,您还可以通过更新 \css\app.css 文件中的相关样式来自定义加载进度指示器的外观和感觉。
blazor-error-ui div 元素用于显示未处理的异常。此 div 元素的样式也位于 \css\app.css 文件中。blazor.webassembly.js 文件是下载 .NET 运行时、您的应用程序的组件和依赖项的脚本。它还初始化运行时以运行 Web 应用程序。
App 组件
App 组件在 App.razor 文件中定义:
App.razor
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
<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>
</Router>
App 组件是 Blazor WebAssembly 应用程序的根组件。它使用 Router 组件来设置 web 应用的路由。在前面代码中,如果找到路由,RouteView 组件接收 RouteData 并使用指定的 DefaultLayout 渲染指定的组件。如果未找到路由,则使用 NotFound 模板,并使用指定的 Layout 渲染 LayoutView。
如您所见,在 Demo 项目中,Found 模板和 NotFound 模板都使用了相同的布局。它们都使用了 MainLayout 组件。然而,它们不需要使用相同的布局组件。我们将在本章后面检查 MainLayout 组件。
Found 模板包含一个 FocusOnNavigate 组件。它包含两个属性:
-
RouteData– 来自Router组件的路由数据 -
Selector– 当导航完成时应该获得焦点的元素的 CSS 选择器
在前面的代码中,当 Router 导航到新页面时,焦点将放在该页面的第一个 h1 元素上。
共享文件夹
Demo 项目的 Shared 文件夹包含共享的 Razor 组件,包括 MainLayout 组件。这些组件中的每一个都可能被其他 Razor 组件使用一次或多次。Shared 文件夹中的所有组件都不包含 @page 指令,因为它们不是可路由的。
页面文件夹
Pages 文件夹包含项目使用的可路由 Razor 组件。可路由组件是 Counter、FetchData 和 Index。这些组件中的每一个都包含一个 @page 指令,用于将用户路由到页面。
客户端文件夹
Client 文件夹包含 Program.cs 文件。Program.cs 文件是应用程序的入口点。它包含名为 Main 的方法:
public static async Task Main(string[] args)
{
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();
}
在前面的方法中,构建并运行了 WebAssemblyHost。作为该过程的一部分,App 组件被定义为 RootComponent 并放置在 wwwroot/index.html 文件的 app 对象中。此外,当 HttpClient 在依赖注入服务中注册时,配置了 HttpClient 的基本地址。可以使用 @inject 指令将注册的服务注入到组件中。有关依赖注入的更多信息,请参阅 第七章,使用 AppState 构建购物车。
提示
HttpClient 允许应用程序发送 HTTP 请求并接收 HTTP 响应。
_Imports.razor 文件
_Imports.razor 文件包含多个 Razor 组件共享的常见 Razor 指令。通过将它们包含在这个文件中,它们不需要包含在各个组件中。一个项目可以包含多个 _Imports.razor 文件。每个文件应用于其当前文件夹和子文件夹。
_Imports.razor文件中的任何@using指令仅应用于 Razor(RAZOR)文件。它们不应用于 C#(CS)文件。当我们在本章后面讨论代码隐藏技术时,这种区别很重要。
Demo项目包括许多类型的文件,分为不同的文件夹。接下来,我们将检查Shared文件夹的内容。
检查共享 Razor 组件
共享 Razor 组件位于Shared文件夹中。Demo项目中有三个共享 Razor 组件:
-
MainLayout组件 -
NavMenu组件 -
SurveyPrompt组件
MainLayout组件
MainLayout组件用于定义Demo项目的页面布局:
Shared/MainLayout.razor
**@inherits LayoutComponentBase**
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/"
target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
突出的代码表明MainLayout组件继承自LayoutComponentBase类。LayoutComponentBase类代表一个布局,并且只有一个属性,即Body属性。Body属性获取要渲染在布局内的内容。
以下图表展示了由Demo项目的MainLayout组件定义的页面布局:

图 2.17:Demo 项目的页面布局
提示
Blazor WebAssembly App项目模板使用Bootstrap 5.1来样式化其页面。如果你不熟悉 Bootstrap 5.1,可以参考getbootstrap.com/docs/5.1/getting-started/introduction/来熟悉其语法。遗憾的是,Microsoft 提供的项目模板并没有使用 Bootstrap 的最新版本。要了解更多关于 Bootstrap 最新版本的信息,请参考getbootstrap.com。
MainLayout组件包括其自己的专用 CSS 样式,这些样式定义在MainLayout.razor.css文件中。这是一个CSS 隔离的例子。通过使用 CSS 隔离,我们可以减少全局样式的数量,并避免嵌套内容中的样式冲突。正如我们之前提到的,所有组件级别的样式将在构建过程中打包到一个 CSS 文件中。
NavMenu组件
NavMenu组件定义了Demo项目的导航菜单。它使用多个NavLink组件来定义各种菜单选项。这是NavMenu组件引用用于项目导航的NavLink组件的部分:
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link"
href="" Match="NavLinkMatch.All">
<span class="oi oi-home"
aria-hidden="true">
</span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus"
aria-hidden="true">
</span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich"
aria-hidden="true">
</span> Fetch data
</NavLink>
</div>
</nav>
</div>
NavLink组件定义在Microsoft.AspNetCore.Components.Routing命名空间中。它表现得像一个a元素,但它增加了突出当前 URL 的功能。这是当Counter组件被选中时NavLink为Counter组件渲染的 HTML:
<a href="counter" class="nav-link active">
<span class="oi oi-plus" aria-hidden="true"></span>
Counter
</a>
用于nav-link类的样式来自 Bootstrap。
NavMenu组件包括其自己的专用 CSS 样式,这些样式在NavMenu.razor.css文件中定义。这是 CSS 隔离的另一个例子。
SurveyPrompt 组件
SurveyPrompt组件创建了一个指向 Blazor 简短调查的链接。
在Demo项目中,Shared文件夹包含非路由组件。接下来,我们将检查Pages文件夹中的路由组件。
检查可路由的 Razor 组件
可路由的 Razor 组件位于Pages文件夹中。一个可路由的 Razor 组件在文件顶部包含一个或多个@page指令。Demo项目中有三个可路由的 Razor 组件:
-
Index 组件
-
Counter 组件
-
FetchData 组件
Index 组件
Demo项目的Home页面使用了在Pages/Index.razor文件中定义的Index组件:
Pages/Index.razor
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
上述代码包括一个@page指令,它引用了 Web 应用的根目录和一些标记。标记包括一个PageTitle组件和一个SurveyPrompt组件。
PageTitle组件是一个内置的 Razor 组件,它渲染一个 HTML title元素。title元素用于在浏览器中定义页面的标题,并在浏览器标签上显示的文本。它还在页面被添加到收藏夹时使用。
重要提示
如果你的组件包含多个PageTitle组件,则只使用最后一个被渲染的组件。其他组件将被忽略。
SurveyPrompt组件是一个在Shared文件夹中定义的自定义组件。
Counter 组件
Counter组件比Index组件更复杂。像Index组件一样,它包含一个用于路由的@page指令和一些标记。然而,它还包含一个 C#代码块:
Pages/Counter.razor
@page "/counter"
<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变量来保存按钮被点击的次数。每次点击Counter按钮时,都会调用Counter组件注册的@onclick处理程序。在这种情况下,它是IncrementCount方法。
IncrementCount方法增加currentCount变量的值,Counter组件重新生成其渲染树。Blazor 将新的渲染树与之前的渲染树进行比较,并将任何修改应用到浏览器的 DOM 上。这导致显示的计数被更新。
FetchData 组件
FetchData组件到目前为止是Demo项目中最复杂的组件。
这些是Pages/FetchData.razor文件中的指令:
@page "/fetchdata"
@inject HttpClient Http
@page指令用于路由,@inject指令用于依赖注入。在此组件中,定义在Program.cs文件中的HttpClient被注入到视图中。有关依赖注入的更多信息,请参阅第七章,使用应用程序状态构建购物车。
下面的标记演示了在开发 Blazor WebAssembly 应用程序时经常使用的一个非常重要的模式。因为应用在浏览器上运行,所以所有数据访问都必须是异步的。这意味着当页面首次加载时,数据将是 null。因此,在尝试处理数据之前,您始终需要测试 null 的情况。
这是 Pages/FetchData.razor 文件中的标记:
<PageTitle> Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</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>
}
前面的标记包括一个 if 语句和一个 foreach 循环。当 forecasts 的值为 null 时,会显示 Loading 消息。一旦 forecasts 的值不再为 null,数组中的所有项都会以表格的形式呈现。
重要提示
页面首次渲染时,forecasts 的值将是 null。如果您没有处理 forecasts 的值为 null 的情况,框架将抛出异常。
如前所述,Blazor 组件有一个定义良好的生命周期。当组件被渲染时,会调用 OnInitializedAsync 方法。在 OnInitializedAsync 方法完成后,组件将被重新渲染。
这是 Pages/FetchData.razor 文件中的代码块:
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await
Http.GetFromJsonAsync<WeatherForecast[]>
("sample-data/weather.json");
}
public 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 的可空数组。然后,它使用 OnInitializedAsync 异步方法填充数组。为了填充数组,使用了 HttpClient 服务的 GetFromJsonAsync 方法。有关 HttpClient 的更多信息,请参阅 第十一章,使用 ASP.NET Web API 构建 Task Manager。
使用组件
通过在另一个组件的标记中包含它们来使用 Razor 组件。我们将向 Home 页面添加一个 Counter 组件。我们这样做如下:
-
返回 Visual Studio。
-
打开
Pages/Index.razor文件。 -
删除
PageTitle组件之后的所有标记。确保您不要删除文件顶部的
@page指令。 -
在
PageTitle组件下方添加以下标记:<Counter /> -
从 构建 菜单中选择 构建解决方案 选项。
-
返回浏览器并导航到 首页。如果
Demo项目没有运行,从 调试 菜单中选择 不调试启动 (Ctrl+F5) 选项来运行它。 -
按 Ctrl+R 刷新浏览器。
提示
每次您更新 C# 代码时,您需要刷新浏览器以便浏览器加载更新的 DLL,除非您使用 热重载。
-
点击
Click me按钮三次以测试Counter组件。 -
当前值现在是
3。我们在另一个 Razor 组件内部嵌套了一个 Razor 组件。接下来,我们将使用
热重载更新组件。
修改组件
通过使用 热重载,我们可以自动更新应用,而无需重新构建它或刷新浏览器。我们将使用 热重载 更新 Counter 组件并重新构建应用。我们这样做如下:
-
返回 Visual Studio,无需关闭浏览器。
如果可以,请配置您的屏幕同时显示浏览器和 Visual Studio。
-
打开
Pages/Counter.razor文件。 -
将
h1元素中的文本更改为以下内容:<h1>Count by 1</h1> -
在工具栏上点击Hot Reload下拉按钮或按Alt+F10。
-
验证浏览器上的文本是否已更改。
-
点击点击我按钮 3 次。
-
当前值现在是
6。
重要提示
当使用Hot Reload更新代码时,当前计数的值没有改变。
-
使用工具栏上的Hot Reload下拉按钮选择文件保存时 Hot Reload。
-
更新
PageTitle组件下方的标记为以下内容:<div class="alert alert-info"> <h1>Count by 1</h1> <p role="status">Current value: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount"> Click me </button> </div> -
上述代码将为
Counter组件添加一些格式,并将标签从当前计数更改为当前值。 -
将以下代码添加到代码块的顶部:
private int increment = 1; -
点击Ctrl+S。
-
显示Hot Reload警告对话框。
-
选择当更新无法应用时始终重新构建复选框。
-
点击重建并应用更改按钮。
-
将
IncrementCount方法更新为以下内容:private void IncrementCount() { currentCount += increment; } -
点击Ctrl+S。
应用程序重新构建并刷新浏览器,而不显示Hot Reload警告对话框。
通过使用Hot Reload,我们能够修改代码并立即在浏览器中看到这些更改。在这个例子中,我们并没有处于调试模式,但重要的是要记住Hot Reload在调试模式下也有效。接下来,我们需要设置增量的值。
向组件添加参数
大多数组件都需要参数。要将参数添加到组件中,请使用Parameter属性。我们将添加一个参数来指定IncrementCount方法使用的增量。我们这样做如下:
-
返回 Visual Studio。
-
打开
Pages/Counter.razor文件。 -
将以下代码添加到代码块的顶部以定义新参数:
[Parameter] [SupplyParameterFromQuery] public int? Increment { get; set; } -
添加以下
OnParametersSet方法,将increment的值设置为Increment参数的值:protected override void OnParametersSet() { if (Increment.HasValue) increment = Increment.Value; }添加
OnParametersSet方法将需要重启,如下图所示:![图形用户界面,文本,电子邮件 自动生成的描述]()
图 2.18:Demo 项目的页面布局
-
从Hot Reload下拉按钮中选择重启应用程序。
-
将
h1元素中的文本更改为以下内容:<h1>Count by @increment</h1> -
更新地址栏为以下内容
/counter?increment=5 -
点击点击我按钮 3 次。
-
当前值现在是
15。
我们添加了一个参数,可以从查询字符串中获取其值。它也可以从属性中获取其值。接下来,我们将向Index组件添加一个Counter组件,每次点击时增加其值 7。
使用具有属性的参数
我们将在Home页面上添加另一个Counter组件实例,该实例使用新的参数。我们这样做如下:
-
打开
Pages/Index.razor文件。 -
将以下标记添加到
Index.razor文件的底部:<hr> <Counter Increment="7"/>在添加标记时,会为新的
Increment参数提供 IntelliSense 支持:![图形用户界面,应用程序,团队描述自动生成]()
图 2.19:IntelliSense
-
按 Ctrl+S。
-
返回浏览器。
-
导航到 首页。
现在的 首页 包含了两个
Counter组件的实例。如果你点击第一个 Click me 按钮,第一个计数器会增加 1;如果你点击第二个 Click me 按钮,第二个计数器会增加 7。 -
点击每个 Click me 按钮,以验证它们是否按预期工作。
添加路由参数
组件可以拥有多个 @page 指令。我们将向 Counter 组件添加一个使用参数的 @page 指令。我们这样做如下:
-
返回 Visual Studio。
-
打开
Pages/Counter.razor文件。 -
从
Increment参数中移除SupplyParameterFromQuery属性。 -
在文件顶部添加以下
@page指令:@page "/counter/{increment:int}" -
Counter组件现在包含两个@page指令。 -
按 Ctrl+S。
-
导航到
Counter页面。 -
更新 URL 到以下:
/counter/4
重要提示
由于页面在更改 URL 时会自动重新加载,因此你不需要刷新浏览器来重新加载页面。
-
点击 Click me 按钮。
计数器现在应该增加 4。
-
更新 URL 到无效的路由:
/counter/a由于这不是一个有效的路由,你将被重定向到
App组件中定义的NotFound内容:![图形用户界面,应用程序,团队描述自动生成]()
图 2.20:页面未找到
提示
如果你需要在代码中导航到 URL,你应该使用 NavigationManager。NavigationManager 提供了一个 NavigateTo 方法,用于将用户导航到指定的 URI,而不会强制页面加载。
使用部分类将标记与代码分离
许多开发者更喜欢将他们的标记与 C# 字段、属性和方法分开。由于 Razor 组件是常规的 C# 类,它们支持部分类。使用 partial 关键字创建部分类。我们将使用部分类将代码块从 RAZOR 文件移动到 CS 文件。我们这样做如下:
-
返回 Visual Studio。
-
右键单击
Pages文件夹,从菜单中选择 Add,Class。 -
将新类命名为
Counter.razor.cs。 -
使用
partial关键字将Counter类更新为部分类:public `partial` class Counter{} -
打开
Pages/Counter.razor文件。 -
将代码块中的所有代码复制到
Counter.razor.cs文件中的部分Counter类。 -
从
Counter.razor文件中删除代码块。 -
按 Ctrl+S。
-
导航到 Counter 页面。
-
点击 Click me 按钮以验证它是否仍然工作。
-
关闭浏览器。
使用部分类可以让你将代码块中的代码移动到单独的文件,从而允许你使用代码隐藏技术。
提示
创建代码后页面的快速方法是右键单击代码块,并使用快速操作和重构选项将块提取到代码后。
我们使用 Microsoft 提供的Blazor WebAssembly App项目模板创建了一个Demo项目。我们为Counter组件添加了一个参数,并将Counter组件代码块中的代码移动到了一个单独的文件中。
摘要
现在,你应该能够创建一个 Blazor WebAssembly 应用程序。
在本章中,我们介绍了 Razor 组件。我们学习了它们的参数、命名约定、生命周期和结构。我们还学习了路由和 Razor 语法。最后,我们学习了如何使用热重载。
之后,我们使用了 Microsoft 提供的Blazor WebAssembly App项目模板创建了Demo项目。我们检查了Demo项目中的每个文件。我们为Counter组件添加了一个参数,并检查了路由的工作方式。最后,我们练习了使用热重载。
问题
以下问题供您考虑:
-
Razor 组件可以包含 JavaScript 吗?
-
Razor 语法支持哪些类型的循环?
-
组件的参数可以使用 POCO 定义吗?
-
热重载会渲染 CSS 文件中的更改吗? -
子组件如何触发无限循环?
进一步阅读
以下资源提供了有关本章主题的更多信息:
-
关于 Bootstrap 的更多信息,请参阅
getbootstrap.com。 -
关于 Razor 语法的更多信息,请参阅
learn.microsoft.com/en-us/aspnet/core/mvc/views/razor。 -
关于
热重载的更多信息,请参阅learn.microsoft.com/en-us/visualstudio/debugger/hot-reload。
第三章:调试和部署 Blazor WebAssembly 应用
调试并不总是愉快的,但它却是软件开发的重要方面。当调试 Blazor WebAssembly 应用时,Microsoft Visual Studio 提供了我们所需的大部分功能。然而,由于使用 Blazor WebAssembly 构建的应用在客户端运行,我们还需要学习如何使用浏览器的 开发者工具 (DevTools) 来调试应用。完成 Blazor WebAssembly 应用的调试后,我们可以使用 Microsoft Visual Studio 来部署它。
在本章中,我们将创建一个简单的游戏,我们将用它来练习调试和部署 Blazor WebAssembly 应用。我们将使用 Visual Studio 和 DevTools 来调试应用。我们将学习如何使用 ILogger 接口记录错误,并探讨处理异常的不同方法。完成调试后,我们将在部署到 Microsoft Azure 之前对应用应用 即时编译 (AOT) 技术。
Shift+Alt+D
在浏览器中进行调试。
知识就是力量!
在本章中,我们将涵盖以下主题:
-
调试 Blazor WebAssembly 应用
-
理解日志记录
-
处理异常
-
使用 即时编译 (AOT) 技术
-
将 Blazor WebAssembly 应用部署到 Microsoft Azure
-
创建“猜数字”项目
技术要求
要完成此项目,您需要在您的电脑上安装 Microsoft Visual Studio 2022。有关如何安装 Microsoft Visual Studio 2022 的免费社区版的说明,请参阅 第一章,Blazor WebAssembly 简介。由于我们将部署此项目到 Microsoft Azure,您需要在 Microsoft Azure 上有一个账户。如果您没有 Microsoft Azure 账户,请参阅 第一章,Blazor WebAssembly 简介,以创建一个免费账户。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter03。
代码实战视频在此处可用:packt.link/Ch3。
调试 Blazor WebAssembly.app
调试是一项重要且有用的技能。您可以使用 Visual Studio 内置的调试工具或使用 DevTools 来调试 Blazor WebAssembly 应用。要在浏览器上进行调试,您必须使用以下浏览器之一:
-
Microsoft Edge(版本 80 或更高)
-
Google Chrome(版本 70 或更高)
Visual Studio 和 DevTools 都支持所有常规的调试场景:
-
设置和移除断点。
-
按 F10 以单步执行代码。
-
按 F11 以进入下一个函数。
-
在 Visual Studio 中按 F5 和在浏览器中按 F8 以恢复代码执行。
-
在“局部变量”显示中查看局部变量的值。
-
查看调用栈。
-
设置“监视”值。
以下场景目前不支持。但是,由于微软正在继续对调试过程进行大量改进,它们将在未来得到支持:
-
在未处理的异常上中断。
-
在启动时触达断点。
首先,我们将查看 Visual Studio 中的调试。
Visual Studio 中的调试
如果你有任何使用 Visual Studio 的经验,你可能已经使用过调试工具。要开始调试,从 Visual Studio 菜单中选择 调试,开始调试,或按 F5。
一旦开始调试,你可以使用上述列出的任何场景来调试代码。例如,要在代码行上设置断点,只需单击该行左侧的空白处。
以下截图显示了在第 36 行设置的断点:

图 3.1:Visual Studio 中的断点
在 Visual Studio 中调试就像按 F5 一样简单。在浏览器中调试需要更多努力。
浏览器中的调试
在浏览器中调试 Blazor WebAssembly 应用需要几个步骤。以下图像显示了在浏览器中开始调试应用程序所需的步骤:

图 3.2:在浏览器中启用调试
这些是启用浏览器调试的步骤:
-
按 Ctrl+F5 启动应用程序,无需调试。
-
按 Shift+Alt+D 开始调试。
提示
要成功开始调试,确保在按 Shift+Alt+D 之前你的应用程序具有焦点。
由于你的浏览器尚未启用远程调试运行,在你按 Shift+Alt+D 之后,你会收到以下警告:
![文本 自动生成的描述]()
图 3.3:无法找到可调试浏览器标签警告
无法找到可调试浏览器标签 警告提供了关于如何进行操作的说明,适用于 Google Chrome 和 Microsoft Edge。由于我们使用 Microsoft Edge 进行这些截图,我们在前面的图像中突出显示了 Microsoft Edge 的说明。
-
要在浏览器中启用调试,我们需要将提供的文本从 无法找到可调试浏览器标签 警告复制到 Windows 运行命令对话框中。按 Win+R 打开运行命令对话框,粘贴文本,然后按 Enter。
此过程将启动另一个具有调试功能的浏览器窗口。如果你被要求同步你的设备,你可以选择 否。
-
关闭之前的浏览器窗口。
此步骤不是必需的。我们包括它是因为同时打开多个浏览器可能会造成混淆。
-
按 Shift+Alt+D。
我们已在浏览器中启用了调试。
现在浏览器中有两个标签页打开。第一个标签页正在运行应用程序,第二个标签页正在运行
DevTools:![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 3.4:启用调试的浏览器
上述图像显示了
DevTools标签。左侧区域是无效的。它用于显示其他标签的屏幕内容。DevTools中的 控制台 标签已选中。然而,我们将使用 源 标签进行调试。一旦开始调试,我们可以使用上述列出的任何场景来调试代码。例如,要在代码行上设置断点,我们只需在代码行左侧的空白处单击。
以下截图显示了已设置在第 36 行的断点:
![图形用户界面,文本,应用程序,电子邮件 描述自动生成]()
图 3.5:DevTools 的源标签
使用 Visual Studio 或直接在浏览器中调试 Blazor WebAssembly 应用程序是可能的。在浏览器中启用调试需要更多的努力,但提供了与 Visual Studio 中相同的功能。
通过有效地使用日志,调试可以更加高效。接下来,我们将学习如何在 Blazor WebAssembly 应用程序中使用日志。
理解日志记录
记录日志是解决任何应用程序问题的基本工具。它有助于识别和解决问题。在微软提供的 Blazor WebAssembly 项目模板中,日志默认是启用的。然而,启用的唯一日志提供者是控制台提供者。
重要提示
控制台 提供者不存储日志,它只显示它们。如果您需要保留日志,您将需要使用不同的提供者。
以下代码示例执行以下操作:
-
将
ILogger<Counter>对象注入到页面中。它使用类的完全限定名称作为日志类别。该日志类别包含由该ILogger实例创建的每个日志消息。 -
调用
LogInformation以在Information日志级别记录指定的字符串。
以下代码在每次点击按钮时都会写入日志:
Counter.razor
@page "/counter"
**@inject ILogger<Counter> logger;**
<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()
{
**logger.LogInformation("Button Clicked!");**
currentCount++;
}
}
以下截图显示了 DevTools 中的 控制台 标签的结果,显示了点击 点击我 按钮的结果:

图 3.6:日志示例
理解日志级别
每当我们向记录器记录一个项目时,我们必须提供日志级别。日志级别表示消息的严重性。
以下表格列出了从最低到最高严重程度的日志级别值:
| 日志级别 | 方法 | 描述 |
|---|---|---|
| Trace | LogTrace |
这些消息非常详细,可能包含敏感数据。它们默认是禁用的,不应在生产环境中启用。 |
| Debug | LogDebug |
这些消息仅在调试和开发期间使用。 |
| Information | LogInformation |
这些消息跟踪应用程序的一般流程。 |
| Warning | LogWarning |
这些消息用于意外或不正常的事件。 |
| 错误 | LogError |
这些消息是针对当前操作中的错误,例如保存失败。 |
| 严重 | LogCritical |
这些消息是针对导致整个应用程序失败的严重错误。 |
表 3.1:日志级别
提示
您应该使用 ILogger 而不是 System.Console.WriteLine 或 System.Diagnostics.Debug.WriteLine,因为那些方法只能让您将文本发送到控制台,并且它们不包括产生消息的类的名称。此外,它们必须在应用程序部署之前被删除或隐藏。
在使用日志记录时,请尝试使用适当的日志级别,并尽量使您的消息尽可能简短,同时不要使其失去意义。使用一致的消息格式,以便它们可以轻松过滤。最后,避免记录冗余或不相关信息,因为日志记录不是免费的;它消耗一些资源。
您可以根据需要调整最小日志级别。
以下 Logging 组件演示了如何使用各种不同的日志级别:
Logging.razor
@page "/logging"
@inject ILogger<Logging> logger;
<PageTitle>Logging</PageTitle>
<h1>Logging</h1>
<button class="btn btn-primary" @onclick="DemoLogging">
Click me
</button>
@code {
private void DemoLogging()
{
logger.LogTrace("Logger: Trace");
logger.LogDebug("Logger: Debug");
logger.LogInformation("Logger: Information");
logger.LogWarning("Logger: Warning");
logger.LogError("Logger: Error");
logger.LogCritical("Logger: Critical");
logger.Log(LogLevel.None, "Logger: None");
}
}
以下截图显示了在 DevTools 中如何渲染不同的日志级别。

图 3.7:日志级别
设置最小日志级别
默认情况下,项目配置为显示所有日志项,其最小日志级别为 Information。您可以通过完成以下步骤来调整此设置:
-
从 工具 菜单中选择 NuGet 包管理器,包管理器控制台 以打开 包管理器控制台。
-
在 包管理器控制台 中输入以下文本并按 Enter 键:
Install-Package Microsoft.Extensions.Logging.Configuration上述代码将向项目中添加 Microsoft.Extenstions.Logging.Configuration NuGet 包。
-
右键单击
wwwroot文件夹,从菜单中选择 添加,新建项 选项。 -
按 Ctrl+E 进入 搜索 文本框。
-
在 搜索 文本框中输入
app settings file。 -
在 名称 文本框中输入
appsettings.json并单击 添加 按钮。 -
将默认的 JSON 替换为以下 JSON:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Debug" } } } -
将以下行添加到
Client/Program.cs文件的Main方法中:builder.Logging.AddConfiguration( builder.Configuration.GetSection("Logging"));
以下代码将配置浏览器仅记录日志级别至少为 Debug 的项目。
日志记录是理解 Blazor WebAssembly 应用程序流程的必要工具。根据消息类型,有不同的日志级别。最小日志级别可以通过 appsetting.json 文件进行调整。
现在让我们看看一些处理异常的不同方法。
处理异常
作为模板的一部分,当 Blazor WebAssembly 应用程序中出现未处理的异常时,屏幕底部将显示一个黄色条。

图 3.8:示例未处理的异常
您可以通过修改 index.html 文件来修改显示的错误消息的文本和样式。黄色条目的 UI 在 wwwroot/index.html 文件中定义:
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">X</a>
</div>
提示
您可以修改在 wwwroot/css/app.css 文件中显示的错误消息的样式。
在前面的截图中,点击 抛出异常 按钮会抛出一个未处理的异常。这是我们在前面的截图中用于创建 抛出异常 按钮的 ThrowException 组件的代码:
<button class="btn btn-primary" @onclick="NewException">
Throw Exception
</button>
@code {
private void NewException()
{
throw new Exception("This is a sample error!");
}
}
要查看异常,我们需要通过按 F12 并选择 控制台 标签来打开 DevTools。默认情况下,未处理的异常会被记录到 控制台。以下截图显示了 控制台 标签:

图 3.9:示例关键日志
在一个完美的世界中,没有未处理的异常。在我们的世界中,有错误边界和自定义错误组件。
设置错误边界
错误边界可以用来处理异常。ErrorBoundary 组件是一个用于在 UI 层面处理未处理错误的内置组件。它包括一个在未发生错误时渲染的 ChildContent 属性和一个在发生错误时渲染的 ErrorContent 属性。ErrorBoundary 组件可以包裹在任意其他组件周围。
以下代码将 ErrorBoundary 组件包裹在 ThrowException 组件周围:
<ErrorBoundary>
<ChildContent>
<ThrowException />
</ChildContent>
</ErrorBoundary>
当按下 抛出异常 按钮时,将显示以下信息:

图 3.10:默认 ErrorBoundary UI
ErrorBoundary 组件的默认 UI 在 wwwroot/css/app.css 文件中定义。默认消息非常通用。我们可以通过使用 ErrorBoundary 组件的 ErrorContent 属性来添加我们自己的自定义错误消息。这是包含 ErrorContent 属性的更新后的 ErrorBoundary 组件:
<ErrorBoundary>
<ChildContent>
<ThrowException />
</ChildContent>
**<****ErrorContent****>**
**<****h3****>****The Throw Exception button caused this error!****<****/****h3****>**
**<****/****ErrorContent****>**
</ErrorBoundary>
以下截图显示了自定义 ErrorContent 的结果:

图 3.11:自定义 ErrorContent
ErrorBoundary 组件仅处理 UI 层面的错误。它允许开发者在一个 UI 点处捕获错误。要程序化处理错误,我们需要创建一个自定义错误组件。
创建自定义错误组件
自定义错误组件可以传递给每个子组件。以下 ErrorHandler 组件在遇到错误时将写入日志:
ErrorHandler.razor
@inject ILogger<ErrorHandler> Logger
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
public void ProcessError(Exception ex)
{
Logger.LogError("Message: {Message}", ex.Message);
}
}
ErrorHandler 需要作为 CascadingParameter 添加到将使用它的组件中。这是 ThrownExceptionHandled 组件:
ThrownExceptionHandled.razor
<button class="btn btn-primary" @onclick="NewException">
Throw Exception
</button>
@code {
[CascadingParameter]
public ErrorHandler? Error { get; set; }
private void NewException()
{
try
{
throw new Exception("This is a sample error!");
}
catch (Exception ex)
{
Error?.ProcessError(ex);
}
}
}
当 ThrownExceptionHandled 组件被 ErrorHandler 组件包裹时,所有未处理的错误都会记录到 控制台:
<ErrorHandler>
<ThrowExceptionHandled />
</ErrorHandler>
以下截图显示了错误是如何渲染到 控制台 的:

图 3.12:处理错误
当遇到未处理的异常时,微软提供的 Blazor WebAssembly 项目模板在页面底部渲染一个通用的黄色条。我们可以使用 ErrorBoundary 组件来自定义 UI。更好的是,我们可以创建一个自定义错误组件,以更好地控制错误处理方式。
现在我们知道了如何处理错误,让我们学习如何为部署准备我们的应用程序。
使用即时编译 (AOT) 编译
默认情况下,Blazor WebAssembly 应用程序在浏览器上运行时使用 .NET 中间语言 (IL) 解释器。即时编译 (AOT) 允许您在部署前将 .NET 代码编译成 WebAssembly。由于编译后的代码比解释代码性能更高,因此您的应用程序将运行得更快。使用 AOT 的唯一缺点是应用程序可能更大,因此应用程序启动时加载所需的时间会更长。
启用 AOT 的步骤如下:
-
在 解决方案资源管理器 中右键单击项目,从菜单中选择 属性。
-
在 搜索属性 文本框中输入
AOT。 -
选中 发布时使用即时编译 (AOT) 复选框。
一旦启用 AOT,每次发布项目时都会进行 AOT 编译。使用 AOT 编译发布应用程序需要更长的时间,但可以使 Blazor WebAssembly 应用程序运行得更快。这对于计算密集型应用程序尤其如此。
重要提示
您必须安装 wasm-tools 才能使用 AOT。要安装 wasm-tools,请运行以下命令并重新启动 Visual Studio:
dotnet workload install wasm-tools
现在我们已经准备好部署 Blazor WebAssembly 应用程序了。
将 Blazor WebAssembly 应用程序部署到微软 Azure
使用 Visual Studio 部署 Blazor WebAssembly 应用程序相当简单。Visual Studio 中包含了一个易于遵循的向导。以下是使用 Visual Studio 2022 将 Blazor WebAssembly 应用程序部署到微软 Azure 的步骤:
-
右键单击项目,从菜单中选择 发布。
这是发布向导的第一页:
![图形用户界面、文本、应用程序 描述自动生成]()
图 3.13:发布向导的第一页
如您所见,提供了许多选项。对于这个项目,我们将把应用程序发布到微软云中。
-
选择 Azure 并点击 下一步 按钮。
这是发布向导的第二页:
![图形用户界面、文本、应用程序 描述自动生成]()
图 3.14:发布向导的第二页
-
选择 Azure App Service (Windows) 并点击 下一步 按钮。
这是 发布 向导的最后一页。此页用于选择用于应用程序的 Azure 应用服务。您也可以使用此页创建 Azure 应用服务。
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 3.15:发布向导的最后页面
-
如果您还没有,请创建一个 Azure 应用服务。
这是 创建新应用服务 对话框的截图:
![图形用户界面,应用程序,电子邮件 自动生成的描述]()
图 3.16:创建新应用服务对话框
如果您还没有托管计划,我们建议您为该项目创建一个 免费 托管计划。以下截图显示了选择 免费 选项的 创建新托管计划 对话框:
![图形用户界面,应用程序,表格 自动生成的描述]()
图 3.17:创建新托管计划对话框
-
点击 完成 按钮。
-
点击 关闭 按钮。
应用程序现在已准备好发布。
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 3.18:发布对话框
-
点击 发布 按钮,等待。
应用发布后,浏览器将自动打开。
将 Blazor WebAssembly 应用部署到 Microsoft Azure 与遵循向导中的步骤一样简单。
创建“猜数字”项目
在本节中,我们将构建一个简单的数字猜谜游戏。游戏将允许多次猜测,并在我们获胜时提醒我们。
这是完成的应用程序的截图:

图 3.19:猜数字游戏
此项目的构建时间大约为 60 分钟。
项目概述
将使用 Microsoft 的 Blazor WebAssembly App Empty 项目模板创建一个空的 Blazor WebAssembly 项目来创建 GuessTheNumber 项目。首先,我们将添加项目所需的功能组件。然后,我们将添加日志记录。我们将在 Visual Studio 和浏览器中调试应用程序。我们将添加一个 ErrorBoundary 组件。最后,我们将该项目部署到 Microsoft Azure。
开始使用项目
我们需要创建一个新的 Blazor WebAssembly 应用。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击 创建新项目 按钮。
-
按 Alt+S 进入 搜索模板 文本框。
-
输入
Blazor并按 Enter 键。以下截图显示了 Blazor WebAssembly App Empty 项目模板。
![图形用户界面,文本,应用程序,聊天或短信 自动生成的描述]()
图 3.20:Blazor WebAssembly App Empty 项目模板
-
选择 Blazor WebAssembly App Empty 项目模板并点击 下一步 按钮。
-
在 项目名称 文本框中输入
GuessTheNumber并点击 下一步 按钮。这是配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 3.21:配置新项目对话框
提示
在前面的示例中,我们将
GuessTheNumber项目放置在E:\Blazor文件夹中。然而,此项目的位置并不重要。 -
选择使用 .NET 7.0 作为 框架 的版本。
-
选择 Configure for HTTPS 复选框。
-
取消选择 ASP.NET Core Hosted 复选框。
-
取消选择 Progressive Web Application 复选框。
这是 附加信息 对话框的截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 3.22:附加信息对话框
-
点击 创建 按钮。
我们已经创建了 GuessTheNumber Blazor WebAssembly 项目。现在我们需要添加一个组件。
添加游戏组件
我们需要添加一个包含 Razor 页面、代码隐藏页面和级联样式表的 Game 组件。我们这样做如下:
-
右键单击
GuessTheNumber项目,从菜单中选择 添加,新文件夹 选项。 -
将新文件夹命名为
Shared。 -
右键单击
Shared文件夹,从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
Game。 -
用以下标记替换
Game组件中的默认代码:<div class="game"> <strong>Guess the Number</strong> <div class="match-val">correct digit</div> <div class="match-pos">correct position</div> <div>digits: @digitCount</div> <hr /> @if (guesses == null) { <h1>Loading...</h1> } else { <div class="guesses"> </div> } </div>上述代码为游戏创建了框架。
-
在
guessesdiv 中输入以下代码:@foreach (var row in guesses) { @for (int i = 0; i < digitCount; i++) { <span class="@row.Matches[i]"> @row.Guess[i] </span> } <br /> }上述代码遍历每个
guesses并在屏幕上渲染它们。 -
在
@foreach语句下方输入以下代码:@if (winner) { <span>Winner!</span> <div> <button @onclick="PlayAgain">Play Again</button> </div> } else { <input type="text" @bind=guess class="guess" inputmode="numeric" size="@digitCount" maxlength="@digitCount" /> <button @onclick="GuessAnswer">Guess</button> }
上述代码检查玩家是否是赢家。如果是赢家,它将显示 再次播放 按钮。如果不是赢家,它将提供一个 input 元素,让他们可以输入另一个猜测。
现在我们已经添加了标记,我们需要添加代码。
添加代码
我们将在一个单独的文件中添加项目的代码。我们这样做如下:
-
右键单击
Shared文件夹,从菜单中选择 添加,类 选项。 -
输入
Game.razor.cs并点击 添加 按钮。 -
将以下
using语句添加到文件顶部:using System.Text; -
在
Game类中添加partial关键字:public `partial` class Game { } -
将以下代码添加到
Game类中:[Parameter] public int? Digits { get; set; } private int digitCount = 4; private string answer = ""; private string guess = ""; private List<Row> guesses = new(); private bool winner = false; protected override void OnParametersSet(){ } private void CalculateAnswer(){ } private void GuessAnswer(){ } private void PlayAgain(){ } public class Row { public string Guess { get; set; } public string[] Matches { get; set; } }上述代码定义了我们将在
Game组件中使用的属性和方法。它还定义了Row类。 -
将以下代码添加到
OnParametersSet方法中:if (Digits.HasValue) { digitCount = (int)Digits; }; CalculateAnswer();上述代码设置了
digitCount的值。在确定digitCount的值之后,它调用CalculateAnswer方法。 -
将以下代码添加到
CalculateAnswer方法中:StringBuilder calculateAnswer = new StringBuilder(); for (int i = 0; i < digitCount; i++) { int nextDigit = new Random().Next(0, 10); calculateAnswer.Append(nextDigit); } answer = calculateAnswer.ToString();上述代码根据
digitCount指定的数字位数计算answer。 -
将以下代码添加到
GuessAnswer方法中:var curGuess = new Row() { Guess = guess, Matches = new string[digitCount] }; for (int i = 0; i < digitCount; i++) { if (answer[i] == guess[i]) { curGuess.Matches[i] = "match-pos"; } else { if (answer.Contains(guess[i])) { curGuess.Matches[i] = "match-val"; } } } guesses.Add(curGuess); guess = ""; if (guess == answer) winner = true;前面的代码将答案中的每个数字与猜测中的每个数字进行比较。如果猜测等于答案,他们就是赢家。
-
将以下代码添加到
PlayAgain方法中:winner = false; guesses = new(); CalculateAnswer();前面的代码重置了游戏。
我们几乎完成了 Game 组件的创建。我们只需要添加一些样式。
添加样式表
我们将使用 CSS 隔离添加一个样式表。我们这样做如下:
-
右键单击
Shared文件夹,从菜单中选择 添加,新建项 选项。 -
按 Ctrl+E 进入 搜索 文本框。
-
在 搜索 文本框中输入
css。 -
在 名称 文本框中输入
Game.razor.css并点击 添加 按钮。 -
输入以下样式:
.game { padding: 15px; font-size: 4rem; } input, button, .guesses { font-size: 4rem; font-family: Courier New, Courier, monospace } .match-pos { color: green; } .match-val { color: red }样式表将字体设置为等宽字体,用于过去猜测列表和当前猜测。同时,用于指定颜色的类也被定义。
-
打开
wwwroot/index.html文件。 -
在
head元素中取消注释对GuessTheNumber.styles.css样式表的链接:<head> <meta charset="utf-8" /> <base href="/" /> <link href="css/app.css" rel="stylesheet" /> **<****link****href****=****"GuessTheNumber.styles.css"** **rel****=****"stylesheet"** **/****>** </head>
让我们通过玩游戏来测试应用程序。
设置和玩游戏
我们需要将 Game 组件添加到 Index 组件中以便玩游戏。我们这样做如下:
-
打开
_Imports.razor文件。 -
将以下
using语句添加到文件中:@using GuessTheNumber.Shared通过将前面的
using语句添加到_Imports.razor文件中,它将自动导入同一文件夹及其子文件夹中的其他.razor文件。 -
打开
Pages/Index.razor文件。 -
将以下
h1元素替换为:<PageTitle>Guess the Number</PageTitle> <Game /> -
按 Ctrl+F5 开始应用程序,不进行调试。
-
输入一个 4 位数字并点击 猜测 按钮。
如果答案中有数字,它将以红色显示。如果数字在答案中,并且它在正确的位置,它将以绿色显示。目标是猜测答案。
-
输入另一个 4 位数字并点击 猜测 按钮。
-
尝试赢得游戏。
由于我们的代码中存在一个错误,所以不可能赢得游戏。让我们添加一些日志记录来尝试找到问题。
添加日志记录
我们将在应用程序中添加日志记录。我们这样做如下:
-
打开
Game.razor.cs文件。 -
将以下
using语句添加到文件顶部:using System.Text.Json; -
将以下代码添加到
Game类的顶部:[Inject] ILogger<Game>? logger { get; set; }前面的代码将一个
ILogger对象注入到Game组件中,以创建一个记录器。 -
将以下代码添加到
CalculateAnswer方法的末尾:logger.LogInformation($"The answer is {answer}"); -
将以下代码添加到
GuessAnswer方法的末尾:logger.LogInformation(JsonSerializer.Serialize(guesses)); -
按 Ctrl+F5 开始应用程序,不进行调试。
-
按 F12 打开
DevTools。 -
选择 控制台 选项卡。
-
输入一个 4 位数字并点击 猜测 按钮。
-
输入另一个 4 位数字并点击 猜测 按钮。
以下截图显示了已记录到 控制台 选项卡的消息:
![图形用户界面,文本 自动生成的描述]()
图 3.23:记录的信息
通过使用日志记录,我们可以确定我们的猜测正在被正确评估。让我们尝试调试我们的代码以找到问题。
在 Visual Studio 中进行调试
首先,我们将使用 Visual Studio 调试 Blazor WebAssembly 应用程序。我们这样做如下:
-
打开
Game.razor.cs文件。 -
在
GuessAnswer方法的第一行添加一个断点。 -
按 F5 以调试模式运行应用程序。
-
输入一个四位数并点击 Guess 按钮。
-
将鼠标悬停在
curGuess上以查看其内容。 -
查看 Locals 窗口。
-
在
guess上设置 Watch。 -
按 F10 以单步执行代码。
-
验证代码是否正确运行。
-
查看 Visual Studio 中的 Output 窗口以帮助理解流程:

图 3.24:Visual Studio 中的输出窗口
-
问题在于在将
guess的值与答案比较之前,它被设置为一个空字符串。 -
将以下代码移动到
GuessAnswer方法的末尾:guess = ""; -
按 F5 以非调试模式运行应用程序。
-
玩游戏直到获胜。
以下截图显示了一个获胜的游戏:
![文本,应用程序,自动生成中等置信度的描述]()
图 3.25:猜数字 – 获胜者
当您的猜测与答案匹配时,您就赢了游戏。让我们通过改变数字的位数来使游戏更难。
更新代码
为了使游戏更难,我们将允许玩家决定他们需要猜测多少位数。我们这样做如下:
-
返回 Visual Studio 2022。
-
打开
Game.razor文件。 -
将
@digitCount替换为以下内容:<input type="number" value=@digitCount inputmode="numeric" min="1" max="10" @onchange="RestartGame" /> -
打开
Game.razor.cs文件。 -
添加以下
RestartGame方法:private void RestartGame(ChangeEventArgs e) { digitCount = Convert.ToInt16(e.Value); PlayAgain(); }上述代码每次更改数字位数时都会重置游戏。
-
按 Ctrl+F5 以非调试模式启动应用程序。
-
播放更新后的游戏。
现在我们将使用浏览器来调试我们的更新。
在浏览器中进行调试
我们将在浏览器中调试新代码。我们这样做如下:
-
按 Shift+Alt+D。
-
将指示的文本复制到您的剪贴板,该文本与您使用的浏览器相对应。
-
按 Win+R,粘贴您复制的文本,然后点击 OK 按钮。
-
按 Shift+Alt+D。
浏览器中打开第二个选项卡。
-
点击 Sources 选项卡并查看
file://节点中的文件。 -
打开
Shared文件夹:

图 3.26:浏览器中的文件夹
-
在
GuessAnswer方法中添加一个断点。 -
返回第一个选项卡。
-
将数字位数更改为 5。
-
输入一个五位数并点击 Guess 按钮。
-
返回第二个选项卡。
-
查看
curGuess的Scope和Local值。 -
按多次 F10 以单步执行函数调用。
-
按 F8 恢复脚本执行。
-
返回第一个选项卡。
-
输入一个三位数并按 Guess 按钮。
-
按 F8 恢复脚本执行。
-
点击页面底部的重新加载链接。
当我们输入的猜测数字少于答案时,遇到了一个未处理的错误。让我们通过添加一个ErrorBoundary组件来解决这个问题。
添加 ErrorBoundary 组件
我们需要将Game组件包裹在一个ErrorBoundary组件中。我们这样做如下:
-
返回 Visual Studio。
-
打开
Index.razor文件。 -
将
Game元素替换为以下内容:<ErrorBoundary> <ChildContent> <Game /> </ChildContent> <ErrorContent> <h1>You have entered an invalid guess!</h1> </ErrorContent> </ErrorBoundary> -
按Ctrl+F5启动应用程序,不进行调试。
-
输入一个三位数并按猜测按钮。
-
验证由
ErrorContent属性定义的消息是否显示。
我们已添加了一个ErrorBoundary组件,当遇到未处理的异常时更新 UI。ErrorContent属性用于定义 UI。现在应用程序已经测试完毕,是时候部署它了。
将应用程序部署到 Microsoft Azure
我们将启用 AOT 编译并将应用程序部署到 Microsoft Azure。我们这样做如下:
-
右键单击
GuessTheNumber项目,从菜单中选择属性。 -
在搜索属性文本框中输入
AOT。 -
勾选发布时使用即时编译(AOT)复选框。
-
右键单击
GuessTheNumber项目,从菜单中选择发布选项。 -
选择Azure并点击下一步按钮。
-
选择Azure App Service (Windows)并点击下一步按钮。
-
选择现有的 Azure App Service 或创建一个新的。
-
点击完成按钮。
重要提示
请耐心等待,将应用程序部署到 Microsoft Azure 需要一些时间。
-
玩游戏。
我们已将
猜数字Web 应用程序部署到 Microsoft Azure。重要提示
在您完成应用程序的测试后,别忘了从您的 Azure 账户中删除您添加的资源。
摘要
现在,你应该能够调试和部署一个 Blazor WebAssembly 应用程序。
在本章中,我们学习了在 Visual Studio 和DevTools中的调试方法。我们学习了不同的日志级别以及如何写入日志。我们学习了如何处理异常。最后,我们学习了在将 Blazor WebAssembly 应用程序部署到 Microsoft Azure 之前如何使用 AOT 编译。
之后,我们使用 Visual Studio 中的Blazor WebAssembly App Empty项目模板创建了一个新项目。我们添加了一个简单的Game组件。我们在应用程序中添加了一些日志记录。我们使用 Visual Studio 和DevTools都添加了应用程序的断点。我们添加了一个ErrorBoundary组件来捕获未处理的错误。最后,我们启用了 AOT 编译并将应用程序部署到 Microsoft Azure。
在下一章中,我们将使用模板组件构建一个模态对话框。
问题
以下问题供您参考:
-
你会如何重写
猜数字游戏以使用自定义错误组件? -
哪些类型的应用程序从 AOT 中受益最大?
-
不同的日志级别有哪些?何时应该使用每个级别?
-
你如何在浏览器中调试 Blazor WebAssembly 应用?
-
你能否免费将 Blazor WebAssembly 应用部署到 Microsoft Azure?
进一步阅读
以下资源提供了有关本章主题的更多信息:
-
有关在 Visual Studio 中调试 C# 代码的更多信息,请参阅
learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-debugger。 -
有关
DevTools的更多信息,请参阅learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/overview。 -
有关日志记录的更多信息,请参阅
learn.microsoft.com/en-us/dotnet/core/extensions/logging。 -
有关 Microsoft Azure 的更多信息,请参阅
azure.microsoft.com。
第四章:使用模板组件构建模态对话框
模态对话框是一个出现在窗口所有其他内容之上的对话框,需要用户交互才能关闭它。模板组件是一个接受一个或多个 UI 模板作为参数的组件。模板组件的 UI 模板可以包含任何 Razor 标记。
在本章中,我们将学习RenderFragment参数、EventCallback参数以及 CSS 隔离。当父组件需要与子组件共享信息时,会使用RenderFragment参数,反之,当子组件需要与其父组件共享信息时,会使用EventCallback参数。CSS 隔离用于将 CSS 样式限定在特定的组件范围内。
在本章中,我们将创建一个模态对话框组件。该组件将是一个模板组件,可以根据其参数渲染不同的 HTML。它将使用事件回调将事件返回给调用组件。它将使用 CSS 隔离来添加使其表现得像模态对话框的格式。我们将通过将其添加到另一个组件来测试模态对话框组件。最后,我们将该组件移动到Razor 类库中,以便它可以轻松地与其他项目共享。
自定义组件
可以重复使用。
创建库!
在本章中,我们将涵盖以下主题:
-
使用
RenderFragment参数 -
使用
EventCallback参数 -
理解 CSS 隔离
-
创建 Razor 类库
-
创建模态对话框项目
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 免费社区版的说明,请参阅第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter04。
《Code in Action》视频在此处可用:packt.link/Ch4。
使用RenderFragment参数
一个RenderFragment参数是 UI 内容的一部分。它用于从父组件向子组件传递 UI 内容。UI 内容可以包括纯文本、HTML 标记、Razor 标记或另一个组件。
以下代码是Alert组件的代码。当Alert组件的Show属性值为true时,会显示其 UI 内容:
Alert.razor
@if (Show)
{
<div>
<div>
<div>
@ChildContent
</div>
<div>
<button @onclick="OnOk">
OK
</button>
</div>
</div>
</div>
}
@code {
[Parameter] public bool Show { get; set; }
[Parameter] public EventCallback OnOk { get; set; }
[Parameter] public RenderFragment ChildContent { get; set;}
}
上述代码,对于Alert组件,包括三种不同类型的参数:简单类型、EventCallback和RenderFragment:
-
第一个参数是
Show属性。它是一个简单类型,类型为Boolean。有关如何将简单类型用作参数的更多信息,请参阅第二章,构建您的第一个 Blazor WebAssembly 应用程序。 -
第二个参数是
OnOk属性。它属于EventCallback类型。在下一节中,我们将了解更多关于EventCallback参数的信息。 -
最后一个参数是
ChildContent属性。它属于RenderFragment类型,是本节的主题。
以下标记使用 Alert 组件在点击 Show Alert 按钮时在对话框中显示当前星期几。在 Alert 元素的开始标签和结束标签之间的 Razor 标记绑定到 Alert 组件的 ChildContent 属性:
@page "/"
<PageTitle>Home</PageTitle>
<Alert Show="showAlert" OnOk="@(() => showAlert = false)">
<h1>Alert</h1>
<p>Today is @DateTime.Now.DayOfWeek.</p>
</Alert>
@if (!showAlert)
{
<button @onclick="@(() => showAlert = true)">
Show Alert
</button>
}
@code {
private bool showAlert = false;
}
以下截图显示了点击 Show Alert 按钮时显示的对话框:

图 4.1:示例警报
要使用元素的内容而不明确指定参数的名称,RenderFragment 参数的名称必须是 ChildContent。例如,以下标记的结果与前面未明确指定 ChildContent 元素的标记相同:
<Alert Show="showAlert" OnOk="@(() => showAlert = false)">
**<****ChildContent****>**
<h1>Alert</h1>
<p>Today is @DateTime.Now.DayOfWeek.</p>
**</****ChildContent****>**
</Alert>
上一段标记中突出显示了 ChildContent 元素。
重要提示
按照惯例,用于捕获父元素内容的 RenderFragment 参数的名称必须是 ChildContent。
通过在标记中明确指定每个参数的名称,可以在组件中包含多个 RenderFragment 参数。在本章中,我们将使用多个 RenderFragment 参数来完成项目。
RenderFragment 参数允许父组件将其要由子组件使用的 UI 内容传达给子组件,而 EventCallback 参数用于从子组件向父组件传达。在下一节中,我们将解释如何使用 EventCallback 参数。
使用 EventCallback 参数
事件回调是在特定事件发生时传递给另一个方法的函数。例如,当 Alert 组件上的按钮被点击时,@onclick 事件使用 OnOk 参数来确定应该调用哪个方法。OnOK 参数引用的方法是在父组件中定义的。
如前所述,EventCallback 参数用于从子组件向父组件共享信息。它们与父组件共享信息,并在发生某些事件,例如按钮点击时通知父组件。父组件只需指定在事件触发时调用的方法。
这是一个 EventCallback 参数的示例:
[Parameter] public EventCallback OnOk { get; set; }
以下示例使用 lambda 表达式为 OnOk 方法。当调用 OnOk 方法时,showAlert 属性的值设置为 false:
<Alert Show="showAlert" OnOk="@(() => showAlert = false)">
<h1>Alert</h1>
<p>Today is @DateTime.Now.DayOfWeek.</p>
</Alert>
@code {
private bool showAlert = false;
}
使用 lambda 表达式创建匿名函数。然而,在使用 EventCallback 参数时,我们不需要使用匿名函数。以下示例展示了如何使用方法而不是匿名函数来更新 OnOk 方法:
<Alert Show="showAlert" OnOk="OkClickHandler">
<h1>Alert</h1>
<p>Today is @DateTime.Now.DayOfWeek.</p>
</Alert>
@code {
private bool showAlert = false;
private void OkClickHandler()
{
showAlert = false;
}
}
上述代码定义了一个新的 OkClickHandler 方法,当按钮被点击时调用。
当编写 Alert 组件时,你可能想直接从组件上的 OnOk 事件更新 Show 参数。你必须不要这样做,因为如果你直接在组件中更新值,并且组件需要重新渲染,任何状态更改都将丢失。如果你需要在组件中维护状态,你应该向组件添加一个私有字段。
重要提示
组件不应向自己的参数写入数据。
有关使用事件的更多信息,请参阅 第八章,使用事件构建看板。
Alert 组件在页面上显示文本,但它还没有像模态对话框那样工作。要使其像模态对话框一样工作,我们需要更新组件使用的样式表。我们可以通过使用 CSS 隔离来实现这一点。在下一节中,我们将解释如何使用 CSS 隔离。
理解 CSS 隔离
用于样式化我们的 Blazor WebAssembly 应用程序的 级联样式表(CSS)的位置通常是 wwwroot 文件夹。通常,这些 CSS 文件中定义的样式应用于 Web 应用程序中的所有组件。然而,有时我们希望对应用于特定组件的样式有更多的控制。为了实现这一点,我们使用 CSS 隔离。使用 CSS 隔离,指定 CSS 文件中的样式将覆盖全局样式,并且仅针对特定组件及其子组件。
启用 CSS 隔离
要添加一个针对特定组件的 CSS 文件,请在与组件相同的文件夹中创建一个与组件同名的 CSS 文件,但带有 CSS 文件扩展名。例如,Alert.razor 组件的 CSS 文件将被称为 Alert.razor.css。
以下标记是 Alert 组件的更新版本。在这个版本中,我们添加了两个突出显示的类:
Alert.razor
@if (Show)
{
<div **class****=****"dialog-container"**>
<div **class****=****"dialog"**>
<div>
@ChildContent
</div>
<div>
<button @onclick="OnOk">
OK
</button>
</div>
</div>
</div>
}
以下 Alert.razor.css 文件定义了新类所使用的样式:
Alert.razor.css
.dialog-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,0.6);
z-index: 2000;
}
.dialog {
background-color: white;
margin: auto;
width: 15rem;
padding: .5rem
}
上述 CSS 包含了 dialog-container 类和 dialog 类的样式:
-
dialog-container: 此类将元素的背景色设置为带有 60% 透明度的黑色,并通过将其 z-index 设置为 2,000 将其放置在其他元素之上。 -
dialog: 此类将元素的背景色设置为白色,将其水平居中在其父元素内,并将其宽度设置为 15 REM。
为了使项目能够使用 CSS,我们需要在 wwwroot/index.html 文件中添加一个链接。按照惯例,需要链接的 CSS 文件名是程序集的名称后跟 .styles.css。例如,如果项目的名称是 Demo4,则需要将 Demo4.styles.css 的链接添加到 wwwroot/index.html 文件中。以下高亮标记显示了应用于 Alert.razor.css 文件中定义的样式的链接:
<head>
<meta charset="utf-8" />
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
**<****link****href****=****"Demo4.styles.css"****rel****=****"****stylesheet"** **/>**
</head>
以下截图显示了使用前面的 Alert.razor.css 文件创建的 Alert 组件:

图 4.2:Alert 组件
在前面的例子中,链接的 Demo4.style.css 文件是在构建时创建的。当它被创建时,Blazor 引擎通过附加框架为每个组件生成的唯一字符串重写每个组件的 CSS 和 HTML。重写的 CSS 样式被捆绑到一个文件中,并保存为静态资源。
这是 Demo4.styles.css 文件的一部分:
.dialog-container[b-j4grw2wm7a] {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,0.6);
z-index: 2000;
}
.dialog[b-j4grw2wm7a] {
background-color: white;
margin: auto;
width: 15rem;
padding: .5rem
}
这是引用重写样式的重写 HTML:

图 4.3:重写的 HTML
按照惯例,Demo4.styles.css 文件的内容位于 obj/{CONFIGURATION}/{TARGET FRAMEWORK}/scopedcss/projectbundle/{ASSEMBLY NAME}.bundle.scp.css 文件中。在上面的例子中,文件位于 \Demo4\obj\Debug\net7.0\scopedcss\bundle 文件夹中。
支持子组件
默认情况下,当使用 CSS 隔离时,CSS 样式只应用于当前组件。如果我们想让 CSS 样式应用于当前组件的子组件,我们需要在我们的样式中使用 ::deep 伪元素。这个伪元素选择元素标识符的子元素。
例如,以下样式将应用于当前组件内的任何 H1 标题,以及当前组件的子组件内的任何 H1 标题:
::deep h1 {
color: red;
}
如果你不想你的组件使用全局样式或者想要通过 Razor 类库共享你的组件,CSS 隔离是有用的。
重要提示
作用域 CSS 不适用于 Razor 组件。它只适用于由 Razor 组件渲染的 HTML 元素。
现在,让我们看看本章我们将要构建的项目。
创建模态对话框项目
在本章中,我们将构建一个模态对话框组件。我们将通过 Razor 标记启用模态对话框组件的 Title 和 Body 可以自定义。我们将把模态对话框组件添加到另一个组件中。
这是模态对话框的截图:

图 4.4:模态对话框
在我们完成模态对话框组件后,我们将将其移动到 Razor 类库中,以便它可以与其他项目共享。
此项目的构建时间大约为 90 分钟。
项目概述
ModalDialog 项目将通过使用 Microsoft 的 Blazor WebAssembly App 空项目模板 创建一个空的 Blazor WebAssembly 项目。我们将添加一个包含多个部分的 Dialog 组件,并使用 CSS 隔离来应用使其表现得像模态对话框的样式。我们将使用 EventCallback 参数在按钮点击时从组件向父组件通信。我们将使用 RenderFragment 参数允许 Razor 标记从父组件向组件通信。最后,我们将创建一个 Razor 类库并将 Dialog 组件移动到其中,以便模态对话框可以与其他项目共享。
项目入门
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击 创建新项目 按钮。
-
按 Alt+S 进入搜索模板文本框。
-
输入
Blazor并按 Enter 键。以下截图显示了 Blazor WebAssembly App 空项目模板。
![图形用户界面,文本,应用程序,聊天或文本消息 自动生成的描述]()
图 4.5:Blazor WebAssembly App 空项目模板
-
选择 Blazor WebAssembly App 空项目模板 并点击 下一步 按钮。
-
在 项目名称 文本框中输入
ModalDialog并点击 下一步 按钮。这是一张用于配置我们新项目的对话框截图:
![图片]()
图 4.6:配置新项目对话框
提示
在前面的示例中,我们将
ModalDialog项目放置到了E:\Blazor文件夹中。然而,这个项目的位置并不重要。 -
选择 .NET 7.0 作为要使用的 框架 版本。
-
勾选 配置为 HTTPS 复选框。
-
取消勾选 ASP.NET Core 承载 复选框。
-
取消勾选 渐进式 Web 应用程序 复选框。
这是一张用于收集关于我们新项目额外信息的对话框截图。
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 4.7:附加信息对话框
-
点击 创建 按钮。
我们已经创建了 ModalDialog Blazor WebAssembly 项目。然而,它实际上是一个空项目。让我们添加 Dialog 组件。
添加 Dialog 组件
Dialog 组件将被共享。因此,我们将将其添加到 Shared 文件夹中。我们这样做如下:
-
右键点击
ModalDialog项目并从菜单中选择 添加,新建文件夹 选项。 -
将新文件夹命名为
Shared。 -
右键点击
Shared文件夹并从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
Dialog。 -
点击 添加 按钮。
-
将
Dialog.razor文件中的标记替换为以下标记:@if (Show) { <div class="dialog-container"> <div class="dialog"> <div class="dialog-title">Title</div> <div class="dialog-body">Body</div> <div class="dialog-buttons"> <button> Ok </button> <button> Cancel </button> </div> </div> </div> } @code { [Parameter] public bool Show { get; set; } }
在前面的代码中,使用 Show 属性来显示和隐藏组件的内容。
我们已经添加了一个 Dialog 组件,但除非向项目中添加了适当的样式,否则它不会像模态对话框那样表现。
添加 CSS 文件
前面的标记包括五个我们将用于将 Dialog 组件样式化为模态对话框的类:
-
dialog-container:此类用于将元素的背景颜色设置为黑色,60% 透明度,并通过将其 z-index 设置为 2,000 将其放置在其他元素之上。 -
dialog:此类用于将元素的背景颜色设置为白色,在其父元素内水平居中,并将其宽度设置为 25 REM。 -
dialog-title:此类用于将背景颜色设置为深灰色,将文本设置为白色,并添加一些填充。 -
dialog-body:此类用于向主体添加一些填充。 -
dialog-buttons:此类用于将背景颜色设置为银色并添加一些填充。
我们需要创建一个 CSS 文件来定义如何为这些类设置样式。我们这样做如下:
-
右键单击
Shared文件夹,从菜单中选择 添加、新建项 选项。 -
在 搜索 框中输入
css。 -
选择 样式表。
-
将样式表命名为
Dialog.razor.css。 -
点击 添加 按钮。
-
将以下样式输入到
Dialog.razor.css文件中:.dialog-container { position: absolute; top: 0; bottom: 0; left: 0; right: 0; background-color: rgba(0,0,0,0.6); z-index: 2000; } .dialog { background-color: white; margin: auto; width: 25rem; } .dialog-title { background-color: #343a40; color: white; padding: .5rem; } .dialog-body { padding: 2rem; } .dialog-buttons { background-color: silver; padding: .5rem; } -
打开
wwwroot/index.html文件。 -
取消注释以下
link元素并将其添加到head元素的底部:<link href="ModalDialog.styles.css" rel="stylesheet" />
由于 CSS 隔离,Dialog.razor.cs 文件中的样式只将由 Dialog 组件使用。接下来,让我们测试 Dialog 组件。
测试 Dialog 组件
要测试 Dialog 组件,我们需要将其添加到另一个组件中。我们将将其添加到用作应用程序 Home 页面的 Index 组件中。我们这样做如下:
-
打开
_Imports.razor文件。 -
添加以下
using语句:@using ModalDialog.Shared -
打开
Pages/Index.razor文件。 -
从
Index.razor文件中删除h1元素。 -
向
Index.razor文件添加以下标记:<PageTitle>Home</PageTitle> <Dialog Show="showDialog"></Dialog> <button @onclick="OpenDialog">Show Dialog</button> @code { private bool showDialog = false; private void OpenDialog() { showDialog = true; } }
重要提示
在编辑 Index 组件时,不要从文件顶部删除 @page 指令。
-
按 Ctrl+F5 启动应用程序,不进行调试。
-
点击 显示对话框 按钮。
这是显示的模态对话框:
![形状,矩形 描述由中等置信度自动生成]()
图 4.8:示例模态对话框
-
点击 确定 按钮。
当你点击 确定 按钮时没有发生任何事情,因为我们还没有添加
@onclick事件。 -
关闭浏览器。
我们将添加几个 EventCallback 参数,以便从 Dialog 组件与 Index 组件进行通信。
添加事件回调参数
我们需要为 确定 按钮和 取消 按钮添加 @onclick 事件。我们这样做如下:
-
返回 Visual Studio。
-
打开
Shared/Dialog.razor文件。 -
添加如下高亮显示的代码指示的每个按钮的
@onclick事件:<button **@****onclick****=****"OnOk"**> OK </button> <button **@****onclick****=****"OnCancel"**> Cancel </button> -
向代码块添加以下参数:
[Parameter] public EventCallback<MouseEventArgs> OnOk { get; set; } [Parameter] public EventCallback<MouseEventArgs> OnCancel { get; set; }
提示
Parameter属性不需要与它应用的属性在同一行上。
-
打开
Pages/Index.razor文件。 -
通过添加以下高亮标记来更新
Dialog元素的标记:<Dialog Show="showDialog" **OnCancel****=****"DialogCancelHandler"** **OnOk****=****"DialogOkHandler"****>** </Dialog> -
将以下方法添加到代码块中:
private void DialogCancelHandler(MouseEventArgs e) { showDialog = false; } private void DialogOkHandler(MouseEventArgs e) { showDialog = false; }
提示
由于e在前面方法中没有使用,我们不需要在方法定义中指定MouseEventArgs。我们包括它是为了演示目的。
-
按Ctrl+F5启动应用程序而不进行调试。
-
点击显示对话框按钮。
-
点击确定按钮。
当你点击确定按钮时,对话框将关闭。
现在让我们更新Dialog组件,以便我们可以自定义它创建的模态对话框的Title和Body属性。
添加RenderFragment参数
我们将为Dialog组件的Title和Body属性使用RenderFragment参数。我们这样做如下:
-
返回 Visual Studio。
-
打开
Shared/Dialog.razor文件。 -
将
dialog-title的标记更新为以下内容:<div class="dialog-title">**@Title**</div> -
将
dialog-body的标记更新为以下内容:<div class="dialog-body">**@Body**</div> -
将以下参数添加到代码块中:
[Parameter] public RenderFragment Title { get; set; } [Parameter] public RenderFragment Body { get; set; } -
打开
Pages/Index.razor文件。 -
将
Dialog元素的标记更新为以下内容:<Dialog Show="showDialog" OnCancel="DialogCancelHandler" OnOk="DialogOkHandler"> **<****Title****>****Quick List [@(Items.Count + 1)]****</****Title****>** **<****Body****>** **Enter New Item:** **<****input** **@****bind****=****"****NewItem"** **/>** **</****Body****>** </Dialog>之前的标记将对话框的标题更改为
快速列表并为用户提供一个文本框来输入列表项。 -
在
Dialog元素下添加以下标记:<ol> @foreach (var item in Items) { <li>@item</li> } </ol>之前的代码将在有序列表中显示
Items列表中的每个项目。 -
在代码块顶部添加以下变量:
private string? NewItem; private List<string> Items = new List<string>(); -
将
DialogCancelHandler更新为以下内容:private void DialogCancelHandler(MouseEventArgs e) { **NewItem =** **""****;** showDialog = false; }之前的代码将清除文本框并隐藏
Dialog组件的内容。 -
将
DialogOkHandler更新为以下内容:private void DialogOkHandler(MouseEventArgs e) { **if** **(!****string****.IsNullOrEmpty(NewItem))** **{** **Items.Add(NewItem);** **NewItem =** **""****;** **};** showDialog = false; }之前的代码将
NewItem添加到Items列表中,清除文本框,并隐藏Dialog组件的内容。 -
按Ctrl+F5启动应用程序而不进行调试。
-
点击显示对话框按钮。
-
在输入新项字段中输入一些文本。
-
点击确定按钮。
-
重复。
每次点击确定按钮,输入新项字段中的文本将被添加到列表中。以下截图显示了一个已添加三个项目并即将使用模态对话框添加第四个项目的列表:
![图形用户界面,文本,应用程序 自动生成的描述]()
图 4.9:示例快速列表
-
关闭浏览器。
要将此新组件与其他项目共享,我们需要将其添加到 Razor 类库中。
创建 Razor 类库
我们可以通过使用 Razor 类库在项目之间共享组件。要创建 Razor 类库,我们将使用Razor 类库项目模板。我们这样做如下:
-
右键单击解决方案,从菜单中选择添加、新建项目选项。
-
在搜索模板文本框中输入
Razor 类库以定位Razor 类库项目模板。以下截图显示了 Razor 类库 项目模板:
![图形用户界面、文本、聊天或文本消息 自动生成的描述]()
图 4.10:Razor 类库项目模板
-
选择 Razor 类库 项目模板。
-
点击 下一步 按钮。
-
将项目命名为
MyComponents并点击 下一步 按钮。 -
选择 .NET 7.0 作为要使用的 框架 版本。
-
取消选择 支持页面和视图 复选框。
-
点击 创建 按钮。
-
右键单击
ModalDialog项目,并从菜单中选择 添加、项目引用 选项。 -
选择
MyComponents复选框并点击 确定 按钮。
我们已经创建了 MyComponents Razor 类库,并且从 ModalDialog 项目中添加了对它的引用。让我们测试它。
测试 Razor 类库
我们刚刚使用项目模板创建的 MyComponents Razor 类库包含一个组件,称为 Component1。在我们继续之前,我们需要测试新的 Razor 类库是否正常工作。我们这样做如下:
-
打开
ModalDialog.Pages/Index.razor文件。 -
在
@page指令下方添加以下using语句:@using MyComponents;提示
如果你将在多个页面上使用此项目,你应该考虑将
using语句添加到ModalDialog._Imports.razor文件中,这样你就不需要在每个使用它的组件中包含它。 -
在
PageTitle元素下方添加以下标记:<Component1 /> -
按 Ctrl+F5 以不带调试启动应用程序。
以下截图显示了
Component1组件应该如何渲染:![包含形状的图片 自动生成的描述]()
图 4.11:Component1
重要提示
如果
Component1组件缺少样式,那是因为 CSS 文件被缓存了。使用以下快捷键组合,Ctrl+Shift+R,来清空缓存并重新加载页面。 -
关闭浏览器。
-
返回 Visual Studio。
-
从
Index组件中删除Component1元素。
我们已经完成了 MyComponents Razor 类库的测试。现在,是时候将我们的自定义 Dialog 组件添加到 MyComponents Razor 类库中。
向 Razor 类库添加一个组件
要共享 Dialog 组件,我们需要将其移动到我们刚刚创建和测试的 Razor 类库中。我们这样做如下:
-
右键单击
ModalDialog.Shared/Dialog.razor文件,并从菜单中选择 复制 选项。 -
右键单击
MyComponents项目,并从菜单中选择 粘贴 选项。 -
右键单击
MyComponents.Dialog.razor文件,并从菜单中选择 重命名 选项。 -
重命名文件
BweDialog.razor。在这种情况下,
Bwe代表 Blazor WebAssembly by Example。提示
在命名 Razor 类库中的组件时,你应该给它们唯一的名称以避免模糊引用错误。大多数组织都会在所有共享组件前加上相同的文本。例如,名为 One Stop Designs (OSD) 的公司可能会在所有共享组件前加上
Osd。 -
打开
ModalDialog.Pages/Index.razor文件。 -
将
Dialog元素重命名为BweDialog。 -
按 Ctrl+F5 以无调试模式启动应用程序。
-
点击 Show Dialog 按钮。
-
在 Enter New Item 字段中输入一些文本。
-
点击 Ok 按钮。
-
重复。
BweDialog 组件现在正从 MyComponents Razor 类库中使用。由于 BweDialog 组件包含在 Razor 类库中,它可以很容易地与其他项目共享。
摘要
现在,你应该能够通过使用 Razor 类库创建一个模态对话框,并将其与多个项目共享。
在本章中,我们介绍了 RenderFragment 参数、EventCallback 参数和 CSS 隔离。
之后,我们使用了 Blazor WebAssembly App Empty 项目模板来创建一个新的项目。我们创建了一个 Dialog 组件,它就像一个模态对话框。Dialog 组件使用 RenderFragment 参数和 EventCallback 参数在它与其父组件之间共享信息。此外,它还使用 CSS 隔离来设置样式。
在本章的最后部分,我们创建了一个 Razor 自定义库,并将 Dialog 组件移动到了新的库中。
到目前为止,在这本书中,我们避免使用 JavaScript。不幸的是,还有一些功能我们只能通过 JavaScript 来完成。在这本书的下一章中,我们将学习如何使用 JavaScript interop 在 Blazor WebAssembly 应用中调用 JavaScript。
问题
以下问题供您思考:
-
你如何用模板组件替换表格?
-
你会如何为
Dialog组件的Title属性和Body属性添加默认值? -
在处理
@onclick事件时,你如何确定哪个按钮被点击了? -
你能否使用 NuGet 包分发你的
Dialog组件?
进一步阅读
以下资源提供了关于本章主题的更多信息:
-
关于 CSS 的更多信息,请参阅
www.w3schools.com/css/default.asp。 -
关于 lambda 表达式的更多信息,请参阅
learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions。 -
关于 ASP.NET Core Razor 组件类库的更多信息,请参阅
learn.microsoft.com/en-us/aspnet/core/blazor/components/class-libraries。 -
关于 NuGet 的更多信息,请参阅
www.nuget.org。
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第五章:使用 JavaScript 互操作性(JS Interop)构建本地存储服务
Blazor WebAssembly 框架使我们能够在浏览器上运行 C# 代码。然而,有一些场景 C# 简直无法处理,对于这些场景,我们需要使用 JavaScript 函数。
在本章中,我们将学习如何使用 JavaScript 与 Blazor WebAssembly。我们将学习如何从 .NET 方法中调用 JavaScript 函数,无论是否有返回值。相反,我们还将学习如何从 JavaScript 函数中调用 .NET 方法。我们将通过使用 JavaScript 互操作性(JS Interop)来完成这两种场景。最后,我们将学习如何通过使用 JavaScript 的 Web Storage API 在浏览器上存储数据。
本章我们将创建的项目将是一个本地存储服务,该服务将读取和写入应用程序的本地存储。为了访问应用程序的本地存储,我们将使用 JavaScript。我们还将创建一个测试组件来测试本地存储服务。测试组件将使用 JavaScript 在 JavaScript 警告框中显示文本。
很遗憾,但这是真的。
我们可能不喜欢 JavaScript,
但我们仍然需要它!
在本章中,我们将涵盖以下主题:
-
为什么使用 JavaScript?
-
探索 JS 互操作性
-
使用本地存储
-
创建本地存储服务
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 的免费社区版的说明,请参阅 第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter05。
代码在行动视频在此处可用:packt.link/Ch5。
为什么使用 JavaScript?
使用 Blazor WebAssembly,我们可以创建无需编写任何 JavaScript 的健壮应用程序。然而,有一些场景需要使用 JavaScript。例如,我们可能有一个我们想要继续使用的喜欢的 JavaScript 库。此外,没有 JavaScript,我们无法操作 DOM 或调用任何 JavaScript API。
这是我们无法直接从 Blazor WebAssembly 框架访问的部分列表:
-
DOM 操作
-
媒体捕获和流 API
-
WebGL API(网页的 2D 和 3D 图形)
-
Web Storage API(
localStorage和sessionStorage) -
地理位置 API
-
JavaScript 弹出框(一个警告框、一个确认框和一个提示框)
-
浏览器的在线状态
-
浏览器的历史记录
-
Chart.js
-
其他第三方 JavaScript 库
前面的列表并不全面,因为目前有数百个可用的 JavaScript 库。然而,需要记住的关键点是,我们不使用 JavaScript 就不能操作 DOM。因此,我们可能始终需要在我们的 Web 应用程序中使用一些 JavaScript。幸运的是,通过使用 JS interop,这很容易做到。
探索 JS interop
要从 .NET 调用 JavaScript 函数,我们使用 IJSRuntime 抽象。这个抽象表示框架可以调用的 JavaScript 运行时实例。要使用 IJSRuntime,我们必须首先通过依赖注入将其注入到我们的组件中。有关依赖注入的更多信息,请参阅第七章,使用应用程序状态构建购物车。
@inject 指令用于将依赖项注入到组件中。以下代码将 IJSRuntime 注入到当前组件中:
@inject IJSRuntime js
IJSRuntime 抽象有两个我们可以用来调用 JavaScript 函数的方法:
-
InvokeAsync
-
InvokeVoidAsync
这两种方法都是异步的。这两种方法之间的区别在于,其中一种方法返回一个值,而另一种方法不返回。我们可以将 IJSRuntime 的实例向下转换为 IJSInProcessRuntime 的实例,以同步运行该方法。最后,我们可以通过使用 JsInvokable 装饰器从 JavaScript 中调用 .NET 方法。我们将在本章后面查看这些方法的示例。
然而,在我们能够调用 JavaScript 方法之前,我们需要将 JavaScript 加载到我们的应用程序中。
加载 JavaScript 代码
将 JavaScript 代码加载到 Blazor WebAssembly 应用程序中有几种方法。一种方法是将 JavaScript 代码直接输入到 wwwroot/index.html 文件中的 body 元素的 script 元素中。然而,我们建议不要直接将 JavaScript 代码输入到 .html 文件中,而是使用外部 JavaScript 文件来存放你的 JavaScript 函数。
我们可以通过在 wwwroot./index.html 文件中引用它来添加外部文件。以下代码引用了位于 wwwroot/scripts 文件夹中的名为 btwInterop.js 的文件:
<script src="img/bweInterop.js"></script>
组织脚本的一个更好的方法是将与特定组件关联的外部 JavaScript 文件放置在一起。要添加与特定组件关联的 JavaScript 文件,请创建一个与组件同名的 JavaScript 文件,但文件扩展名是 JavaScript。例如,定义在 MyComponent.razor 文件中的 MyComponent 组件将使用 MyComponent.razor.js 作为其关联的 JavaScript 文件。
为了使组件能够引用 JavaScript 文件中的代码,必须在组件的 OnAfterRenderAsync 方法中导入该文件。以下示例中使用了 import 标识符来导入一个 JavaScript 文件:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await js.InvokeAsync<IJSObjectReference>
("import", "./Pages/MyComponent.razor.js");
}
}
在前面的代码中,正在导入的 JavaScript 文件位于 Pages 文件夹中,文件名为 MyComponent.razor.js。
重要提示
当应用程序发布时,同站 JavaScript 文件将被自动移动到 wwwroot 文件夹。这种方法称为 JavaScript 隔离,它使得下载 JavaScript 文件变得延迟。
前面的代码使用 IJSRuntime 的 InvokeAsync 方法从 .NET 调用 JavaScript 的 import 函数。
从 .NET 方法调用 JavaScript 函数
我们可以使用两种不同的 IJSRutime 方法从 .NET 异步调用 JavaScript:
-
InvokeAsync -
InvokeVoidAsync
要从 .NET 同步调用 JavaScript 函数,IJSRutime 必须被转换为 IJSInProcessRuntime。
InvokeAsync
InvokeAsync 方法是一个异步方法,用于调用返回值的 JavaScript 函数。
这是 IJSRuntime 的 InvokeAsync 方法:
ValueTask<TValue> InvokeAsync<TValue>(string identifier,
params object[] args);
在前面的代码中,第一个参数是 JavaScript 函数的标识符,第二个参数是一个 JSON-序列化参数的数组。第二个参数是可选的。InvokeAsync 方法返回一个 ValueTask,其类型为 TValue。TValue 是 JavaScript 返回值的 JSON-反序列化实例。
在 JavaScript 中,Window 对象代表浏览器的窗口。为了确定当前窗口的宽度和高度,我们使用 Window 对象的 innerWidth 和 innerHeight 属性。
以下 JavaScript 代码包含一个名为 getWindowSize 的方法,该方法返回 Window 对象的宽度和高度:
wwwroot/bweInterop.js
var bweInterop = {};
bweInterop.getWindowSize = function () {
var size = {
width: window.innerWidth,
height: window.innerHeight
}
return size;
}
重要提示
在这本书中,我们将使用 bweInterop 命名空间来组织我们的 JavaScript 代码,并最小化命名冲突的风险。
这是用于在 .NET 中存储窗口大小的 WindowSize 类的定义:
public class WindowSize
{
public int? Width { get; set; }
public int? Height { get; set; }
}
以下 Index 组件从 bweInterop.js 文件中调用 GetWindowSize 方法:
Pages/Index.razor
@page "/"
@inject IJSRuntime js
<PageTitle>Home</PageTitle>
@if (windowSize.Width != null)
{
<h2>
Window Size: @windowSize.Width x @windowSize.Height
</h2>
}
<button @onclick="GetWindowSize">Get Window Size</button>
@code {
private WindowSize windowSize = new WindowSize();
private async Task GetWindowSize()
{
windowSize = await js.InvokeAsync<WindowSize>(
"bweInterop.getWindowSize");
}
}
在前面的代码中,IJSRuntime 被注入到组件中。当点击 获取窗口大小 按钮时,GetWindowSize 方法使用 IJSRuntime 的 InvokeAsync 方法调用 getWindowSize JavaScript 函数。GetWindowSize JavaScript 函数将窗口的宽度和高度返回到 windowSize 属性。最后,组件重新生成其渲染树并将任何更改应用到浏览器的 DOM 上。
这是点击 获取窗口大小 按钮后页面的截图:

图 5.1:窗口大小示例
IJSRuntime 的 InvokeSync 方法用于调用返回值的 JavaScript 函数。如果我们不需要返回值,可以使用 InvokeAsync 方法代替。
InvokeVoidAsync
InvokeVoidAsync 方法是一个异步方法,用于调用不返回值的 JavaScript 函数。
这是 IJSRuntime 的 InvokeVoidAsync 方法:
InvokeVoidAsync(string identifier, params object[] args);
就像 InvokeAsync 方法一样,第一个参数是被调用的 JavaScript 函数的标识符,第二个参数是一个 JSON-serializable 参数数组。第二个参数是可选的。
在 JavaScript 中,Document 对象代表 HTML 文档的根节点。Document 对象的 title 属性用于指定出现在浏览器标题栏中的文本。假设我们想在 Blazor WebAssembly 应用程序中的组件之间导航时更新浏览器的标题。为此,我们需要使用 JavaScript 来更新 title 属性。
下面的 JavaScript 代码导出一个名为 setDocumentTitle 的函数,该函数将 Document 对象的 title 属性设置为 title 参数提供的值:
Shared/Document.razor.js
export function setDocumentTitle(title) {
document.title = title;
}
上述代码使用 export 语句导出 setDocumentTitle 函数。
重要提示
JavaScript 中的 export 语句用于从 JavaScript 导出函数,以便导入到其他程序中。
下面的 Document 组件使用 setDocumentTitle JavaScript 函数来更新浏览器的标题栏:
Shared/Document.razor
@inject IJSRuntime js
@code {
[Parameter] public string Title { get; set; } = "Home";
protected override async Task OnAfterRenderAsync
(bool firstRender)
{
if (firstRender)
{
IJSObjectReference module =
await js.InvokeAsync<IJSObjectReference>
("import", "./Shared/Document.razor.js");
await module.InvokeVoidAsync
("setDocumentTitle", Title);
}
}
}
在上述代码中,IJSRuntime 被注入到组件中。然后,OnAfterRenderAsync 方法使用 InvokeAsync 方法导入 JavaScript 代码,并使用 InvokeVoidAsync 方法调用 setDocumentTitle JavaScript 函数。
重要提示
我们在并置的 JavaScript 代码中未使用 bweInterop 命名空间,以强调它只被一个组件引用。
下面的标记使用 Document 组件将浏览器的标题栏更新为 Home – My App:
<Document Title="Home - My App" />
下面的截图显示了生成的文档标题:

图 5.2:更新的文档标题
提示
您可以使用内置的 PageTitle 组件来设置页面的标题。
默认情况下,JS 互操作调用是异步的。要执行同步 JS 互操作调用,我们需要使用 IJSInProcessRuntime。
IJSInProcessRuntime
到目前为止,在本章中,我们只看了异步调用 JavaScript 函数。但我们也可以同步调用 JavaScript 函数。我们通过将 IJSRuntime 降级为 IJSInProcessRuntime 来实现这一点。IJSInProcessRuntime 允许我们的 .NET 代码同步调用 JS 互操作调用。这可以是有益的,因为这些调用比它们的异步对应物开销更小。
这些是 IJsInProcessRuntime 的同步方法:
-
Invoke -
InvokeVoid
以下代码使用 IJSInProcessRuntime 来同步调用一个 JavaScript 函数:
@inject IJSRuntime js
@code {
private string GetGuid()
{
string guid =
((IJSInProcessRuntime)js).Invoke<string>("getGuid");
return guid;
}
}
在上述代码中,IJsRuntime 实例已被降级为 IJSInProcessRuntime 实例。IJSInProcessRuntime 实例的 Invoke 方法用于调用 getGuid JavaScript 方法。
IJSRuntime 抽象提供了从 .NET 方法直接调用 JavaScript 函数的方法。它们可以是异步调用或同步调用。直接从 JavaScript 函数调用 .NET 方法需要一个特殊的属性。
从 JavaScript 函数调用 .NET 方法
我们可以通过使用 JSInvokable 属性装饰方法,从 JavaScript 调用公共 .NET 方法。
以下 .NET 方法被 JSInvokable 属性装饰,以便可以从 JavaScript 调用:
private WindowSize windowSize = new WindowSize();
[JSInvokable]
public void GetWindowSize(WindowSize newWindowSize)
{
windowSize = newWindowSize;
StateHasChanged();
}
在前面的代码中,每次从 JavaScript 调用 GetWindowSize 方法时,都会更新 windowSize 属性。在 windowSize 属性更新后,组件的 StateHasChanged 方法被调用,以通知组件其状态已更改,因此组件应该重新渲染。
重要提示
组件的 StateHasChanged 方法仅在 EventCallback 方法中自动调用。在其他情况下,必须手动调用以通知 UI 它可能需要重新渲染。
要从 JavaScript 调用 .NET 方法,我们必须为 JavaScript 创建一个 DotNetObjectReference 类,以便用于定位 .NET 方法。DotNetObjectReference 类包装了一个 JS 互操作参数,表示该值不应序列化为 JSON,而应作为引用传递。
重要提示
为了避免内存泄漏并允许对创建 DotNetObjectReference 类的组件进行垃圾回收,你必须勤奋地销毁每个 DotNetObjectReference 实例。
以下代码创建了一个包装 Resize 组件的 DotNetObjectReference 实例。然后,该引用被传递到 JavaScript 方法中:
private DotNetObjectReference<Resize> objRef;
protected async override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
objRef = DotNetObjectReference.Create(this);
await js.InvokeVoidAsync(
"bweInterop.registerResizeHandler",
objRef);
}
}
我们可以使用 DotNetObjectReference 创建的组件引用从 JavaScript 调用 .NET 组件中的方法。在下面的 JavaScript 中,registerResizeHandler 函数创建了一个在初始化时调用,并且每次窗口调整大小时都会被调用的 resizeHandler 函数。
bweInterop.js
bweInterop.registerResizeHandler = function (dotNetObjectRef) {
function resizeHandler() {
dotNetObjectRef.invokeMethodAsync('GetWindowSize',
{
width: window.innerWidth,
height: window.innerHeight
});
};
resizeHandler();
window.addEventListener("resize", resizeHandler);
}
在前面的示例中,使用了 invokeMethodAsync 函数来调用被 JSInvokable 属性装饰的 GetWindowSize .NET 方法。
提示
你可以使用 invokeMethod 函数或 invokeMethodAsync 函数从 JavaScript 调用 .NET 实例方法。
这是 Resize 组件的完整 .NET 代码:
Resize.razor
@page "/resize"
@inject IJSRuntime js
@implements IDisposable
<PageTitle>Resize</PageTitle>
@if (windowSize.Width != null)
{
<h2>
Window Size: @windowSize.Width x @windowSize.Height
</h2>
}
@code {
private DotNetObjectReference<Resize> objRef;
private WindowSize windowSize = new WindowSize();
protected async override Task OnAfterRenderAsync(
bool firstRender)
{
if (firstRender)
{
objRef = DotNetObjectReference.Create(this);
await js.InvokeVoidAsync(
"bweInterop.registerResizeHandler",
objRef);
}
}
[JSInvokable]
public void GetWindowSize(WindowSize newWindowSize)
{
windowSize = newWindowSize;
StateHasChanged();
}
public void Dispose()
{
objRef?.Dispose();
}
}
Resize 组件的前置代码显示了浏览器当前宽度和高度。当你调整浏览器大小时,显示的值会自动更新。此外,当组件被销毁时,DotNetObjectReference 对象也会被销毁。要测试 Resize 组件,请按 Ctrl+F5 启动应用程序,不进行调试。应用程序启动后,导航到 /resize 页面并调整窗口大小。
IJSRuntime 抽象为我们提供了一种从 .NET 调用 JavaScript 函数以及从 JavaScript 调用 .NET 方法的方式。
我们将使用 JavaScript 的 Web Storage API 来完成本章的项目。但在我们能够使用它之前,我们需要了解它是如何工作的。
使用本地存储
JavaScript 的 Web Storage API 为浏览器提供了存储键/值对的机制。对于每个网络浏览器,可以在 Web Storage 中存储的数据大小至少为每个来源 5 MB。localStorage 机制在 JavaScript 的 Web Storage API 中定义。我们需要使用 JS interop 来访问应用程序的本地存储,因为 Web Storage API 需要使用 JavaScript。
应用程序的本地存储限于特定的 URL。如果用户重新加载页面或关闭并重新打开浏览器,本地存储的内容将保留。如果用户打开多个标签,每个标签共享相同的本地存储。本地存储中的数据在明确清除之前会保留,因为它没有过期日期。
重要提示
使用 InPrivate 窗口或 Incognito 窗口创建的 localStorage 对象中的数据,在最后一个标签关闭时会被清除。
这些是 localStorage 的方法:
-
key:此方法根据其在
localStorage中的位置返回指定键的名称。 -
getItem:此方法从
localStorage返回指定键的值。 -
setItem:此方法接受一个键和值对,并将它们添加到
localStorage。 -
removeItem:此方法从
localStorage中删除指定的键。 -
clear:此方法清除
localStorage。
重要提示
sessionStorage 也在 Web Storage API 中定义。与在多个浏览器标签间共享其值的 localStorage 不同,sesssionStorage 仅限于单个浏览器标签。因此,如果用户重新加载页面,数据会持续存在,但如果用户关闭标签(或浏览器),数据将被清除。
要查看应用程序本地存储的内容,请按 F12 打开浏览器开发者工具,并选择 Application 选项卡。在左侧菜单的 Storage 部分选择 Local Storage。以下截图显示了 Microsoft Edge 中 DevTools 对话框的 Application 选项卡:

图 5.3:本地存储
通过使用 Web Storage API,在浏览器中存储数据并检索它非常容易。现在,让我们快速了解一下本章将要构建的项目。
创建本地存储服务
在本章中,我们将构建一个本地存储服务。该服务将同时写入和读取应用程序的本地存储。我们将使用 JS 互操作来实现这一点。我们将使用InvokeVoidAsync方法写入本地存储,使用InvokeAsync方法从本地存储读取。最后,我们将创建一个组件来测试我们的服务。
测试组件将读取和写入本地存储。它将使用 JS 互操作在 JavaScript 警告框中显示本地存储的内容。
以下截图显示了测试组件和应用程序的本地存储。当点击保存到本地存储按钮时,localStorageData文本框中的值将保存到本地存储。

图 5.4:本地存储服务测试页面
当点击从本地存储读取按钮时,localStorageData的值将在 JavaScript 警告框中显示。以下截图显示了显示本地存储值的警告示例:

图 5.5:从本地存储读取
此项目的构建时间大约为 60 分钟。
项目概述
使用 Microsoft 的Blazor WebAssembly App Empty项目模板创建一个空的 Blazor WebAssembly 项目,将创建LocalStorage项目。首先,我们将添加一个 JavaScript 文件,其中包含我们的服务将需要使用的 JavaScript 函数来更新应用程序的本地存储。接下来,我们将创建接口和类,其中包含将调用 JavaScript 函数的.NET 方法。最后,我们将通过添加一个本地 JavaScript 文件来测试我们的服务。
项目入门
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击创建新项目按钮。
-
按Alt+S进入搜索模板文本框。
-
输入
Blazor并按Enter键。以下截图显示了Blazor WebAssembly App Empty项目模板:
![图形用户界面,文本,应用程序,聊天或文本消息 自动生成的描述]()
图 5.6:Blazor WebAssembly App Empty 项目模板
-
选择Blazor WebAssembly App Empty项目模板并点击下一步按钮。
-
在项目名称文本框中输入
LocalStorage并点击下一步按钮。这是我们配置新项目所使用的对话框的截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 5.7:配置新项目对话框
提示
在前面的示例中,我们将
LocalStorage项目放置在E:/Blazor文件夹中。然而,此项目的位置并不重要。 -
选择.NET 7.0作为要使用的框架版本。
-
选中配置为 HTTPS复选框。
-
取消选中ASP.NET Core 托管复选框。
-
取消选中渐进式 Web 应用程序复选框。
这是用于收集有关我们新项目额外信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 5.8:附加信息对话框
-
点击创建按钮。
我们已经创建了一个空的LocalStorage Blazor WebAssembly 项目。现在,让我们开始添加我们将需要的用于读取和写入本地存储的 JavaScript 函数。
编写 JavaScript 以访问本地存储
我们需要编写将读取和写入应用程序本地存储的 JavaScript 函数。我们这样做如下:
-
右键点击
wwwroot文件夹,从菜单中选择添加,新建文件夹选项。 -
将新文件夹命名为
scripts。 -
右键点击
scripts文件夹,从菜单中选择添加,新建项选项。 -
在搜索框中输入
javascript。 -
选择JavaScript 文件。
-
将 JavaScript 文件命名为
bweInterop.js。 -
点击添加按钮。
-
输入以下 JavaScript:
var bweInterop = {}; bweInterop.setLocalStorage = function (key, data) { localStorage.setItem(key, data); } bweInterop.getLocalStorage = function (key) { return localStorage.getItem(key); }上述 JavaScript 包含一个
setLocalStorage函数,用于写入本地存储,以及一个getLocalStorage函数,用于从本地存储读取。 -
打开
wwwroot/index.html文件。 -
在
body元素的底部添加以下标记:<script src="img/bweInterop.js"></script>
重要提示
在wwwroot/index.html文件中,引用您自定义 JavaScript 的script标签应该位于 Blazor 脚本引用之后。
现在我们需要添加将调用这些 JavaScript 函数的.NET 代码。首先,我们将为我们的服务创建接口。
添加 ILocalStorageService 接口
我们需要为我们的服务创建一个接口。我们这样做如下:
-
右键点击
LocalStorage项目,从菜单中选择添加,新建文件夹选项。 -
将新文件夹命名为
Services。 -
右键点击Services文件夹,然后从菜单中选择添加,新建项选项。
-
在搜索框中输入
interface。 -
选择接口。
-
将文件命名为
ILocalStorageService。 -
点击添加按钮。
-
将以下代码添加到
ILocalStorageService接口中:ValueTask SetItemAsync<T>(string key, T item); ValueTask<T?> GetItemAsync<T>(string key);上述方法将用于设置本地存储的值。
-
打开
Program.cs文件。 -
添加以下
using语句:using LocalStorage.Services; -
在注册
HttpClient的代码之后添加以下代码:builder.Services.AddScoped <ILocalStorageService, LocalStorageService>();
上述代码将LocalStorageService注册到依赖注入容器中。有关依赖注入的更多信息,请参阅第七章,使用应用程序状态构建购物车。
我们已经定义了服务的抽象方法并将其注册到应用程序中。现在,是时候创建LocalStorageService类了。
创建 LocalStorageService 类
我们需要基于我们刚刚创建的接口创建一个新的类。我们这样做如下:
-
右键单击Services文件夹,从菜单中选择添加,类选项。
-
将新类命名为
LocalStorageService。 -
将
LocalStorageService类更新为从ILocalStorageService继承:public class LocalStorageService : ILocalStorageService -
将以下代码添加到
LocalStorageService类中:private IJSRuntime js; public LocalStorageService(IJSRuntime JsRuntime) { js = JsRuntime; }上述代码定义了
LocalStorageService类的构造函数。 -
将
SetItemAsync方法添加到LocalStorageService类中:public async ValueTask SetItemAsync<T>(string key, T item) { await js.InvokeVoidAsync( "bweInterop.setLocalStorage", key, JsonSerializer.Serialize(item)); }SetItemAsync方法使用键和要存储在localStorage中的项的序列化版本调用bweInterop.setLocalStorageJavaScript 函数。 -
将
GetItemAsync方法更新为以下内容:public async ValueTask<T?> GetItemAsync<T>(string key) { var json = await js.InvokeAsync<string> ("bweInterop.getLocalStorage", key); return JsonSerializer.Deserialize<T>(json); }GetItemAsync方法使用键调用bweInterop.getLocalStorageJavaScript 函数。如果bweInterop.getLocalStorage返回一个值,则该值将被反序列化并返回。
我们已经完成了我们的服务。现在我们需要对其进行测试。
创建DataInfo类
DataInfo类将用于存储我们从应用程序的本地存储中读取和写入的数据:
-
右键单击
LocalStorage项目,从菜单中选择添加,新文件夹选项。 -
将新文件夹命名为
Models。 -
右键单击Models文件夹,从菜单中选择添加,类选项。
-
将新类命名为
DataInfo。 -
将以下属性添加到
DataInfo类中:public string? Value { get; set; } public int Length { get; set; } public DateTime Timestamp { get; set; }
DataInfo类包括数据、关于数据长度的信息,以及数据更新的日期和时间。
现在我们已经定义了一个对象来存储我们的数据,是时候测试将数据写入应用程序的本地存储了。
写入本地存储
我们需要使用我们的本地存储服务测试向应用程序的本地存储写入。我们这样做如下:
-
打开
Pages/Index.razor文件。 -
删除
H1元素。 -
添加以下指令:
@using LocalStorage.Services -
添加以下标记:
<PageTitle>Local Storage Service</PageTitle> <h2>Local Storage Service</h2> localStorageData: <input type="text" @bind-value="data" size="25" /> <hr /> <button @onclick="SaveToLocalStorageAsync"> Save to Local Storage </button>上述标记添加了一个文本框,用于输入要保存到应用程序本地存储的数据,以及一个按钮来调用
SaveToLocalStorageAsync方法。 -
右键单击Pages文件夹,从菜单中选择添加,类选项。
-
将新类命名为
Index.razor.cs。 -
通过添加
partial关键字将类转换为部分类:Public **partial** class Index -
将以下内容添加到代码中:
[Inject] ILocalStorageService? localStorage { get; set; } private string? data; async Task SaveToLocalStorageAsync() { var dataInfo = new DataInfo() { Value = data, Length = data!.Length, Timestamp = DateTime.Now }; await localStorage!.SetItemAsync<DataInfo?>( "localStorageData", dataInfo); }上述代码将
LocalStorageService注入到组件中,并定义了SaveToLocalStorageAsync方法。SaveToLocalStorageAsync方法在将数据保存到localStorage时使用localStorageData作为键。 -
按Ctrl+F5以不带调试启动应用程序。

图 5.9:本地存储服务测试页面
-
在localStorageData文本框中输入单词
Test。 -
点击保存到本地存储按钮。
-
按F12打开浏览器的开发者工具。
-
选择应用程序选项卡。
-
打开
Local Storage。 -
在localStorageData文本框中输入不同的单词。
-
点击保存到本地存储按钮。
-
验证应用程序的本地存储是否已更新。
-
关闭浏览器。
我们已经使用 Web Storage API 将数据保存到应用程序的本地存储中。接下来,我们需要学习如何从应用程序的本地存储中读取。由于我们将在 JavaScript 弹窗中显示数据,我们需要添加一些 JavaScript 代码来调用弹窗函数。
添加一个本地 JavaScript 文件
我们需要添加一个本地 JavaScript 文件来包含将要调用弹窗函数的 JavaScript 代码。我们这样做如下:
-
返回 Visual Studio。
-
右键单击 Pages 文件夹,从菜单中选择 添加,新建项 选项。
-
在 搜索 框中输入
javascript。 -
选择 JavaScript 文件。
-
将 JavaScript 文件命名为
Index.razor.js。 -
将以下 JavaScript 添加到
Index.razor.js文件中:export function showLocalStorage(data) { alert(data); }前面的代码导出了
showLocalStorage函数,该函数打开一个弹窗,包含由data参数指定的文本。 -
打开
Pages/Index.razor.cs文件。 -
通过添加以下代码将
IJSRuntime实例注入到Index组件中:[Inject] IJSRuntime js { get; set; } -
添加以下属性:
private IJSObjectReference? module; -
添加
OnAfterRenderAsync方法:protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { module = await js.InvokeAsync<IJSObjectReference> ("import", "./Pages/Index.razor.js"); } }
位于 Pages/Index.razor.js 文件中的 JavaScript 函数现在可以从 Index 组件中调用。
从本地存储读取
我们需要使用我们的本地存储服务来测试从应用程序的本地存储读取。我们这样做如下:
-
打开
Pages/Index.razor文件。 -
在现有按钮下方添加以下按钮:
<button @onclick="ReadFromLocalStorageAsync"> Read from Local Storage </button>前面的标记添加了一个调用
ReadFromLocalStorageAsync方法的按钮。 -
打开
Pages/Index.razor.cs文件。 -
添加
ReadFromLocalStorageAsync方法:async Task ReadFromLocalStorageAsync() { if (module is not null) { DataInfo? savedData = await localStorage!.GetItemAsync <DataInfo>("localStorageData"); string result = $"localStorageData = {savedData!.Value}"; await module.InvokeVoidAsync ("showLocalStorage", result); } }ReadFromLocalStorageAsync方法在访问应用程序的本地存储时使用localStorageData键。 -
按 Ctrl+F5 以不带调试启动应用程序。
-
点击 从本地存储读取 按钮。
-
验证弹窗的内容与应用程序本地存储的内容是否匹配。
ReadFromLocalStorage 方法在本地 JavaScript 文件中调用了 showLocalStorage 函数。我们现在已经完成了本地存储服务的测试。
摘要
你现在应该能够通过使用 JS 互操作来从你的 Blazor WebAssembly 应用程序调用 JavaScript 函数来创建一个本地存储服务。
在本章中,我们解释了为什么你可能仍然需要使用 JavaScript,以及如何使用 IJSRuntime 抽象从 .NET 异步和同步地调用 JavaScript 函数。相反,我们解释了如何从 JavaScript 函数调用 .NET 方法。最后,我们解释了如何使用应用程序的本地存储在浏览器中存储数据。
之后,我们使用了Blazor WebAssembly App Empty项目模板来创建一个新的项目。我们添加了一些 JavaScript 函数来读取和写入应用程序的本地存储。然后,我们添加了一个类来调用这些 JavaScript 函数。在章节的最后部分,我们通过添加一个打开 JavaScript 警告框的 JavaScript 文件来测试我们的本地存储服务。
使用 Blazor WebAssembly 的最大好处之一是所有代码都在浏览器上运行。这意味着使用 Blazor WebAssembly 构建的 Web 应用可以离线运行。在下一章中,我们将利用这一优势来创建一个渐进式 Web 应用。
问题
以下问题供您思考:
-
IJSRuntime能否用来渲染 UI? -
你会如何将我们的本地存储服务添加到 Razor 类库中?
-
使用本地化 JavaScript 文件有哪些好处?
-
你认为你还会继续使用 JavaScript 吗?如果是的话,你打算用它来做什么?
-
在什么场景下你需要异步调用 JavaScript 而不是同步调用?
进一步阅读
以下资源提供了关于本章涵盖主题的更多信息:
-
想要了解更多关于使用 JavaScript 的信息,请参考
www.w3schools.com/js。 -
想要了解更多关于 JavaScript 的详细信息,请参考
developer.mozilla.org/en-US/docs/Web/javascript。 -
想要查看 JavaScript 的参考信息,请参考
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference。 -
想要了解更多关于
localStorage的信息,请参考www.w3.org/TR/webstorage/#the-localstorage-attribute。 -
想要了解更多关于 Microsoft Edge (Chromium)开发者工具的信息,请参考
learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium。
第六章:将天气应用构建为渐进式网络应用(PWA)
作为 Web 开发者,我们开发了各种类型的精彩 Web 应用,但直到最近,Web 应用能做什么与原生应用能做什么之间一直存在分歧。一类名为渐进式网络应用(PWAs)的新应用类别正在帮助我们弥合这一分歧,通过使我们能够向 Web 应用添加类似原生的功能、可靠性和可安装性。PWA 是一种利用原生应用功能的同时保留 Web 应用所有功能的 Web 应用。
在本章中,我们将学习什么定义了 PWA,以及如何通过向现有 Web 应用添加清单文件和服务工作者来创建 PWA。
本章中创建的项目将是一个本地 5 天天气预报应用,它可以作为原生应用安装在 Windows、macOS、iPhone、Android 手机等设备上,并且可以通过各种应用商店进行分发。我们将使用 JavaScript 的地理位置 API来获取设备的地理位置,并使用OpenWeather One Call API来获取该位置的天气预报。我们将通过添加清单文件和服务工作者将应用转换为 PWA。服务工作者将使用CacheStorage API来缓存信息,以便 PWA 可以在离线状态下工作。
原生应用,我是吗?
Web 应用,我是吗?
PWA!
在本章中,我们将涵盖以下主题:
-
理解 PWA
-
与清单文件(manifest files)一起工作
-
与服务工作者一起工作
-
使用
CacheStorageAPI -
使用
GeolocationAPI -
使用
OpenWeather One CallAPI -
创建 PWA
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 免费社区版的说明,请参阅第一章,Blazor WebAssembly 简介。
我们将使用一个外部天气 API 来访问我们项目的天气预报数据。我们将使用的 API 是OpenWeather One Call API。这是一个由OpenWeather提供的免费 API(openweathermap.org)。要开始使用此 API,您需要创建一个账户并获取一个 API 密钥。如果您不想创建账户,可以使用我们提供的weather.json文件,该文件位于本章 GitHub 仓库中。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter06.
代码实战视频在此处可用:packt.link/Ch6.
理解 PWA(渐进式网络应用)
PWA 是一种使用现代 Web 功能向用户提供类似原生应用程序体验的 Web 应用。PWA 看起来和感觉就像原生应用程序,因为它们在自己的应用程序窗口中运行,而不是在浏览器的窗口中,并且可以从开始菜单或任务栏启动。PWA 提供离线体验,并且由于使用缓存而可以即时加载。它们可以接收推送通知,并在后台自动更新。最后,尽管它们不需要在应用商店中列出以进行分发,但可以通过各种应用商店进行分发。
许多大型公司,如 Pinterest、星巴克、Trivago 和 Twitter,都采用了 PWA。公司被 PWA 吸引,因为它们可以一次开发,然后到处使用。
PWA 的感觉就像是一个原生应用程序,这是由于一系列技术的结合。要将 Web 应用转换为 PWA,它必须使用 HTTPS,并包含清单文件和服务工作者。
HTTPS
要转换为 PWA,Web 应用必须使用 HTTPS,并且必须在安全网络上提供服务。这不应该成为问题,因为大多数浏览器将不再通过 HTTP 提供服务。因此,即使你并没有计划将 Blazor WebAssembly 应用转换为 PWA,你也应该始终使用 HTTPS。
提示
要启用 HTTPS,需要一个安全套接字层(SSL)证书。免费 SSL 证书的一个很好的来源是Let’s Encrypt(letsencrypt.org)。它是一个免费、自动和开放的证书授权机构(CA)。
清单文件
清单文件是一个简单的JavaScript 对象表示法(JSON)文档,它包含应用程序的名称、默认值和 Web 应用程序启动时的启动参数。它描述了应用程序的外观和感觉。
这是一个简单清单文件的示例:
{
"name": "My Sample PWA",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
清单文件必须包含应用程序的名称和至少一个图标。我们将在下一节更详细地探讨清单文件。
服务工作者
服务工作者是一个 JavaScript 文件,它定义了 PWA 的离线体验。它拦截并控制 Web 浏览器如何处理其网络请求和资产缓存。
这是包含在 Microsoft 提供的Blazor WebAssembly PWA项目模板中的service-worker.js文件的内容:
self.addEventListener('fetch', () => { });
这只是一行代码,并且——正如你所看到的——它实际上并没有做什么。然而,它算作一个服务工作者,并且是转换应用程序为 PWA 所需的所有技术需求。我们将在本章后面更详细地探讨更健壮的服务工作者。
PWA 是一种可以像原生应用程序一样安装在设备上的 Web 应用。如果一个 Web 应用使用 HTTPS 并包含清单文件和服务工作者,它可以被转换为 PWA。让我们更详细地看看清单文件。
与清单文件一起工作
manifest.json to the index.html file:
<link href="manifest.json" rel="manifest" />
这里是一个比上一个示例更健壮的清单文件,它包含比之前示例更多的字段:
manifest.json
{
"dir": "ltr",
"lang": "en",
"name": " 5-Day Weather Forecast",
"short_name": "Weather",
"scope": "/",
"display": "standalone",
"start_url": "./",
"background_color": "transparent",
"theme_color": "transparent",
"description": "This is a 5-day weather forecast.",
"orientation": "any",
"related_applications": [],
"prefer_related_applications": false,
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"url": "https://bweweather.azurewebsites.net",
"screenshots": [],
"categories": ["weather"]
}
如前所述,清单文件必须包含应用程序的名称和至少一个图标。除此之外,其他所有内容都是可选的,尽管我们强烈建议您在清单文件中至少包含description、short_name和start_url。
这些是前面manifest.json文件中使用的键:
-
dir:name、short_name和description的基本方向。它可以是ltr、rtl或auto。 -
lang:name、short_name和description的主要语言。 -
name: 应用的名称。最大长度为 45 个字符。 -
short_name: 应用的简称。最大长度为 12 个字符。 -
scope: 应用的导航范围。 -
display: 应用显示的方式。有效的选项有fullscreen、standalone、minimal-UI或browser。 -
start_url: 应用的统一资源定位符(URL)。 -
background_color: 应用在启动屏幕上安装时使用的背景颜色。 -
theme_color: 默认的主题颜色。 -
description: 应用的简短描述。 -
orientation: 默认屏幕方向。一些选项包括any、natural、landscape和portrait。 -
related_applications: 开发者希望突出的任何相关应用。这些通常是原生应用。 -
prefer_related_applications: 一个值,通知用户代理相关应用比网页应用更受欢迎。 -
icons: 应用使用的一个或多个图片。这通常是文件中最大的部分,因为许多设备更喜欢不同尺寸的图片。 -
url: 应用的地址。 -
screenshots: 应用运行时的一组图片。 -
categories: 表示应用所属类别的字符串数组。
上述列表并未包括可以在manifest.json文件中包含的所有键。此外,每年仍在添加更多键。
一个清单文件控制了 PWA 在用户面前的显示方式,并且是转换网页应用为 PWA 所必需的。还需要一个服务工作者来将网页应用转换为 PWA。让我们更详细地看看服务工作者。
与服务工作者一起工作
服务工作者提供了 PWA 背后的魔法。它们用于缓存、后台同步和推送通知。服务工作者是一个 JavaScript 文件,它拦截并修改导航和资源请求。它使我们能够完全控制哪些资源被缓存以及我们的 PWA 在不同情况下的行为。
服务工作者只是一个在浏览器后台运行的脚本。它与应用分离,没有文档对象模型(DOM)访问。它运行在与为您的应用提供动力的主 JavaScript 不同的线程上,因此它不会阻塞。它被设计成完全异步的。
服务工作者生命周期
在与服务工作者一起工作时,了解它们的生命周期非常重要,因为离线支持可以为网络应用增加大量的复杂性。服务工作者生命周期中有三个步骤——安装、激活和 获取,如下面的图所示:

图 6.1:服务工作者生命周期
安装
在安装步骤期间,服务工作者通常会缓存网站的一些静态资源,例如一个您已离线的启动屏幕。如果文件成功缓存,则服务工作者已安装。然而,如果任何文件下载和缓存失败,则服务工作者未安装,并且不会移动到激活步骤。
如果服务工作者没有成功安装,它将在下次运行网络应用时尝试安装。因此,开发者可以确信,如果服务工作者已经成功安装,缓存将包含所有被指定为缓存的静态资源。在安装步骤成功完成后,将启动激活步骤。
激活
在激活步骤期间,服务工作者处理旧缓存的管理工作。由于之前的安装可能已创建缓存,这是应用程序删除缓存的机会。在激活步骤成功完成后,服务工作者准备开始处理获取事件。
获取
在获取步骤期间,服务工作者控制其范围内的所有页面。它处理从 PWA 发起的网络请求时发生的所有获取事件。服务工作者将继续获取,直到它被终止。
更新服务工作者
为了更新为我们网站运行的服务工作者,我们需要更新服务工作者的 JavaScript 文件。每次用户导航到我们的网站时,浏览器都会下载当前的服务工作者并将其与已安装的服务工作者进行比较。如果它们不同,它将尝试替换旧服务工作者。
然而,这不会立即发生。新服务工作者必须等待旧服务工作者不再控制,然后才能激活。旧服务工作者将保持控制,直到所有打开的页面都关闭。当新服务工作者接管控制时,其激活事件将触发。
缓存管理在激活回调期间处理。我们在激活回调期间管理缓存的原因是,如果我们要在安装步骤中清除任何旧缓存,那么控制所有当前页面的旧服务工作者将突然无法从该缓存中提供文件。
以下截图显示了一个等待激活的服务工作者:

图 6.2:服务工作者等待激活
提示
服务工作者将在用户在所有标签页中离开应用后才被激活。即使应用只在该一个标签页中运行,重新加载标签页也不足以激活服务工作者。然而,你可以通过点击跳过等待链接来激活一个等待激活的服务工作者。跳过等待链接在图 6.2中被突出显示。
服务工作者的类型
服务工作者有许多不同类型,从极其简单到更复杂。以下图表显示了不同类型的服务工作者,按从简单到复杂的顺序排列:

图 6.3:从简单到复杂的服务工作者类型
离线页面
这是最简单的一种功能服务工作者创建方式。我们只需要创建一个 HTML 页面来指示应用处于离线状态。每当应用无法连接到网络时,我们只需显示该 HTML 页面。
页面离线副本
使用这种类型的服务工作者,我们将每个页面在缓存中的副本存储为我们访客查看它们时的状态。当应用离线时,它从缓存中提供页面。这种方法可能只适用于页面数量有限的应用,因为如果用户想要查看的页面尚未被该用户查看,它将不会在缓存中,应用将失败。
离线页面与离线副本
这种类型的服务工作者是页面离线副本服务工作者的改进版本。它结合了前两种服务工作者类型。使用这种类型的服务工作者,我们存储每个页面的缓存副本,当我们的访客查看它们时。当应用离线时,它从缓存中提供页面。如果用户想要查看的页面不在缓存中,我们显示指示应用离线的 HTML 页面。
缓存优先网络
这种类型的服务工作者始终首先使用缓存。如果请求的页面在缓存中,它会在从服务器请求页面并更新缓存之前提供该页面。使用这种服务工作者,我们始终在请求页面之前提供缓存中的页面版本,因此无论用户在线还是离线,用户都会收到相同的数据。
重要提示
缓存优先的网络服务工作者是微软推荐的服务工作者类型。
高级缓存
这种类型的服务工作者是前述每种类型的组合。使用这种类型的服务工作者,我们使用不同的规则指定不同的文件和路由进行缓存。例如,某些数据,如股价,永远不应该被缓存,而其他不经常变化的数据应该被缓存。
背景同步
这是最复杂的服务工作者类型。它允许用户在离线时继续使用应用程序添加和编辑数据。然后,当他们重新上线时,应用程序将同步他们的数据与网络。
这不是所有可用服务工作者类型的完整列表。然而,它应该能让你了解服务工作者功能和灵活性的强大,以及缓存的重要性。我们列表中的所有服务工作者都依赖于 CacheStorage API 进行缓存。
使用 CacheStorage API
CacheStorage API 用于缓存 request/response 对象对,其中 request 对象是键,response 对象是值。它被设计为供服务工作者使用,以提供离线功能。一个 caches 对象是 CacheStorage 的实例。它是一个位于 window 对象中的全局对象。
我们可以使用以下代码来测试浏览器上是否可用 CacheStorage:
const hasCaches = 'caches' in self;
caches 对象用于维护特定 Web 应用程序的缓存列表。缓存不能与其他 Web 应用程序共享,并且它们与浏览器的 HTTP 缓存隔离。它们完全通过我们编写的 JavaScript 进行管理。
这些是 CacheStorage 的一些方法:
-
delete(cacheName): 此方法删除指定的缓存并返回true。如果指定的缓存未找到,则返回false。 -
has(cacheName): 如果指定的缓存存在,则此方法返回true,否则返回false。 -
keys: 此方法返回一个包含所有缓存名称的字符串数组。 -
open(cacheName): 此方法打开指定的缓存。如果它不存在,则创建并打开。
当我们打开 CacheStorage 的一个实例时,会返回一个 Cache 对象。以下是 Cache 对象的一些方法:
-
add(request): 此方法接受一个请求,并将生成的响应添加到缓存中。 -
addAll(requests): 此方法接受一个请求数组,并将所有生成的响应添加到缓存中。 -
delete(request): 如果它能找到并删除指定的请求,则此方法返回true,否则返回false。 -
keys(): 此方法返回一个键数组。 -
match(request): 此方法返回与匹配请求关联的响应。 -
put(request, response): 此方法将request和response对添加到缓存中。
提示
除非我们明确请求更新,否则 Cache 对象不会更新。此外,这些对象也不会过期。我们需要在它们变得过时后删除它们。
服务工作者使用 CacheStorage API 允许 PWA 在离线时继续运行。接下来,我们将解释如何使用 Geolocation API。
使用 Geolocation API
JavaScript 的 Geolocation API 提供了一种机制,使我们能够获取用户的地理位置。使用 Geolocation API,我们可以获取浏览器正在运行的设备的坐标。
Geolocation API 通过 navigator.geolocation 对象访问。当我们调用 navigator.geolocation 对象时,用户的浏览器会请求用户允许访问其位置。如果他们接受,浏览器将使用设备的定位硬件,例如智能手机上的 全球定位系统 (GPS),来确定其位置。
在我们尝试使用 navigator.geolocation 对象之前,我们应该验证它是否被浏览器支持。以下代码测试浏览器是否支持地理位置:
if (navigator.geolocation) {
var position = await getPositionAsync();
} else {
throw Error("Geolocation is not supported.");
};
对于本章的项目,我们将使用 getCurrentPosition 方法来检索设备的地理位置。此方法使用两个回调函数。success 回调函数返回一个 GeolocationPosition 对象,而 error 回调函数返回一个 GeolocationPositionError 对象。如果用户拒绝我们访问他们的位置,它将在 GeolocationPositionError 对象中报告。
这些是 GeolocationPosition 对象的属性:
-
coords.latitude: 这个属性返回一个表示设备纬度的双精度值。 -
coords.longitude: 这个属性返回一个表示设备经度的双精度值。 -
coords.accuracy: 这个属性返回一个表示纬度和经度精度的双精度值,以米为单位。 -
coords.altitude: 这个属性返回一个表示设备海拔的双精度值。 -
coords.altitudeAccuracy: 这个属性返回一个表示海拔精度的双精度值,以米为单位。 -
coords.heading: 这个属性返回一个表示设备前进方向的双精度值,以度为单位。如果设备处于静止状态,则该值为 NaN。 -
coords.speed: 这个属性返回一个表示设备速度的双精度值,以米/秒为单位。 -
timestamp: 这个属性返回响应的日期和时间。
GeolocationPosition 对象始终返回 coords.latitude、coords.longitude、coords.accuracy 和 timestamp 属性。其他属性仅在它们可用时返回。
通过使用 JavaScript 的 Geolocation API,我们可以确定设备的纬度和经度。我们需要这些信息来使用 OpenWeather 的 One Call API 为我们的项目请求本地天气预报。
使用 OpenWeather One Call API
本章项目中数据源是一个由 OpenWeather 提供的免费 API。它被称为 OpenWeather One Call API (openweathermap.org/api/one-call-api)。此 API 可以返回当前、预报和历史天气数据。我们将使用它来获取未来 5 天的本地预报。这是使用 OpenWeather One Call API 进行 API 调用的格式:
https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&appid={API key}
这些是 OpenWeather One Call API 的参数:
-
lat: 纬度。此参数是必需的。 -
lon: 经度。此参数是必需的。 -
appid: API 密钥。此参数是必需的。创建账户后,您可以从API keys选项卡管理您的 API 密钥。 -
units: 测量单位。此设置为Standard、Metric或Imperial。 -
exclude: 排除数据。此用于简化返回的数据。由于我们只会使用每日预报,我们将排除当前、每分钟和每小时数据以及我们的项目警报。这是一个逗号分隔的列表。 -
lang: 输出语言。
这是从 OpenWeather One Call API 的响应片段:
weather.json 片段
{
**"****dt"****:** **1616436000****,**
"sunrise": 1616416088,
"sunset": 1616460020,
"temp": {
"day": 58.5,
**"min"****:** **54.75****,**
**"max"****:** **62.6****,**
"night": 61.29,
"eve": 61.25,
"morn": 54.75
},
"feels_like": {
"day": 49.69,
"night": 51.91,
"eve": 50.67,
"morn": 47.03
},
"pressure": 1011,
"humidity": 85,
"dew_point": 54.01,
"wind_speed": 17.83,
"wind_deg": 168,
"weather": [
{
"id": 502,
"main": "Rain",
**"description"****:** **"heavy intensity rain"****,**
**"icon"****:** **"10d"**
}
],
"clouds": 98,
"pop": 1,
"rain": 27.91,
"uvi": 2.34
},
在前面的 JSON 片段中,我们突出显示了本章项目中使用的字段。
重要提示
此项目使用 OpenWeather One Call API 的 2.5 版本。他们最近发布了 API 的 3.0 版本。如果您一天内调用 API 超过 1,000 次,使用新版本的 API 需要支付少量费用。
OpenWeather One Call API 是一个简单的 API,我们将使用它来获取指定位置的每日预报。现在,让我们快速了解一下本章将要构建的项目。
创建 PWA
在本章中,我们将构建一个 Blazor WebAssembly 应用程序来显示本地的 5 天天气预报,并将其转换为 PWA。
我们将构建的 Web 应用程序使用 JavaScript 的 Geolocation API 确定设备的当前纬度和经度。它使用 OpenWeather One Call API 获取本地天气预报,并使用各种 Razor 组件向用户显示天气预报。完成 Web 应用程序后,我们将通过添加标志、清单文件和服务工作者将其转换为 PWA。最后,我们将安装、运行和卸载 PWA。
这是完成的应用程序的截图:

图 6.4:5 天天气预报应用程序
此项目的构建时间大约为 90 分钟。
项目概述
将使用 Microsoft 的 Blazor WebAssembly App Empty 项目模板创建一个 WeatherForecast 项目。首先,我们将使用 JavaScript 互操作性与 Geolocation API 获取设备的坐标。然后,我们将使用 OpenWeather One Call API 获取这些坐标的天气预报。接下来,我们将创建几个 Razor 组件来显示预报。
要将 Web 应用程序转换为 PWA,我们将添加一个标志、一个清单文件和一个离线页面服务工作者。在测试服务工作者后,我们将安装、运行和卸载 PWA。
项目入门
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击 创建新项目 按钮。
-
按 Alt+S 进入 搜索模板 文本框。
-
输入
Blazor并按 Enter 键。以下截图显示了Blazor WebAssembly App Empty项目模板。
![图形用户界面、文本、应用程序、聊天或文本消息 自动生成的描述]()
图 6.5:Blazor WebAssembly App Empty 项目模板
-
选择Blazor WebAssembly App Empty项目模板并点击下一步按钮。
-
在项目名称文本框中输入
WeatherForecast并点击下一步按钮。这是用于配置我们新项目的对话框截图:
![图形用户界面、文本、应用程序、电子邮件 自动生成的描述]()
图 6.6:配置新项目对话框
提示
在前面的示例中,我们将
WeatherForecast项目放置在E:/Blazor文件夹中。然而,这个项目的位置并不重要。 -
选择.NET 7.0作为要使用的框架版本。
-
勾选配置为 HTTPS复选框。
-
取消勾选ASP.NET Core 承载复选框。
-
取消勾选渐进式 Web 应用复选框。
这是用于收集有关我们新项目额外信息的对话框截图:
![图形用户界面、文本、应用程序、电子邮件 自动生成的描述]()
图 6.7:附加信息对话框
重要提示
我们没有勾选渐进式 Web 应用复选框的原因是,我们将在这个项目中将应用转换为 PWA。
-
点击创建按钮。
我们创建了一个空的WeatherForecast Blazor WebAssembly 项目。让我们通过添加用于确定设备位置的 JavaScript 函数来开始吧。
添加 JavaScript 以确定我们的位置
我们需要添加一个 JavaScript 函数来确定我们的当前纬度和经度。我们这样做如下:
-
右键点击
wwwroot文件夹,从菜单中选择添加,新建文件夹选项。 -
将新文件夹命名为
scripts。 -
右键点击
scripts文件夹,从菜单中选择添加,新建项选项。 -
在搜索框中输入
javascript。 -
选择JavaScript 文件。
-
将文件命名为
bweInterop.js。提示
在这本书中,我们将使用
bweInterop命名空间来为我们的 JavaScript 代码结构化和最小化命名冲突的风险。 -
点击添加按钮。
-
输入以下 JavaScript:
var bweInterop = {}; bweInterop.getPosition = async function () { function getPositionAsync() { return new Promise((success, error) => { navigator.geolocation.getCurrentPosition (success, error); }); } if (navigator.geolocation) { var position = await getPositionAsync(); var coords = { latitude: position.coords.latitude, longitude: position.coords.longitude }; return coords; } else { throw Error("Geolocation is not supported."); }; }前面的 JavaScript 代码使用
GeolocationAPI 返回设备的纬度和经度。如果未允许或不受支持,则会抛出错误。 -
打开
wwwroot/index.html文件。 -
在
body元素的底部添加以下标记:<script src="img/bweInterop.js"></script>
我们创建了一个使用Geolocation API 返回我们当前纬度和经度的 JavaScript 函数。接下来,我们需要在我们的 Web 应用中调用它。
调用 JavaScript 函数
我们需要添加一个类来存储我们的位置,然后我们可以调用我们的bweInterop.getPosition函数。我们这样做如下:
-
右键单击
WeatherForecast项目,从菜单中选择 添加,新建文件夹 选项。 -
将新文件夹命名为
Models。 -
右键单击
Models文件夹,从菜单中选择 添加,类 选项。 -
将新类命名为
Position。 -
将以下属性添加到
Position类中:public double Latitude { get; set; } public double Longitude { get; set; }这是我们将用来存储我们坐标的类。
-
打开
Pages/Index.razor文件。 -
删除
H1元素。 -
添加以下指令:
@using WeatherForecast.Models @inject IJSRuntime js -
添加以下标记:
<PageTitle>Weather Forecast</PageTitle> @if (pos == null) { <p><em>@message</em></p> } else { <h2> Latitude: @pos.Latitude, Longitude: @pos.Longitude </h2> } @code { string message = "Loading..."; Position? pos; }上述标记显示如果
pos属性为null,则显示message字段的值。否则,它显示pos属性中的纬度和经度。 -
将以下
OnInitializedAsync方法添加到代码块中:protected override async Task OnInitializedAsync() { try { await GetPositionAsync(); } catch (Exception) { message = "Geolocation is not supported."; }; }上述代码尝试在页面初始化时获取我们的坐标。
-
将以下
GetPositionAsync方法添加到代码块中:private async Task GetPositionAsync() { pos = await js.InvokeAsync<Position>( "bweInterop.getPosition"); }上述代码使用 JavaScript 互操作来调用我们编写的使用
GeolocationAPI 返回我们坐标的 JavaScript 函数。有关 JavaScript 互操作的更多信息,请参阅 第五章,使用 JavaScript 互操作 (JS Interop) 构建本地存储服务。 -
按 Ctrl+F5 以无调试模式启动应用程序。
以下截图是请求您允许访问位置的对话框示例:
![表格 自动生成的低置信度描述]()
图 6.8:地理位置权限对话框
-
点击 允许 按钮以允许应用程序访问您的位置。
以下截图是我们天气预报应用程序的 主页:
![图形用户界面,文本,应用程序 自动生成的描述]()
图 6.9:显示我们坐标的主页
您可以使用以下截图所示的 位置访问允许 对话框禁用应用程序访问您位置的能力:
![图形用户界面,应用程序 自动生成的描述]()
图 6.10:位置访问允许对话框
位置访问允许 对话框通过浏览器工具栏上的突出显示按钮访问。您可能想切换权限以查看这对应用程序的影响。
-
关闭浏览器。
我们已经使用 Geolocation API 在 主页 上显示我们的纬度和经度。接下来,我们需要将这些坐标提供给 OpenWeather One Call API。
添加一个 OpenWeather 类
我们需要添加一个 OpenWeather 类来捕获来自 OpenWeather One Call API 的结果。我们这样做如下:
-
返回 Visual Studio。
-
右键单击
Models文件夹,从菜单中选择 添加,类 选项。 -
将新类命名为
OpenWeather。 -
点击 添加 按钮。
-
添加以下类:
public class OpenWeather { public Daily[] Daily { get; set; } } public class Daily { public long Dt { get; set; } public Temp Temp { get; set; } public Weather[] Weather { get; set; } } public class Temp { public double Min { get; set; } public double Max { get; set; } } public class Weather { public string Description { get; set; } public string Icon { get; set; } }
上述类将用于存储来自 OpenWeather One Call API 的响应。
现在我们需要添加一个组件来显示响应。我们将使用 Bootstrap 来为新组件添加样式。
安装 Bootstrap
我们需要在我们的 Web 应用程序中安装 Bootstrap。我们这样做如下:
-
右键点击
wwwroot/css文件夹,从菜单中选择 添加,客户端库 选项。 -
在 库 搜索文本框中输入
bootstrap并按 Enter 键。 -
选择 选择特定文件。
-
只选择
css文件,如图下截图所示:

图 6.11:添加客户端库对话框
-
点击 安装 按钮。
重要提示
安装
Bootstrap后,将在wwwroot/css文件夹中添加一个新文件夹。这个新文件夹包含Bootstrap所需的所有 CSS 文件。在本项目中,我们只会使用bootstrap.min.css文件。现在
Bootstrap已安装,让我们验证它是否正常工作。 -
打开
wwwroot/index.html文件。 -
在
css/app.css样式表链接之前,向head元素添加以下标记:<link href="css/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> -
打开
Pages/Index.razor页面。 -
在
PageTitle组件下方添加以下标记:<div class="alert alert-info"> Bootstrap is installed! </div> -
按 Ctrl+F5 以无调试模式启动应用程序。
-
验证页面顶部是否现在为蓝色。
-
关闭浏览器。
现在 Bootstrap 已正确安装,我们可以添加新组件。
添加 DailyForecast 组件
我们需要新组件来显示每天的预报。我们这样做如下:
-
右键点击
WeatherForecast项目,从菜单中选择 添加,新建文件夹 选项。 -
将新文件夹命名为
Shared。 -
右键点击
Shared文件夹,从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
DailyForecast。 -
点击 添加 按钮。
-
将现有标记替换为以下标记:
<div class="card text-center"> <div class="card-header"> @Date </div> <div class="card-body"> <img src="img/@IconUrl" /> <h4 class="card-title">@Description</h4> <b>@((int)HighTemp) F°</b> / @((int)LowTemp) F° </div> </div> @code { }此组件使用
Bootstrap的Card组件来显示每日预报。 -
在代码块中添加以下代码:
[Parameter] public long Seconds { get; set; } [Parameter] public double HighTemp { get; set; } [Parameter] public double LowTemp { get; set; } [Parameter] public string? Description { get; set; } [Parameter] public string? Icon { get; set; } private string? Date; private string? IconUrl; protected override void OnInitialized() { Date = DateTimeOffset .FromUnixTimeSeconds(Seconds) .LocalDateTime .ToLongDateString(); IconUrl = String.Format( "https://openweathermap.org/img/wn/{0}@2x.png", Icon); }
上述代码定义了用于显示每日天气预报的参数。OnInitialized 方法用于格式化 Date 和 IconUrl 字段。
我们已添加一个 Razor 组件,使用 Bootstrap 的 Card 组件显示每天的天气预报。
获取预报
我们需要获取天气预报。我们可以通过调用 OpenWeather One Call API 或使用 GitHub 中的 weather.json 文件来获取预报。我们这样做如下:
-
打开
Pages/Index.razor文件。 -
添加以下
using语句:@using System.Text @using WeatherForecast.Shared -
添加以下指令:
@inject HttpClient Http -
在代码块顶部添加以下字段:
OpenWeather? forecast; -
将
GetForecastAsync方法添加到代码块:private async Task GetForecastAsync() { } -
在
GetForecastAsync方法中添加以下代码:if (pos != null) { string APIKey = "{Your API Key}"; StringBuilder url = new StringBuilder(); url.Append("https://api.openweathermap.org"); url.Append("/data/2.5/onecall?"); url.Append("lat="); url.Append(pos.Latitude); url.Append("&lon="); url.Append(pos.Longitude); url.Append("&exclude="); url.Append("current,minutely,hourly,alerts"); url.Append("&units=imperial"); url.Append("&appid="); url.Append(APIKey); forecast = await Http.GetFromJsonAsync<OpenWeather> (url.ToString()); }重要提示
您需要将
{Your_API_Key}替换为从OpenWeather获得的 API 密钥。此外,您的 API 密钥可能需要几小时才能激活。前述方法使用
OpenWeather One CallAPI,并通过GetPositionAsync方法获取的坐标来填充forecast对象。如果您不能使用
OpenWeather One CallAPI,请使用以下版本的GetForecastAsync方法:forecast = await Http.GetFromJsonAsync<OpenWeather> ("sample-data/weather.json");之前版本的
GetForecastAsync方法使用静态文件来填充forecast对象。它假设weather.json文件已经从本章的 GitHub 仓库下载,并且已经放置在wwwroot/sample-data文件夹中。 -
更新
OnInitializedAsync方法以调用GetForecastAsync方法并更新错误信息,如下所示:try { await GetPositionAsync(); await GetForecastAsync(); } catch (Exception) { message = "Error encountered."; };
现在我们已经填充了预报对象,我们可以显示预报。
显示预报
我们需要向 主页 添加一组每日预报。我们这样做如下:
-
删除
div元素。 -
将
@if语句替换为以下标记:@if (forecast == null) { <p><em>@message</em></p> } else { <div class="card-group"> @foreach (var item in forecast.Daily.Take(5)) { <DailyForecast Seconds="@item.Dt" LowTemp="@item.Temp.Min" HighTemp="@item.Temp.Max" Description="@item.Weather[0].Description" Icon="@item.Weather[0].Icon" /> } </div> }之前的标记循环遍历
forecast对象五次。它使用DailyForecast组件来显示每日预报。 -
按 Ctrl+F5 以无调试模式启动应用程序。
-
关闭浏览器。
我们已经完成了天气预报应用。现在,我们需要将其转换为 PWA。为此,我们需要添加一个标志、一个清单文件和一个服务工作者。
添加标志
我们需要添加一个图片作为应用的标志。我们这样做如下:
-
右键点击
wwwroot文件夹,从菜单中选择 添加,新建文件夹 选项。 -
将新文件夹命名为
images。 -
将本章 GitHub 仓库中的
Sun-512.png图片复制到images文件夹。
清单文件中至少必须包含一个图片才能安装 PWA。现在,我们可以添加一个清单文件。
添加一个清单文件
要将 Web 应用转换为 PWA,我们需要添加一个清单文件。我们这样做如下:
-
右键点击
wwwroot文件夹,从菜单中选择 添加,新建项 选项。 -
在 搜索 框中输入
json。 -
选择 JSON 文件。
-
将文件命名为
manifest.json。 -
点击 添加 按钮。
-
输入以下 JSON 代码:
{ "lang": "en", "name": "5-Day Weather Forecast", "short_name": "Weather", "display": "standalone", "start_url": "./", "background_color": "#ffa500", "theme_color": "transparent", "description": "This is a 5-day weather forecast app", "orientation": "any", "icons": [ { "src": "images/Sun-512.png", "type": "image/png", "sizes": "512x512" } ] } -
打开
wwwroot/index.html文件。 -
将以下标记添加到
head元素的底部:<link href="manifest.json" rel="manifest" /> -
在前面的标记下方添加以下标记:
<link rel="apple-touch-icon" sizes="512x512" href="images/Sun-512.png" />提示
使用 iOS Safari 时,您必须包含前面的链接标签来指示它使用指定的图标,否则它将通过截图页面内容来生成图标。
我们已经向我们的 Web 应用添加了一个清单文件来控制它在安装时的外观和行为。接下来,我们需要添加一个服务工作者。
添加一个简单的服务工作者
要完成将 Web 应用转换为 PWA 的过程,我们需要添加一个服务工作者。我们这样做如下:
-
右键点击
wwwroot文件夹,从菜单中选择 添加,新建项 选项。 -
在 搜索 框中输入
html。 -
选择 HTML 页面。
-
将文件命名为
offline.html。 -
点击 添加 按钮。
-
将以下标记添加到
body元素中:<h1>You are offline.</h1> -
右键点击
wwwroot文件夹,从菜单中选择 添加,新建项 选项。 -
在 搜索 框中输入
javascript。 -
选择
JavaScript 文件。 -
将文件命名为
service-worker.js。 -
点击添加按钮。
-
添加以下常量:
const OFFLINE_VERSION = 1; const CACHE_PREFIX = 'offline'; const CACHE_NAME = '${CACHE_PREFIX}${OFFLINE_VERSION}'; const OFFLINE_URL = 'offline.html';前面的代码设置当前缓存的名称和我们将使用的文件名称以指示我们处于离线状态。
-
添加以下事件监听器:
self.addEventListener('install', event => event.waitUntil(onInstall(event))); self.addEventListener('activate', event => event.waitUntil(onActivate(event))); self.addEventListener('fetch', event => event.respondWith(onFetch(event)));前面的代码指定了以下步骤中要使用的函数:安装、激活和获取。
-
添加以下
onInstall函数:async function onInstall(event) { console.info('Service worker: Install'); const cache = await caches.open(CACHE_NAME); await cache.add(new Request(OFFLINE_URL)); }前面的函数打开指定的缓存。如果缓存尚不存在,它将创建缓存然后打开它。缓存打开后,它将指定的请求/响应对添加到缓存中。
-
添加以下
onActivate函数:async function onActivate(event) { console.info('Service worker: Activate'); const cacheKeys = await caches.keys(); await Promise.all(cacheKeys .filter(key => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME) .map(key => caches.delete(key))); }前面的代码获取所有缓存的名称。所有与指定缓存名称不匹配的缓存都将被删除。
提示
清除过时缓存是您的责任。每个浏览器都对 Web 应用可以使用的存储量有限制。如果您违反了该限制,浏览器可能会删除您的所有缓存。
-
添加以下
onFetch函数:async function onFetch(event) { if (event.request.method === 'GET') { try { return await fetch(event.request); } catch (error) { const cache = await caches.open(CACHE_NAME); return await cache.match(OFFLINE_URL); }; }; }在前面的代码中,如果获取失败,将打开缓存,并服务之前缓存的离线页面。
-
打开
wwwroot/index.html文件。 -
将以下标记添加到
body元素的底部:<script> navigator.serviceWorker.register('service-worker.js'); </script>前面的代码注册了服务工作者。
我们已添加一个离线页面服务工作者,当 PWA 离线时将显示offline.html页面。
测试服务工作者
我们需要测试服务工作者是否允许我们在离线状态下工作。我们这样做如下:
-
按Ctrl+F5以无调试模式启动应用程序。
-
点击F12以打开
开发者工具界面。 -
选择应用程序选项卡。
-
从左侧菜单中选择清单选项以查看应用清单详情。

图 6.12:应用清单详情
-
从左侧菜单中选择服务工作者选项以查看当前客户端安装的服务工作者,如图下所示:
![图形用户界面,应用程序 自动生成的描述]()
图 6.13:服务工作者对话框
在前面的屏幕截图中,我们已突出显示离线复选框和查看所有注册链接。
提示
点击查看所有注册链接以查看您设备上安装的所有服务工作者。您可能会惊讶地发现您的计算机上安装了多少服务工作者。
-
从左侧菜单中选择缓存存储选项以查看缓存。
-
点击offline1缓存以查看其内容,如图下所示:

图 6.14:缓存存储
-
从左侧菜单中选择服务工作者选项。
-
在服务工作者对话框中勾选离线复选框。
提示
图 16.13 中的离线复选框被突出显示。
-
刷新浏览器,你应该看到以下屏幕:
![图形用户界面,文本,应用,聊天或短信描述自动生成]()
图 6.15:离线页面
显示的页面来自浏览器的缓存。
-
在服务工作者对话框中取消选中离线复选框。
-
刷新浏览器。
由于网络应用现在已重新上线,离线页面不再显示。
我们已经测试过服务工作者使我们的网络应用能够离线工作。现在,我们可以安装 PWA。
安装 PWA
我们需要通过安装来测试 PWA。我们这样做如下:
- 从浏览器的菜单中选择应用可用。安装 5 天天气预报菜单选项:

图 6.16:安装 5 天天气预报选项
提示
在基于 Chromium 的浏览器中,安装按钮位于 URL 栏。然而,对于其他类型的浏览器,您需要从菜单按钮或分享按钮安装 PWA。
- 在安装 PWA对话框中点击安装按钮:

图 6.17:安装 PWA 对话框
-
在应用已安装对话框中点击允许按钮。
![图形用户界面,文本,应用描述自动生成]()
图 6.18:应用已安装对话框
安装后,PWA 无地址栏显示。它出现在我们的任务栏上,我们可以从开始菜单运行它。以下截图显示了安装后的 PWA:
![图形用户界面,应用描述自动生成]()
图 6.19:已安装 PWA
-
关闭应用。
-
按下 Windows 键并搜索5 天天气预报应用。
-
打开5 天天气预报应用。
当应用打开时,其图标会出现在任务栏上。如果我们想的话,可以将其固定到任务栏上。
我们已成功安装并运行了 PWA。卸载 PWA 和安装一样简单。
卸载 PWA
我们需要卸载 PWA。我们这样做如下:
-
关闭5 天天气预报应用。
-
按下 Windows 键并搜索5 天天气预报应用。
-
右键点击5 天天气预报应用,并从菜单中选择卸载选项。

图 6.20:卸载 5 天天气预报应用
-
点击卸载按钮。
![图形用户界面,文本,应用,聊天或短信描述自动生成]()
图 6.21:卸载 5 天天气预报应用
我们已卸载 PWA。
重要提示
应用从任务栏中移除可能需要几秒钟。
摘要
你现在应该能够通过添加清单文件和服务工作者将 Blazor WebAssembly 应用程序转换为 PWA。
在本章中,我们介绍了 PWA。我们解释了如何通过添加清单文件和服务工作者将 Web 应用程序转换为 PWA。我们解释了如何处理清单文件和服务工作者。我们详细解释了不同类型的服务工作者,并解释了如何使用 CacheStorage API 来缓存请求/响应对。最后,我们演示了如何使用 Geolocation API 和 OpenWeather One Call API。
之后,我们使用了 Blazor WebAssembly App Empty 项目模板来创建一个新的项目。我们添加了一个使用 Geolocation API 获取我们坐标的 JavaScript 函数。我们添加了一些模型来捕获坐标,并使用 JavaScript 互操作调用了 JavaScript 函数。我们使用 OpenWeather One Call API 获取当地的 5 天天气预报。我们安装了 Bootstrap 并创建了一个 Razor 组件来显示每天的预报。
在本章的最后部分,我们通过添加图片、清单文件和离线页面服务工作者将 Blazor WebAssembly 应用程序转换为 PWA。最后,我们安装、运行和卸载了 PWA。我们可以应用我们的新技能将现有的 Web 应用程序转换为结合 Web 应用程序优势和原生应用外观和感觉的 PWA。
在下一章中,我们将使用 依赖注入(DI)来构建购物车应用程序。
问题
以下问题供您思考:
-
服务工作者是异步的还是同步的?
-
是否可以在服务工作者内部使用
localStorage进行数据存储? -
服务工作者能否操作 DOM?
-
PWA 是否安全?
-
PWA 是否具有平台特定性?
-
PWA 和原生应用之间有什么区别?
进一步阅读
以下资源提供了有关本章主题的更多信息:
-
有关
GeolocationAPI 规范的更多信息,请参阅w3c.github.io/geolocation-api。 -
有关使用
GeolocationAPI 的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/API/Geolocation_API。 -
有关
WeatherAPI 的更多信息,请参阅openweathermap.org/api。 -
有关
Web 应用程序清单规范的更多信息,请参阅www.w3.org/TR/appmanifest。 -
有关
Service Worker规范的更多信息,请参阅w3c.github.io/ServiceWorker。 -
有关使用
CacheStorageAPI 的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/API/CacheStorage。 -
想要了解更多关于服务工作者(service workers)的示例,请参考 Workbox 网站上的
developers.google.com/web/tools/workbox。 -
如需了解有关微软的
PWABuilder的更多信息,请参考www.pwabuilder.com。 -
如需 PWA 图片生成器,请参考
www.pwabuilder.com/imageGenerator。 -
如需了解有关
Bootstrap的更多信息,请参考getbootstrap.com/。
第七章:使用应用程序状态构建购物车
有时,我们需要我们的应用程序在不同的页面之间保持其状态。我们可以通过使用 依赖注入(DI)来实现这一点。DI 用于访问在中央位置配置的服务。
在本章中,我们将创建一个购物车。当我们从购物车中添加和删除项目时,应用程序将维护购物车中项目的列表。当我们导航到另一个页面然后再返回到带有购物车的页面时,购物车的内容将被保留。此外,购物车的总额将在所有页面上显示。
应用程序状态,
依赖注入
一个建立在信任基础上的团队!
在本章中,我们将涵盖以下主题:
-
介绍应用程序状态
-
理解 DI
-
创建购物车项目
技术要求
要完成此项目,您需要在您的电脑上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 免费社区版的说明,请参阅 第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter07。
代码实战视频在此处提供:packt.link/Ch7。
介绍应用程序状态
在 Blazor WebAssembly 应用中,浏览器的内存用于保存应用程序的状态。这意味着当用户在页面之间导航时,除非我们保留它,否则状态会丢失。我们将使用 AppState 模式来保留应用程序的状态。
在 AppState 模式中,一个服务被添加到 DI 容器中,以协调相关组件之间的状态。该服务包含所有需要维护的状态。因为服务由 DI 容器管理,所以它可以比单个组件存活更久,并且可以在 UI 发生变化时保留应用程序的状态。
该服务可以是一个简单的类或一个复杂的类。一个服务可以用来管理整个应用程序中多个组件的状态。AppState 模式的优点之一是它导致了展示逻辑和业务逻辑之间更大的分离。
重要提示
当用户重新加载页面时,保存在浏览器内存中的应用程序状态会丢失。
对于本章的项目,我们将使用 DI 服务实例来保留应用程序的状态。
理解 DI
DI(依赖注入)是一种技术,其中对象访问在中央位置配置的服务。中央位置是 DI 容器。在使用 DI 时,每个消费类不需要创建它所依赖的注入类的实例。它由框架提供,并被称为服务。在 Blazor WebAssembly 应用程序中,服务在program.cs文件中定义。
我们在这本书中已经使用了 DI(依赖注入),以下是一些使用的服务:
-
HttpClient -
IJSRuntime -
NavigationManager
DI 容器
当 Blazor WebAssembly 应用程序启动时,它配置一个 DI 容器。DI 容器负责构建服务的实例,并且它一直存在,直到用户关闭运行 web 应用的浏览器标签页。在以下示例中,CartService实现被注册为ICartService:
builder.Services.AddSingleton<ICartService, CartService>();
在服务被添加到 DI 容器后,我们使用@inject指令将服务注入到任何依赖它的类中。@inject指令接受两个参数,类型和属性:
-
类型:这是服务的类型。 -
属性:这是接收服务的属性的名称。
以下示例展示了如何使用@inject指令来引用在前面代码中注册的CartService:
@inject ICartService cartService
依赖注入在组件实例创建后、OnInitialized或OnInitializedAsync生命周期事件执行之前进行。这意味着你无法在组件的构造函数中使用注入的类,但你可以在OnInitialized或OnInitializedAsync方法中使用它。
每个服务在其注册时指定其生命周期。
服务生命周期
使用 DI 注入的服务生命周期可以是以下任何值之一:
-
单例
-
范围限定
-
短暂
单例
如果服务的生命周期被定义为Singleton,这意味着将创建一个类的单个实例,并且该实例将在整个应用程序中共享。任何使用该服务的组件都将收到相同服务的实例。在 Blazor WebAssembly 应用程序中,这适用于当前在浏览器当前标签页中运行的当前应用程序的生命周期。
以下代码将创建ICartService类的共享实例:
builder.Services.AddSingleton<ICartService, CartService>();
这是我们将在本章的项目中用来管理应用程序状态的服务的生命周期。
范围限定
如果服务的生命周期被定义为Scoped,这意味着将为每个范围创建类的新的实例。由于 Blazor WebAssembly 应用程序的范围概念与应用程序的生命周期相一致,因此注册为Scoped的服务被当作Singleton服务处理。
以下代码将创建ICartService类的共享实例:
builder.Services.AddScoped<ICartService, CartService>();
上述代码创建的实例与使用AddSingleton方法创建的实例相同。
重要提示
在 Microsoft 提供的 Blazor WebAssembly 项目模板中,他们使用Scoped服务来创建数据访问的HttpClient实例。这是因为 Microsoft 的项目模板使用Scoped服务生命周期来与服务器端 Blazor 保持对称。
瞬时
如果服务的生命周期被定义为Transient,这意味着每次请求服务实例时都会创建该类的新实例。当使用Transient服务时,DI 容器充当工厂,创建类的唯一实例。一旦实例创建并注入到依赖组件中,容器就不再对它感兴趣。
以下代码将创建OneUseService类的瞬时实例:
builder.Services.AddTransient<IOneUseService, OneUseService>();
服务的生命周期在 DI 容器中定义。服务实例可以是作用域的或瞬时的。
我们可以使用 DI 将相同的服务实例注入到多个组件中。DI 被AppState模式使用,以允许应用程序在组件之间保持状态。
现在,让我们快速了解一下我们将在本章中构建的项目。
创建购物车项目
在本章中,我们将构建一个包含购物车的 Blazor WebAssembly 应用。我们将能够向购物车添加和删除不同的产品。购物车的总价将在应用的每个页面上显示。
以下为完成的应用程序的截图:

图 7.1:购物车应用
此项目的构建时间大约为 60 分钟。
项目概述
将使用 Microsoft 的Blazor WebAssembly App Empty项目模板创建一个空的 Blazor WebAssembly 项目来创建ShoppingCart项目。首先,我们将添加逻辑以向购物车添加和删除产品。然后,我们将演示在页面之间导航时购物车的状态会丢失。为了保持购物车的状态,我们将在 DI 容器中注册一个使用AppState模式的服务。最后,我们将演示通过将新服务注入相关组件,购物车的状态不会丢失。
创建购物车项目
我们需要创建一个新的 Blazor WebAssembly 应用。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击创建新项目按钮。
-
按Alt+S键进入搜索模板文本框。
-
输入
Blazor并按Enter键。以下截图显示了Blazor WebAssembly App Empty项目模板。
![图形用户界面,文本,应用程序,聊天或文本消息 自动生成的描述]()
图 7.2:Blazor WebAssembly App Empty 项目模板
-
选择Blazor WebAssembly App Empty项目模板并点击下一步按钮。
-
在项目名称文本框中输入
ShoppingCart并点击下一步按钮。这是用于配置我们新项目的对话框截图:
![]()
图 7.3:配置新项目对话框
提示
在前面的示例中,我们将
ShoppingCart项目放置在E:/Blazor文件夹中。然而,此项目的位置并不重要。 -
选择.NET 7.0作为要使用的框架版本。
-
选择配置为 HTTPS复选框。
-
取消选择ASP.NET Core 托管复选框。
-
取消选择渐进式 Web 应用复选框。
这是用于收集有关我们新项目额外信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 7.4:额外信息对话框
-
点击创建按钮。
我们已创建了一个空的 ShoppingCart Blazor WebAssembly 项目。我们将使用 Bootstrap 来样式化我们的前端。
安装 Bootstrap
我们需要在我们的 Web 应用中安装 Bootstrap。我们这样做如下:
-
右键单击
wwwroot/css文件夹,从菜单中选择添加、客户端库选项。 -
在库搜索文本框中输入
bootstrap并按Enter键。 -
选择选择特定文件。
-
仅选择css文件,如图下所示:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 7.5:添加客户端库对话框
提示
尽管前面的截图选择了
Bootstrap版本 5.2.2,但你可以使用任何版本的Bootstrap来完成此项目。 -
点击安装按钮。
重要提示
安装
Bootstrap后,将在wwwroot/css文件夹中添加一个新文件夹。这个新文件夹包含Bootstrap所需的所有 CSS 文件。在这个项目中,我们只会使用bootstrap.min.css文件。 -
打开
wwwroot/index.html文件。 -
在
css/app.css样式表链接之前,将以下标记添加到head元素中:<link href="css/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> -
打开
MainLayout.razor文件。 -
将
main元素更新为以下内容:<main class="px-4"> @Body </main>上述代码使用
Bootstrap在页面主体周围添加填充。 -
打开
Pages/Index.razor文件。 -
删除
h1元素。 -
添加以下标记:
<PageTitle>Home</PageTitle> <div class="bg-light"> <ol class="breadcrumb"> <li class="breadcrumb-item active">Home</li> </ol> </div> <h1 class="display-3">Welcome to Blazing Tasks!</h1> <h2> The store is open for business. </h2> <button type="button" class="btn btn-danger mt-2"> Start Shopping </button>上述代码包含一个面包屑,该面包屑将使用
Bootstrap进行样式化。面包屑指示页面在导航层次结构中的当前位置。代码还包括一个按钮。然而,该按钮目前还没有任何功能。 -
按 Ctrl+F5 以不带调试启动应用程序。
这是主页的截图:
![包含文本的图片 自动生成的描述]()
图 7.6:ShoppingCart 项目的首页
-
点击开始购物按钮以验证没有任何操作发生。
-
关闭浏览器。
当用户点击开始购物按钮时,他们应该导航到商店页面。然而,商店页面目前还不存在。
添加 Store 组件
我们需要添加一个Store组件。我们这样做:
-
右键点击
Pages文件夹,从菜单中选择添加、Razor 组件选项。 -
将新组件命名为
Store。 -
点击添加按钮。
-
删除
H3元素。 -
在代码块之前添加以下标记:
@page "/store" <PageTitle>Store</PageTitle> <ol class="breadcrumb bg-light"> <li class="breadcrumb-item"><a href="#">Home</a></li> <li class="breadcrumb-item active">Store</li> </ol>上述代码包含一个面包屑,我们可以使用它来导航到首页。
-
打开
Pages/Index.razor文件。 -
添加以下
@inject指令:@inject NavigationManager navigationNavigationManager服务由框架提供,用于管理 URI 导航。 -
添加以下代码块:
@code{ protected void OpenStore() { navigation.NavigateTo("store"); } }在上述代码中,当调用
OpenStore时,NavigationManager将导航到Store页面。由于这是一个 SPA,页面不需要重新加载。 -
向开始购物按钮添加以下
@onclick事件:@onclick="OpenStore" -
按Ctrl+F5启动应用程序,不进行调试。
-
点击开始购物按钮以导航到商店页面。
-
点击首页面包屑以返回首页。
-
关闭浏览器。
我们已经添加了商店页面。然而,它是空的。
添加 Product 类
我们需要添加可供销售的产品。我们这样做:
-
右键点击
ShoppingCart项目,从菜单中选择添加、新建文件夹选项。 -
将新文件夹命名为
Models。 -
右键点击
Models文件夹,从菜单中选择添加、类选项。 -
将新类命名为
Product。 -
点击添加按钮。
-
将以下属性添加到
Product类中:public int ProductId { get; set; } public string? ProductName { get; set; } public int Price { get; set; } public string? Image { get; set; } -
右键点击
wwwroot文件夹,从菜单中选择添加、新建文件夹选项。 -
将新文件夹命名为
sample-data。 -
右键点击
sample-data文件夹,从菜单中选择添加、新建项选项。 -
在搜索框中输入
json。 -
选择JSON 文件。
-
将文件命名为
products.json。 -
点击添加按钮。
-
将文件更新为以下内容:
[ { "productId": 1, "productName": "Charger", "price": 15, "image": "charger.jpg" }, { "productId": 2, "productName": "Ear Buds", "price": 22, "image": "earbuds.jpg" }, { "productId": 3, "productName": "Key Chain", "price": 1, "image": "keychain.jpg" }, { "productId": 4, "productName": "Travel Mug", "price": 8, "image": "travelmug.jpg" }, { "productId": 5, "productName": "T-Shirt", "price": 20, "image": "tshirt.jpg" } ]
提示
您可以从 GitHub 仓库复制products.json文件。
-
右键点击
wwwroot文件夹,从菜单中选择添加、新建文件夹选项。 -
将新文件夹命名为
images。 -
从 GitHub 仓库复制以下图片到
images文件夹:Charger.jpg、Earbuds.jpg、KeyChain.jpg、TravelMug.jpg和Tshirt.jpg。
我们已经将产品集合添加到我们的 Web 应用中。接下来,我们需要完成Store页面。
完成 Store 组件
我们需要添加我们商店中可供销售的项目列表,并且我们需要添加选择我们想要购买的项目的能力。我们这样做:
-
打开
Pages/Store.razor文件。 -
添加以下指令:
@using ShoppingCart.Models @inject HttpClient Http -
向代码块添加以下代码:
public IList<Product>? products; public IList<Product> cart = new List<Product>(); private int total; protected override async Task OnInitializedAsync() { products = await Http.GetFromJsonAsync<Product[]> ("sample-data/products.json"); }上述代码使用
HttpClient从products.json文件中填充产品列表。 -
在代码块之前添加以下
if语句:@if (products == null) { <p><em>Loading...</em></p> } else { <h1 class="display-3">Products</h1> <div class="row"> <div id="products" class="col-xl-4 col-lg-6"> </div> <div id="cart" class="col-xl-3 col-lg-4"> </div> </div> }上述代码在产品列表加载前显示加载中...消息。一旦产品列表加载完成,它将显示产品和购物车。
-
将以下标记添加到
products元素中:<table width="100%"> @foreach (Product item in products) { <tr> <td> <img src="img/@item.Image" /> </td> <td class="align-middle"> @item.ProductName </td> <td class="align-middle"> $@item.Price </td> <td class="align-middle"> <button type="button" class="btn btn-danger"> Add to Cart </button> </td> </tr> } </table>前面的标记添加了一个显示所有销售产品的表格。
-
将以下标记添加到
cart元素中:@if (cart.Any()) { <h2>Your Cart</h2> <ul class="list-group"> @foreach (Product item in cart) { <li class="list-group-item p-2"> <button type="button" class="btn btn-sm btn-danger me-2"> Delete </button> @item.ProductName - $@item.Price </li> } </ul> <div class="p-2"> <h3>Total: $@total</h3> </div> }前面的标记显示了购物车中的所有项目。
-
将
AddProduct方法添加到代码块:private void AddProduct(Product product) { cart.Add(product); total += product.Price; }前面的代码将指定的产品添加到购物车,并按产品的价格增加总额。
-
将
DeleteProduct方法添加到代码块:private void DeleteProduct(Product product) { cart.Remove(product); total -= product.Price; }前面的代码从购物车中删除指定的产品,并按产品的价格减少总额。
-
将以下
@onlclick事件添加到添加到购物车按钮:@onclick="@(() => AddProduct(item))" -
前面的代码在按钮点击时调用
AddProduct方法。有关事件处理的更多信息,请参阅第八章,使用事件构建看板。 -
将以下
@onlclick事件添加到删除按钮:@onclick="@(()=>DeleteProduct(item))"前面的代码在按钮点击时调用
DeleteProduct方法。
我们已经完成了对Store页面的更新。现在我们需要测试它。
展示应用程序状态已丢失
我们需要运行我们的 Web 应用程序以测试Store页面。我们这样做如下:
-
按Ctrl+F5启动应用程序,不进行调试。
-
点击开始购物按钮。
-
使用添加到购物车按钮添加几个产品。
-
使用删除按钮从购物车中删除产品。
-
在导航菜单中选择主页选项。
-
点击开始购物按钮返回Store页面。
-
确认购物车现在为空。
当我们在 Web 应用程序中的页面之间导航时,状态会丢失。
-
关闭浏览器。
我们可以通过使用 DI 来启用AppState模式来维护状态。我们将在项目中添加一个CartService,我们将使用 DI 来管理它。
创建 ICartService 接口
我们需要创建一个ICartService接口。我们这样做如下:
-
返回 Visual Studio。
-
右键单击
ShoppingCart项目,从菜单中选择添加、新建文件夹选项。 -
将新文件夹命名为
Services。 -
右键单击
Services文件夹,从菜单中选择添加、新建项选项。 -
在搜索框中输入
interface。 -
选择接口。
-
将文件命名为
ICartService。 -
点击添加按钮。
-
输入以下代码:
IList<Product> Cart { get; } int Total { get; } event Action OnChange; void AddProduct(Product product); void DeleteProduct(Product product);重要提示
Visual Studio 将自动添加以下
using语句:using ShoppingCart.Models;
我们已经创建了ICartService接口。现在我们需要创建一个继承它的类。
创建 CartService 类
我们需要创建CartService类。我们这样做如下:
-
右键单击
Services文件夹,从菜单中选择添加、类选项。 -
将类命名为
CartService。 -
点击添加按钮。
-
更新类如下:
public class CartService : ICartService { private List<Product> cart = new(); private int total; public IList<Product> Cart { get => cart; } public int Total { get => total; } public event Action? OnChange; }CartService类继承自ICartService接口。 -
将
NotifyStateChanged方法添加到类中:private void NotifyStateChanged() => OnChange?.Invoke();在之前的代码中,当调用
NotifyStateChanged方法时,会触发OnChange事件。 -
将
AddProduct方法添加到类中:public void AddProduct(Product product) { cart.Add(product); total += product.Price; NotifyStateChanged(); }之前的代码将指定的产品添加到产品列表中并增加总数。它还调用了
NotifyStateChanged方法。 -
将
DeleteProduct方法添加到类中:public void DeleteProduct(Product product) { cart.Remove(product); total -= product.Price; NotifyStateChanged(); }之前的代码从产品列表中移除了指定的产品并减少了总数。它还调用了
NotifyStateChanged方法。
我们已经完成了CartService类的编写。现在我们需要在 DI 容器中注册CartService。
在 DI 容器中注册 CartService
在将CartService注入到我们的Store组件之前,我们需要在 DI 容器中注册CartService。我们这样做如下:
-
打开
Program.cs文件。 -
在注册
HttpClient的代码之后添加以下代码:builder.Services.AddScoped<ICartService, CartService>();
我们已经注册了CartService。现在我们需要更新Store页面以使用它。
将CartService注入到 Store 组件中
我们需要将CartService注入到Store组件中。我们这样做如下:
-
打开
Pages\Store.razor文件。 -
添加以下指令:
@using ShoppingCart.Services @inject ICartService cartService -
将添加到购物车按钮的
@onclick事件更新如下:@onclick="@(() => cartService.AddProduct(item))"之前的标记使用
cartService将产品添加到购物车中。 -
更新
cart元素如下:@if (cartService.Cart.Any()) { <h2>Your Cart</h2> <ul class="list-group"> @foreach (Product item in cartService.Cart) { <li class="list-group-item p-2"> <button class="btn btn-sm btn-danger me-2"> Delete </button> @item.ProductName - $@item.Price </li> } </ul> <div class="p-2"> <h3>Total: $@cartService.Total</h3> </div> }之前的代码将
cart属性的引用替换为cartService的引用。 -
向删除按钮添加以下
@onclick事件:@onclick="@(() =>cartService.DeleteProduct(item))"之前的标记使用
cartService从购物车中删除产品。 -
从代码块中删除
cart属性、total属性、AddProduct方法和DeleteProduct方法。 -
按Ctrl+F5以无调试模式启动应用程序。
-
点击开始购物按钮。
-
使用添加到购物车按钮添加一些产品。
-
使用删除按钮从购物车中删除一个产品。
-
在导航菜单中选择主页选项。
-
点击开始购物按钮返回商店页面。
-
确认购物车不为空。
我们已经确认CartService正在工作。现在我们需要将购物车总额添加到所有页面。
将购物车总额添加到所有页面
要在所有页面上查看购物车总额,我们需要将购物车总额添加到一个在所有页面上都使用的组件中。由于MainLayout组件被所有页面使用,我们将购物车总额添加到其中。我们这样做如下:
-
返回 Visual Studio。
-
打开
Shared\MainLayout.razor文件。 -
添加以下
@using指令:@using ShoppingCart.Services -
添加以下
@inject指令:@inject ICartService cartService -
在
main元素上方添加以下标记:<div class="alert alert-primary"> <h2>Cart Total: $@cartService.Total</h2> </div> -
按Ctrl+F5以无调试模式启动应用程序。
-
向购物车添加一些商品。
-
确认页面顶部的购物车总额字段没有更新。
当我们向购物车添加新项目时,页面顶部的购物车总额没有更新。我们需要处理这个问题。
使用 OnChange 方法
我们需要通知组件何时需要更新。我们这样做如下:
-
返回 Visual Studio。
-
打开
Shared\MainLayout.razor文件。 -
添加以下
@implements指令:@implements IDisposable -
添加以下
@code块:@code{ protected override void OnInitialized() { cartService.OnChange += StateHasChanged; } public void Dispose() { cartService.OnChange -= StateHasChanged; } }在前面的代码中,组件的
StateHasChanged方法在OnInitialized方法中订阅了cartService.OnChange方法,并在Dispose方法中取消订阅。 -
按 Ctrl+F5 启动应用程序,不进行调试。
-
使用 添加到购物车 按钮添加一些产品到购物车。
-
确认页面顶部的 购物车总计 字段已更新。
-
使用 删除 按钮从购物车中删除一个产品。
-
确认页面顶部的 购物车总计 字段已更新。
-
在导航菜单中选择 首页 选项。
-
确认 首页 顶部的 购物车总计 字段正确显示。
我们已更新组件,使其在
CartService的OnChange方法被调用时调用StateHasChanged方法。
提示
不要忘记在销毁组件时取消订阅事件。
你必须取消订阅事件,以防止每次 cartService.OnChange 事件被触发时 StateHasChanged 方法被调用。否则,你的应用程序将经历资源泄漏。
摘要
你现在应该能够使用 DI 将 AppState 模式应用到 Blazor WebAssembly 应用程序中。
在本章中,我们介绍了应用程序状态和 DI。我们解释了如何使用 DI 容器以及如何将服务注入到组件中。我们还讨论了单例、作用域和瞬态服务生命周期的区别。
之后,我们使用了 Blazor WebAssembly App Empty 项目模板来创建一个新的项目。我们安装了 Bootstrap 来美化前端。我们将一个 Store 组件添加到项目中,并演示了在页面间导航时应用程序状态会丢失。为了保持应用程序的状态,我们在 DI 容器中注册了 CartService 服务。最后,我们演示了通过使用 AppState 模式,我们可以保持购物车的状态。
我们可以将我们的新技能与 DI 结合起来,以维护任何 Blazor WebAssembly 应用的应用程序状态。
在下一章中,我们将使用事件构建一个看板板。
问题
以下问题供你思考:
-
当页面重新加载时,可以使用
localStorage来维护购物车的状态吗? -
为什么我们不需要在
Store组件中调用StateHasChanged方法? -
你会如何更新购物车,以便一次可以添加多种类型的产品?
-
在使用 DI 时,各种服务生命周期之间有什么区别?
进一步阅读
以下资源提供了有关本章涵盖主题的更多信息:
-
想了解更多关于依赖注入(DI)的信息,请参阅
learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0。 -
想了解更多关于事件的信息,请参阅
learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/。
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第八章:使用事件构建看板
作为开发者,我们努力使我们的应用程序尽可能动态。为此,我们使用事件。事件是对象发送的消息,指示已发生动作。Razor 组件可以处理许多不同类型的事件。
在本章中,我们将学习如何在 Blazor WebAssembly 应用程序中处理不同类型的事件。我们将学习如何使用 lambda 表达式以及如何防止默认操作。我们还将学习如何使用任意参数和属性展开来简化我们对组件分配属性的方式。
本章中创建的项目将是一个使用拖放 API 的看板。看板以视觉方式表示流程的各个阶段的工作。我们的看板将由三个Dropzone组件组成,每个组件代表不同的阶段。最后,我们将使用任意参数和属性展开来创建一个组件,用于向我们的看板添加新任务。
需要处理事件。
内置事件参数
lambda 表达式。
在本章中,我们将涵盖以下主题:
-
事件处理
-
属性展开
-
任意参数
-
创建看板项目
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 免费社区版的说明,请参阅第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 存储库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter08。
《Code in Action》视频在此处可用:packt.link/Ch8。
事件处理
Razor 组件通过使用名为@on{EVENT}的 HTML 元素属性来处理事件,其中EVENT是事件名称。
以下代码在点击点击我按钮时调用OnClickHandler方法:
<button type="button" @onclick="OnClickHandler">
Click Me
</button>
@code {
private void OnClickHandler()
{
// ...
}
}
事件处理器会自动触发 UI 渲染。因此,在处理它们时,我们不需要调用StateHasChanged。事件处理器可以引用与事件相关联的任何参数。此外,它们可以用来调用同步和异步方法。
以下代码在复选框更改时调用异步的OnChangeHandlerAsync方法:
<input type="checkbox" @onchange="OnChangedHandlerAsync" />Is OK?
@code {
bool isOk;
private async Task OnChangedHandlerAsync(ChangeEventArgs e)
{
isOk = (bool)e.Value!;
// await ...
}
}
在前面的代码中,使用ChangeEventArgs类提供有关更改事件的信息。ChangeEventArgs类只有一个属性。它是Value属性,对于此对象,它可以是true或false。
提示
事件参数是可选的,并且只有在它们被方法使用时才应包含。
ChangeEventArgs 类继承自 EventArgs 类。ASP.NET Core 框架支持的 EventArgs 类也由 Blazor WebAssembly 框架支持。以下是支持的 EventArgs 列表:
-
ClipboardEventArgs -
DragEventArgs -
ErrorEventArgs -
EventArgs -
FocusEventArgs -
ChangeEventArgs -
KeyboardEventArgs -
MouseEventArgs -
PointerEventArgs -
WheelEventArgs -
ProgressEventArgs -
TouchEventArgs
EventArgs 类是前面每个类的基类。我们可以通过创建一个从 EventArgs 类派生的类来创建我们自己的自定义事件数据类。
到目前为止,我们已经探讨了调用不带参数或带有由事件自动提供的参数的方法的方法。然而,有时我们需要提供自己的参数。
Lambda 表达式
当我们需要向方法提供参数时,我们可以使用 lambda 表达式。Lambda 表达式用于创建匿名函数。它们使用 => 操作符来分隔参数和表达式的主体。
Lambda 表达式的主体可以使用两种形式。它们可以使用表达式或语句块作为其主体。在以下示例中,第一个按钮使用表达式,第二个按钮使用 statement 块:
<h1>@message</h1>
<button type="button"
@onclick="@(() => SetMessage("Blazor is Awesome!"))">
Who Is Awesome?
</button>
<button type="button"
@onclick="@(() => { @message = "Blazor Rocks!"; })">
Who Rocks?
</button>
@code{
private string? message;
private void SetMessage(string newMessage)
{
message = newMessage;
}
}
在前面的代码中,当点击 Who Is Awesome? 按钮时,lambda 表达式调用 SetMessage 方法来更新 message 字段的值。当点击 Who Rocks? 按钮时,语句 lambda 表达式使用语句来更新 message 字段的值。
提示
如果语句 lambda 的主体只包含一个语句,则括号是可选的。此外,尽管您可以在语句 lambda 的主体中包含任意数量的语句,但我们建议最多限制为两到三个语句。
阻止默认操作
有时,我们需要阻止与事件相关联的默认操作。我们可以通过使用 @on{EVENT}:preventDefault 指令属性来实现,其中 EVENT 是事件名称。
例如,在拖动元素时,默认行为阻止它被拖放到另一个元素中。然而,对于本章中的看板项目,我们需要将项目拖放到各种拖放区。因此,我们需要阻止这种行为。
以下代码阻止了 ondragover 默认行为的发生。通过阻止默认行为,我们将允许将元素拖放到用作拖放区的 div 元素中:
<div class="dropzone"
dropzone="true"
ondragover="event.preventDefault();">
</div>
聚焦元素
有时我们需要以编程方式将焦点给予 HTML 元素。在这些情况下,我们使用 ElementReference 类型的 FocusAsync 方法。ElementReference 通过向我们要给予焦点的 HTML 元素添加 @ref 属性来识别。要分配焦点到 HTML 元素,必须定义一个类型为 ElementReference 的字段。
以下代码在每次点击按钮时将输入元素的值添加到任务列表中,并将焦点设置回输入元素:
Focus.razor
@page "/focus"
<input type="text" @ref="taskInput" @bind-value="@taskName" />
<button type="button" @onclick="OnClickHandlerAsync">
Add Task
</button>
@foreach (var item in tasks)
{
<div>@item</div>
}
@code {
private string? taskName;
private ElementReference taskInput;
private List<string> tasks = new();
private async Task OnClickHandlerAsync()
{
tasks.Add(taskName!);
taskName = "";
await taskInput.FocusAsync();
}
}
在前面的代码中,taskInput 被定义为 ElementReference。它通过 @ref 属性与 input 元素相关联。在 OnClickHandlerAsync 事件中,调用了 FocusAsync 方法。结果是每次点击按钮时,焦点都会返回到 input 元素。
重要提示
由于 FocusAsync 方法依赖于 DOM,它仅在元素渲染后才能工作。
Blazor WebAssembly 框架使我们能够通过使用 @on{EVENT} 属性轻松访问事件。所有我们在 ASP.NET 框架中习惯使用的 EventArgs 都受支持。我们使用 lambda 表达式为被事件调用的方法提供参数。我们使用 preventDefault 指令属性来防止默认操作。最后,使用 ElementReference 类型的 FocusAsync 方法以编程方式将焦点分配给 HTML 元素。
在处理组件时,我们通常需要提供多个属性。使用属性展开,我们可以避免在 HTML 标记中直接分配属性。
属性展开
当子组件有许多参数时,在 HTML 中为每个值分配可能会很繁琐。为了避免这样做,我们可以使用属性展开。
使用属性展开,属性被捕获在一个字典中,然后作为一个单元传递给组件。每个字典条目添加一个属性。该字典必须实现 IEnumerable<KeyValuePair<string,object>> 或 IReadOnlyDictionary<string, object> 并具有字符串键。我们使用 @attributes 指令引用字典。
这是名为 BweButton 的组件的代码,它包含许多不同的参数:
BweButton.razor
<button type="@Type"
class="@Class"
disabled="@Disabled"
title="@Title"
@onclick="@ClickEvent">
@ChildContent
</button>
@code {
[Parameter] public string? Class { get; set; }
[Parameter] public bool Disabled { get; set; }
[Parameter] public string? Title { get; set; }
[Parameter] public string? Type { get; set; }
[Parameter] public EventCallback ClickEvent { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
}
这是用于渲染 BweButton 组件而不使用属性展开的示例标记:
<BweButton Class="button button-red"
Disabled="false"
Title="This is a red button"
Type="button"
ClickEvent="OnClickHandler">
Submit
</BweButton>
这是前面标记渲染的按钮:

图 8.1:渲染的 BweButton
这是我们在本节中用于样式化按钮的 CSS:
.button {
color: white;
cursor: pointer;
padding: 2em;
}
.button-red {
background-color: red;
}
.button-black {
background-color: black;
}
在前面的 CSS 中,button 类中的所有元素都将具有白色文本和 2em 的填充。button-red 类中的元素将具有红色背景色,而 button-black 类中的元素将具有黑色背景色。
通过使用属性展开,我们可以将前面的标记简化为以下内容:
<BweButton @attributes="InputAttributes"
ClickEvent="OnClickHandler">
Submit
</BweButton>
这是前面标记中使用的 InputAttributes 的定义:
public Dictionary<string, object> InputAttributes { get; set; } =
new ()
{
{ "Class", "button button-red"},
{ "Disabled", false},
{ "Title", "This is a red button" },
{ "Type", "submit" }
};
上述代码定义了传递给 BweButton 的 InputAttributes。生成的按钮与之前直接设置属性而不使用 InputAttributes 的按钮相同。
属性展开的真正威力在于它与任意参数结合时。
随意参数
在前面的例子中,我们使用了明确定义的参数来分配按钮的属性。一种更高效的方法是使用随意参数。随意参数是一个没有由组件明确定义的参数。Parameter 属性有一个 CaptureUnmatchedValues 属性,用于允许参数捕获不匹配其他任何参数的值。
这是我们的按钮的新版本,称为 BweButton2。它使用随意参数:
BweButton2.razor
<button @attributes="InputAttributes" >
@ChildContent
</button>
@code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? InputAttributes{get; set;}
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
前面的代码包括一个名为 InputAttributes 的参数,其 CaptureUnmatchedValues 属性设置为 true。
重要提示
一个组件只能有一个参数,其 CaptureUnmatchedValues 属性设置为 true。
这是用于渲染我们按钮新版本的更新后的标记:
<BweButton2 @attributes="InputAttributes2"
@onclick="OnClickHandler"
class="button button-black">
Submit
</BweButton2>
这是前面标记中使用的 InputAttributes2 的定义:
public Dictionary<string, object> InputAttributes2 { get; set; } =
new()
{
{ "class", "button button-red" },
{ "title", "This is another button" },
{ "name", "btnSubmit" },
{ "type", "button" },
{ "myAttribute", "123"}
};
尽管在按钮的新版本中,字典中的所有属性都没有被明确定义,BweButton2 仍然被渲染。在前面的例子中,类属性被设置了两次。
这是前面代码渲染的按钮:

图 8.2:使用随意参数渲染的 BweButton2
按钮现在是黑色的原因是因为按钮标记中的 @attributes 指令的位置。当属性被撒到元素上时,它们是从左到右处理的。因此,如果有重复的属性被分配,那么在顺序中出现的较晚的那个将被使用。
随意参数用于允许组件渲染之前未定义的属性。这对于支持大量自定义的组件非常有用,例如包含 input 元素的组件。
现在,让我们快速了解一下本章将要构建的项目。
创建看板板项目
在本章中,我们将构建的 Blazor WebAssembly 应用程序是一个看板(Kanban)板。这个看板将包含三个拖放区域:高优先级、中优先级和低优先级。我们可以在这些拖放区域之间拖放任务,并添加额外的任务。每当任务被拖放到不同的拖放区域时,任务上的徽章指示器将更新以匹配拖放区域的优先级。
以下是完成的应用程序的截图:

图 8.3:看板应用
此项目的构建时间大约为 45 分钟。
项目概述
将使用 Microsoft 的Blazor WebAssembly App Empty项目模板创建一个空的 Blazor WebAssembly 项目来创建KanbanBoard项目。首先,我们将向项目中添加Bootstrap。然后,我们将创建TaskItem类和一个Dropzone组件。我们将向Home页面添加三个Dropzone组件以创建看板。最后,我们将添加NewTask组件,以便我们能够向看板添加新任务。
创建看板项目
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们按照以下步骤进行:
-
打开 Visual Studio 2022。
-
点击创建新项目按钮。
-
按Alt+S键进入搜索模板文本框。
-
输入
Blazor并按Enter键。下面的截图显示了Blazor WebAssembly App Empty项目模板。
![图形用户界面,文本,应用程序,聊天或短信 自动生成的描述]()
图 8.4:Blazor WebAssembly App Empty 项目模板
-
选择Blazor WebAssembly App Empty项目模板并点击下一步按钮。
-
在项目名称文本框中输入
KanbanBoard并点击下一步按钮。这是用于配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 8.5:配置新项目对话框
提示
在前面的示例中,我们将
KanbanBoard项目放置在E:/Blazor文件夹中。然而,此项目的位置并不重要。 -
选择.NET 7.0作为要使用的框架版本。
-
选择配置为 HTTPS复选框。
-
取消选择ASP.NET Core 承载复选框。
-
取消选择渐进式 Web 应用程序复选框。
这是用于收集有关我们新项目额外信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 8.6:附加信息对话框
-
点击创建按钮。
我们已创建了一个空的KanbanBoard Blazor WebAssembly 项目。我们将使用 Bootstrap 的网格系统来布局我们的看板。
安装 Bootstrap
我们需要在我们的 web 应用程序中安装Bootstrap。我们按照以下步骤进行:
-
右键点击
wwwroot/css文件夹,从菜单中选择添加,客户端库选项。 -
在库搜索文本框中输入
bootstrap并按Enter键。 -
选择选择特定文件。
-
仅选择以下截图所示的css文件。
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 8.7:添加客户端库对话框
提示
虽然前面的截图选择了
Bootstrap的 5.2.2 版本,但你可以使用任何版本的Bootstrap来完成此项目。 -
点击安装按钮。
重要提示
安装
Bootstrap后,wwwroot/css文件夹中会新增一个文件夹。这个新文件夹包含了Bootstrap所需的所有 CSS 文件。在本项目中,我们只会使用bootstrap.min.css文件。 -
打开
wwwroot/index.html文件。 -
在
css/app.css样式表的链接之前添加以下标记:<link href="css/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> -
打开
MainLayout.razor文件。 -
更新
main元素为以下内容:<main class="container"> @Body </main>
前面的代码在页面主体周围添加了一个container。在本项目中,我们将使用 Bootstrap 的网格系统来布局我们的内容。它依赖于一系列容器、行和列。我们将在稍后添加行和列。
我们将使用看板板来优先处理任务。但首先,我们需要一种定义任务的方法。
添加以下类
我们需要添加一个TaskPriority枚举和一个TaskItem类。我们这样做如下:
-
右键点击
KanbanBoard项目,从菜单中选择添加,新建文件夹选项。 -
将新文件夹命名为
Models。 -
右键点击
Models文件夹,从菜单中选择添加,类选项。 -
将新类命名为
TaskPriority。 -
点击添加按钮。
-
将类替换为以下
TaskPriority枚举:public enum TaskPriority { High, Mid, Low } -
右键点击
Models文件夹,从菜单中选择添加,类选项。 -
将新类命名为
TaskItem。 -
点击添加按钮。
-
将以下属性添加到
TaskItem类中:public string? TaskName { get; set; } public TaskPriority Priority { get; set; }
我们已添加TaskPriority枚举和TaskItem类来表示看板板上的任务。接下来,我们需要创建拖放区域。
创建拖放组件
我们需要添加一个Dropzone组件。我们这样做如下:
-
右键点击
KanbanBoard项目,从菜单中选择添加,新建文件夹选项。 -
将新文件夹命名为
Shared。 -
右键点击
Shared文件夹,从菜单中选择添加,Razor 组件选项。 -
将新组件命名为
Dropzone。 -
点击添加按钮。
-
删除
h3元素。 -
添加以下
@using指令:@using KanbanBoard.Models -
将以下参数添加到代码块中:
[Parameter] public List<TaskItem> TaskItems { get; set; } = new(); [Parameter] public TaskPriority Priority { get; set; } [Parameter] public EventCallback<TaskPriority> OnDrop { get; set; } [Parameter] public EventCallback<TaskItem> OnStartDrag { get; set; }在前面的代码中,
TaskItems参数用于跟踪已拖放到Dropzone中的任务。Priority参数用于指示Dropzone中任务的优先级。OnDrop事件表示当任务被拖放到Dropzone时触发的事件,而OnStartDrag事件表示当任务从Dropzone拖动时触发的事件。 -
添加以下标记:
<div class="col"> <h2 style="">@Priority.ToString() Priority</h2> <div class="dropzone" ondragover="event.preventDefault();" @ondrop="OnDropHandler"> @foreach (var item in TaskItems .Where(q => q.Priority == Priority)) { } </div> </div>前面的标记通过优先级标记了
Dropzone,并通过防止ondragover事件的默认值,允许元素被拖放到其中。当元素被拖放到Dropzone时,会调用OnDropHandler方法。最后,它遍历TaskItems类中所有匹配Priority的项。 -
在
@foreach循环内添加以下标记:<div class="draggable" draggable="true" @ondragstart="@(() => OnDragStartHandler(item))"> @item.TaskName <span class="badge text-bg-secondary"> @item.Priority </span> </div>上述标记通过将
draggable属性设置为true使div元素可拖动。当元素被拖动时,将调用OnDragStartHandler方法。 -
将以下
OnDropHandler方法添加到代码块中:private void OnDropHandler() { OnDrop.InvokeAsync(Priority); }上述代码调用了
OnDrop方法。 -
将以下
OnDragStartHandler方法添加到代码块中:private void OnDragStartHandler(TaskItem task) { OnStartDrag.InvokeAsync(task); }
上述代码调用了 OnStartDrag 方法。
我们已经添加了一个 Dropzone 组件。现在我们需要给组件添加一些样式。
添加样式表
我们将通过 CSS 隔离向 Dropzone 组件添加样式表。我们这样做:
-
右键单击
Shared文件夹,从菜单中选择 添加,新项目 选项。 -
在 搜索 框中输入
css。 -
选择 样式表。
-
将样式表命名为
Dropzone.razor.css。 -
点击 添加 按钮。
-
输入以下样式:
.draggable { margin-bottom: 10px; padding: 10px 25px; border: 1px solid #424d5c; background: #ff6a00; color: #ffffff; border-radius: 5px; cursor: grab; } .draggable:active { cursor: grabbing; } .dropzone { padding: .75rem; border: 2px solid black; min-height: 20rem; }提示
您可以从 GitHub 仓库复制
Dropzone.razor.css文件。 -
打开
wwwroot/index.html文件。 -
在
head元素的底部取消注释以下link元素:<link href="KanbanBoard.styles.css" rel="stylesheet" />
我们已经完成了 Dropzone 组件的样式设置。现在我们可以组合看板了。
创建看板
我们需要添加三个 Dropzone 组件来创建我们的看板,每个类型一个。我们这样做:
-
打开
_Imports.razor文件。 -
添加以下
using语句:@using KanbanBoard.Models @using KanbanBoard.Shared -
打开
Pages\Index.razor文件。 -
移除
h1元素。 -
添加以下标记:
<PageTitle>Kanban Board</PageTitle> <div class="row"> <Dropzone Priority="TaskPriority.High" TaskItems="TaskItems" OnDrop="OnDrop" OnStartDrag="OnStartDrag" /> <Dropzone Priority="TaskPriority.Mid" TaskItems="TaskItems" OnDrop="OnDrop" OnStartDrag="OnStartDrag" /> <Dropzone Priority="TaskPriority.Low" TaskItems="TaskItems" OnDrop="OnDrop" OnStartDrag="OnStartDrag" /> </div>上述代码为每个优先级添加了三个
Dropzone组件。 -
添加以下代码块:
@code { public TaskItem? CurrentItem; List<TaskItem> TaskItems = new(); protected override void OnInitialized() { TaskItems.Add(new TaskItem { TaskName = "Call Mom", Priority = TaskPriority.High }); TaskItems.Add(new TaskItem { TaskName = "Buy milk", Priority = TaskPriority.Mid }); TaskItems.Add(new TaskItem { TaskName = "Exercise", Priority = TaskPriority.Low }); } }上述代码使用三个任务初始化了
TaskItems对象。 -
将
OnStartDrag方法添加到代码块中:private void OnStartDrag(TaskItem item) { CurrentItem = item; }上述代码将
CurrentItem的值设置为当前正在拖动的项目。当项目随后被放下时,我们将使用此值。当Dropzone组件触发@ondragstart事件时,它将调用此方法。 -
将
OnDrop方法添加到代码块中:private void OnDrop(TaskPriority priority) { CurrentItem!.Priority = priority; }上述代码将
CurrentItem的Priority属性设置为与CurrentItem放入的Dropzone相关的优先级。当Dropzone组件触发@ondrop事件时,它将调用此方法。 -
按 Ctrl+F5 启动应用程序,不进行调试。
-
将所有任务拖到 高优先级 拖放区域。
在将每个任务拖入 高优先级 拖放区域后,请确认任务的徽章已更新为 高。
我们已经创建了一个非常简单的包含三个项目的看板。让我们添加通过 UI 添加更多项目的功能。
创建 NewTask 组件
我们需要添加一个 NewTask 组件。我们这样做:
-
返回 Visual Studio。
-
右键单击
Shared文件夹,从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
NewTask。 -
点击 添加 按钮。
-
移除
h3元素。 -
添加以下标记:
<div class="row pt-3" > <div class="input-group mb-3"> <label class="input-group-text" for="inputTask"> Task </label> <input @ref="taskInput" type="text" id="inputTask" class="form-control" @bind-value="@taskName" @attributes="InputParameters" /> <button type="button" class="btn btn-outline-secondary" @onclick="OnClickHandlerAsync"> Add Task </button> </div> </div>上述标记包括一个标签、一个文本框和一个按钮。文本框包含一个
@ref属性,我们将在稍后使用它来设置文本框的焦点。这是我们在工作的
NewTask组件的截图:![图片]()
图 8.8:NewTask 组件
-
将以下代码添加到代码块中:
private string? taskName; private ElementReference taskInput; [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? InputParameters{ get; set; } [Parameter] public EventCallback<string> OnSubmit { get; set; }上述代码定义了一个参数,
InputParameters,它将被用于属性展开。 -
将
OnClickHandlerAsync方法添加到代码块中:private async Task OnClickHandlerAsync() { if (!string.IsNullOrWhiteSpace(taskName)) { await OnSubmit.InvokeAsync(taskName); taskName = null; await taskInput.FocusAsync(); } }
上述代码调用OnSubmit方法,将taskName字段设置为null,并将焦点设置到taskInput对象。
我们现在已经创建了NewTask组件。接下来,我们需要开始使用它。
使用 NewTask 组件
我们需要将NewTask组件添加到Home页面。我们这样做如下:
-
打开
Pages\Index.razor文件。 -
在
PageTitle组件下方添加以下标记:<NewTask OnSubmit="AddTask" @attributes="InputAttributes" /> -
将以下代码添加到代码块中:
public Dictionary<string, object> InputAttributes = new () { { "maxlength", "25" }, { "placeholder", "enter new task" }, { "title", "This textbox is for adding your tasks." } };在上述代码中,属性正在被设置,而没有被显式定义。
-
将
AddTask方法添加到代码块中:private void AddTask(string taskName) { var taskItem = new TaskItem() { TaskName = taskName, Priority = TaskPriority.High }; TaskItems.Add(taskItem); }上述代码将新项的优先级设置为
高,并将其添加到TaskItems对象中。 -
按Ctrl+F5以无调试模式启动应用程序。
-
输入一个新任务并点击添加任务按钮。
当点击
NewTask组件的添加任务按钮时,会调用AddTask方法。文本框会被清空,并且焦点会回到文本框。 -
输入另一个新任务并点击添加任务按钮。
-
拖放任务以更改它们的优先级。
我们已经增加了向看板添加新任务的功能。
摘要
你现在应该能够处理你的 Blazor WebAssembly 应用中的事件。你也应该对使用属性展开和任意参数感到舒适。
在本章中,我们介绍了事件处理。我们解释了如何使用EventArgs以及如何使用 lambda 表达式向方法提供参数。我们还解释了如何防止默认操作以及如何使用@ref属性以编程方式将焦点设置到特定元素。最后,我们介绍了属性展开和任意参数。
之后,我们使用了Blazor WebAssembly App Empty项目模板来创建一个新的项目,并将Bootstrap添加到项目中。接下来,我们向项目中添加了一个Dropzone组件,并使用它来创建看板。最后,我们在演示属性展开和任意参数的同时,增加了向看板添加任务的功能。
现在你已经知道如何在你的 Blazor WebAssembly 应用中处理不同类型的事件,你可以创建更响应式的应用程序。而且,由于你可以使用字典来传递显式声明的属性和隐式属性到组件中,你可以更快地创建组件,因为你不需要显式地定义每个参数。
在下一章中,我们将创建一个可以上传和读取 Excel 文件的应用程序。
问题
以下问题供您思考:
-
你如何更新看板,以便用户可以删除任务?
-
你为什么想在用于属性展开的字典中包含一个在组件上未定义的属性,无论是显式还是隐式?
-
DragEventArgs类的基类是什么?
进一步阅读
以下资源提供了关于本章涵盖主题的更多信息:
-
更多关于文档对象模型(DOM)事件的信息,请参阅
developer.mozilla.org/en-US/docs/Web/Events。 -
更多关于
EventArgs类的信息,请参阅learn.microsoft.com/en-us/dotnet/api/system.eventargs。 -
更多关于
DragEventArgs类的信息,请参阅learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.web.drageventargs。
第九章:上传和读取 Excel 文件
There are many different scenarios that require a web app to allow users to upload local files. Also, there are just as many different types of files that a user may need to upload.
在本章中,我们将学习如何使用 InputFile 组件上传不同类型的文件。我们将学习如何上传图像文件以及如何调整上传的图像大小。我们还将学习如何使用 Virtualize 组件进行虚拟化,仅渲染可见数据。最后,我们将学习如何使用 Open XML SDK 读取 Microsoft Excel 文件。
本章中创建的项目将是一个 Excel 读取器,它将允许我们上传 Excel 文件并在表中查看其内容,使用虚拟化。
解析各个部分
读取 Excel 文件 –
不复杂!
在本章中,我们将涵盖以下主题:
-
上传文件
-
使用虚拟化
-
读取 Excel 文件
-
创建 Excel 读取器项目
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 的免费社区版的说明,请参阅 第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter09。
The Code in Action video is available here: packt.link/Ch9.
上传文件
InputFile 组件是一个内置的 Blazor 组件,用于将文件上传到 Blazor 应用程序。它渲染一个 HTML input 元素,类型为 file,并为文件内容提供流。它位于 Microsoft.AspNetCore.Components.Forms 命名空间中。
InputFile 组件的 OnChange 事件用于设置在文件被选中时调用的回调。以下是一个在文件被选中时调用 OnChangeHandler 方法的 InputFile 组件示例:
<InputFile OnChange="OnChangeHandler"
accept="image/png, image/jpeg" />
这是前面示例的结果 HTML 标记:
<input accept="image/png, image/jpeg" type="file" _bl_2="">
在前面的 HTML 标记中,_bl_2 属性用于 Blazor 的内部处理,但其他一切都是典型的 input 元素。accept 属性用于过滤在 选择文件 对话框中显示的文件类型。
以下截图显示了前面的标记是如何渲染的:

图 9.1:渲染的 InputFile 组件
当点击 InputFile 组件的 选择文件 按钮时,将打开 选择文件 对话框,允许用户选择文件。以下是 选择文件 对话框的截图:

图 9.2:选择文件对话框
重要提示
虽然我们已经指出,只有 image/png 和 image/jpg 类型的文件应该被接受,但 选择文件 对话框允许用户通过文件类型选择器选择 所有文件 (.)。因此,永远无法保证用户选择的文件类型。
InputFileChangeEventArgs 类用于提供有关正在引发的 OnChange 事件的信息。以下代码将 selectedFile 字段设置为提供给 OnChangeHandler 方法的 InputFileChangeEventArgs 对象的 File 属性的值:
IBrowserFile? selectedFile;
private void OnChangeHandler(InputFileChangeEventArgs e)
{
selectedFile = e.File;
}
File 属性表示上传的文件,其类型为 IBrowserFile。
这些是 IBrowserFile 接口的属性:
-
ContentType– 文件的内容类型 -
LastModified– 文件的最后修改日期 -
Name– 文件名 -
Size– 文件的大小(以字节为单位)
重要提示
你永远不应该信任从互联网上传的文件。你应该将其视为可能对你的应用程序、服务器和网络构成安全风险的潜在威胁。ContentType、LastModified、Name 和 Size 属性都可以被操纵以损害你的系统,并且不可信。
IBrowserFile 接口只有一个方法。它是 OpenReadStream 方法,用于读取上传的文件。默认情况下,流的最大大小为 500 KB。但是,可以使用 maxAllowedSize 参数来增加流的最大大小。在以下示例中,流的最大大小被设置为 MAXFILESIZE 的值:
var stream = await file.OpenReadStream(MAXFILESIZE);
缩放图像
图像可能非常大。有时我们需要限制允许上传到应用程序中的图像的大小。在这些情况下,我们可以使用 RequestImageFileAsync 方法来缩放图像。以下是 RequestImageFileAsync 方法的参数:
-
Format– 新图像的格式 -
MaxWidth– 新图像的最大宽度 -
MaxHeight– 新图像的最大高度
重要提示
RequestImageFileAsync 方法不会验证图像是否有效。因此,任何结果都应被视为不可信。
当使用 RequestImageFileAsync 方法时,图像会使用提供的尺寸进行缩放,同时保留原始图像的宽高比。以下 UploadImage 组件会缩放上传的图像:
UploadImage.razor
@page "/uploadImage"
@using Microsoft.AspNetCore.Components.Forms
<PageTitle>Upload Image</PageTitle>
<h1>Upload Image</h1>
<InputFile OnChange="OnChangeHandler"
accept="image/png, image/jpeg" />
@if (@image != null)
{
<p>Old file size: @uploadedFile!.Size.ToString("N0") bytes</p>
<p>New file size: @resizedFile!.Size.ToString("N0") bytes</p>
<img src="img/data:@uploadedFile.ContentType;base64,@image" />
}
@code {
IBrowserFile? uploadedFile;
IBrowserFile? resizedFile;
string? image;
async Task OnChangeHandler(InputFileChangeEventArgs e)
{
uploadedFile = e.File;
resizedFile = await uploadedFile.RequestImageFileAsync(
uploadedFile.ContentType,
100,
100
);
var buffer = new byte[resizedFile.Size];
var stream = await resizedFile.OpenReadStream()
.ReadAsync(buffer);
image = Convert.ToBase64String(buffer);
}
}
上述代码使用 RequestImageFileAsync 方法将图像缩放为 100 x 100 像素。它使用 OpenReadStream 方法读取文件,然后将其编码为 Base64 字符串。最后,当 image 属性的值不再是 null 时,渲染图像的原始大小、缩放后图像的新大小以及缩放后的图像本身。
提示
虽然我们的示例直接将文件读入内存,但我们建议你直接将流复制到磁盘上的文件或上传到外部服务,例如 Azure Blob 存储。
这是一张使用 上传图片 页面上传的一张非常大的照片的截图,照片是在我们前往美国维尔京群岛圣约翰的旅行中拍摄的:

图 9.3:上传图片结果
在前面的示例中,我们能够将文件大小从 6,413,880 字节减少到 4,719 字节。
处理多个文件
默认情况下,InputFile 组件只允许选择单个文件。然而,可以通过使用 multiple 属性来使用 InputFile 组件上传多个文件,如下面的示例所示:
<InputFile OnChange="OnChange" multiple />
当使用 multiple 属性时,InputFileChangeEventArgs 的 FileCount 属性用于确定已上传的文件数量。当允许上传多个文件时,而不是使用 File 属性来访问文件,我们使用 InputFileChangeEventArgs 的 GetMultipleFiles 方法来遍历已上传的文件列表。
在以下示例中,使用 GetMultipleFiles 方法来返回所选文件的列表。然后使用 foreach 循环遍历文件列表:
async Task OnChange(InputFileChangeEventArgs e)
{
var files = e.GetMultipleFiles();
foreach (var file in files)
{
// do something
}
}
重要提示
您不能向已选文件的列表中添加文件。每次使用 InputFile 时,之前的文件列表都会被新的文件列表替换。
InputFile 组件可以通过使用 multiple 属性将一个或多个文件一次性上传到 Blazor WebAssembly 应用程序。
InputFile 组件可以用来上传许多不同类型的文件,例如 Excel 文件和图片文件。通过使用 RequestImageFileAsync 方法,我们可以调整上传的图片大小。我们可以使用 multiple 属性来允许用户上传多个文件。
使用虚拟化
有时候我们需要处理大量项目。在这种情况下,渲染每个项目并不高效。仅渲染项目子集会更高效。这种技术称为 虚拟化。
内置的 Virtualize 组件用于渲染集合的可见项目。具体来说,当我们在使用循环渲染项目集合并使用滚动来限制在任何给定时刻可见的项目数量时,会使用它。Virtualize 组件计算可见项目的列表并仅渲染这些项目。由于它不渲染不可见的项目,因此比使用渲染集合中每个项目的方程序能效更高。它位于 Microsoft.AspNetCore.Components.Web.Virtualization 命名空间中。
重要提示
当使用 Virtualize 组件时,所有项目必须在像素中具有相同的高度。
Virtualize<TItem> 类包含以下属性:
-
ItemContent– 项目模板。仅在使用Placeholder属性时需要。 -
Items– 项目集合。此属性不能与ItemsProvider方法一起使用。 -
ItemSize– 每个项目的高度(以像素为单位)。默认值为 50 像素。 -
ItemsProvider– 异步检索项目集合的函数。此属性不能与Items方法一起使用。 -
OverscanCount– 在可见区域前后应渲染的项目数量。在滚动时,这将有助于减少渲染量。默认值为 3。 -
Placeholder– 在组件等待ItemsProvider提供项目时渲染的内容。此属性不能与Items方法一起使用。 -
SpacerElement– 用于显示每个项目的元素类型。默认为div。
在我们家的附近,有一个气象站,它持续记录当前的温度和湿度。每天收集了数千个数据点。
这是用于从气象站收集数据的 Weather 类:
public class Weather
{
public DateTime Date { get; set; }
public int Temperature { get; set; }
public int Humidity { get; set; }
}
以下代码将使用 foreach 循环显示每个数据点:
@using Microsoft.AspNetCore.Components.Web.Virtualization
<div style="height:200px;overflow-y:scroll">
@foreach (Weather weather in weatherHistory)
{
<p>
@weather.Date.ToShortTimeString():
Temp:@weather.Temperature
Humidity:@weather.Humidity
</p>
}
</div>
在前面的代码中,尽管 foreach 循环位于限制显示行数的 div 元素内,但 UI 仍然需要在将控制权返回给用户之前渲染所有行。由于 Weather 对象集合包含数千条记录,用户在等待 UI 渲染所有行时将经历一些延迟。我们可以使用 Virtualize 组件只渲染显示的数据。
渲染本地数据
以下代码使用 Virtualize 组件而不是 foreach 循环来从内存中渲染数据:
<div style="height:200px;overflow-y:scroll">
<Virtualize Items="@weatherHistory" Context="weather">
<p>
@weather.Date.ToShortTimeString():
Temp:@weather.Temperature
Humidity:@weather.Humidity
</p>
</Virtualize>
</div>
前面的代码将允许页面更快地加载,因为只有位于 div 元素内部的行才会被渲染。Virtualize 组件计算容器内可以容纳的项目数量,并且只渲染这些项目。当用户滚动浏览项目时,Virtualize 组件确定需要渲染哪些项目,并将它们渲染出来。
渲染远程数据
以下代码使用 ItemsProvider 方法从远程数据源获取要渲染的项目列表:
<div style="height:200px;overflow-y:scroll">
<Virtualize ItemsProvider="@LoadWeather"
Context="weather"
ItemSize="10"
OverscanCount="2">
<ItemContent>
<p>
@weather.Date.ToShortTimeString():
Temp:@weather.Temperature
Humidity:@weather.Humidity
</p>
</ItemContent>
<Placeholder>
<p><em>Loading Weather...</em></p>
</Placeholder>
</Virtualize>
</div>
在前面的示例中,当 Virtualize 组件需要更新正在渲染的 Weather 对象列表时,会调用 LoadWeather 方法。
这是 LoadWeather 方法的简单实现:
private async ValueTask<ItemsProviderResult<Weather>>
LoadWeather(ItemsProviderRequest request)
{
return new ItemsProviderResult<Weather>(
await FetchWeather(request.StartIndex, request.Count),
totalCount);
}
private async Task<IEnumerable<Weather>>
FetchWeather(int start, int count)
{
// call a service
}
在前面的代码中,LoadWeather 方法接受 ItemsProviderRequest 并返回 ItemsProviderResult。需要注意的是,ItemsProviderRequest 包含一个 StartIndex 属性和一个 Count 属性。StartIndex 是请求数据的起始索引,而 Count 是请求的项目数量。
在本章的项目中,我们将从上传到我们应用程序的 Excel 电子表格中读取值。因此,我们需要学习如何从 Excel 电子表格中读取。
读取 Excel 文件
我们可以使用 Open XML SDK 来读取和写入 Microsoft Excel 文件。它为我们提供了处理 Excel 文件、Word 和 PowerPoint 文件的工具。要使用 Open XML SDK,我们需要将 DocumentFormat.OpenXml NuGet 包添加到我们的项目中。
带有 XLSX 文件扩展名的现代 Excel 文件是由压缩的 XML 文件集合组成的。要查看单个文件,将文件扩展名从 XLSX 更改为 ZIP,并使用 .zip 查看器查看文件。你也可以提取文件。
当使用 Open XML SDK 时,Excel 文档用 SpreadsheetDocument 类表示。这是该类中元素的层次结构:
-
workbook– 文档的根元素 -
sheets– 工作表的容器 -
sheet– 指向工作表定义文件的指针 -
worksheet– 包含工作表数据的定义 -
sheetData– 数据 -
row– 数据行 -
c– 数据行中的一个单元格 -
v– 单元格的值
为了演示 Excel 文件的格式,我们创建了一个名为 Sample.xlsx 的示例 Excel 文件。该示例工作簿包含两个工作表。第一个工作表标题为 Numbers,第二个工作表标题为 Welcome。Numbers 工作表包含两行数字,而 Welcome 工作表在 A1 单元格中包含字符串 Hello World。
这是 Sample.xlsx 文件的截图:

图 9.4:Sample.xlsx
重要提示
你可以从 GitHub 仓库下载 Sample.xlsx 的副本。
如果我们将 Sample.xlsx 的文件名更改为 Sample.zip 并提取所有文件,这将得到以下文件结构:

图 9.5:Sample.zip 的文件结构
如果你熟悉 Microsoft Excel,xl 文件夹下的文件将对你来说很熟悉。xl 文件夹包含一个 workbook.xml 文件和一个 worksheets 文件夹,每个工作表都有一个文件。
workbook.xml 文件列出了工作簿中的所有工作表。以下来自 workbook.xml 文件的标记显示了 sheets 元素的内容:
<sheets>
<sheet name="Numbers" sheetId="1" r:id="rId1"/>
<sheet name="Welcome" sheetId="2" r:id="rId2"/>
</sheets>
这是遍历给定 SpreadsheetDocument 中所有工作表的代码:
private List<string> ReadSheetList(SpreadsheetDocument doc)
{
List<string> mySheets = new();
WorkbookPart wbPart = doc.WorkbookPart;
Sheets sheets = wbPart.Workbook.Sheets;
foreach (Sheet item in sheets)
{
mySheets.Add(item.Name);
}
return mySheets;
}
worksheets 文件夹包含一个文件,对应于 workbook.xml 文件中 sheets 元素中标识的每个工作表。在我们的例子中,它们被命名为 sheet1.xml 和 sheet2.xml。以下来自 sheets1.xml 文件的标记显示了 Numbers 工作表的 sheetData 元素的内容:
<sheetData>
<row r="1" spans="1:3" x14ac:dyDescent="0.25">
<c r="A1"><v>1</v></c>
<c r="B1"><v>2</v></c>
<c r="C1"><v>3</v></c>
</row>
<row r="2" spans="1:3" x14ac:dyDescent="0.25">
<c r="A2"><v>4</v></c>
<c r="B2"><v>5</v></c>
<c r="C2"><v>6</v></c>
</row>
</sheetData>
如你所见,sheetData 由一系列行组成。每一行有几个单元格,每个单元格都有一个值。
以下来自 sheets2.xml 文件的标记显示了 Welcome 工作表的 sheetData 元素的内容:
<sheetData>
<row r="1" spans="1:1" x14ac:dyDescent="0.25">
<c r="A1" t="s"><v>0</v></c>
</row>
</sheetData>
我们期望 A1 单元格的值为Hello World。然而,它的值却是0(零)。原因是所有字符串都存储在sharedStrings.xml文件中,而sheetData中只包含字符串在sharedStrings.xml文件中的位置索引。每个唯一的字符串在sharedStrings.xml文件中只包含一次。
这是sharedStrings.xml文件中的数据:
<sst count="1" uniqueCount="1">
<si><t>Helllo World</t></si>
</sst>
以下代码遍历SpreadsheetDocument中的每个工作表,并返回每个工作表第一行第一个单元格的值:
private List<string> ReadFirstCell(SpreadsheetDocument doc)
{
List<string> A1Value = new();
WorkbookPart wbPart = doc.WorkbookPart;
var stringTable = wbPart
.GetPartsOfType<SharedStringTablePart>()
.FirstOrDefault();
Sheets sheets = wbPart.Workbook.Sheets;
foreach (Sheet item in sheets)
{
WorksheetPart wsPart =
(WorksheetPart)(wbPart.GetPartById(item.Id));
SheetData sheetData = wsPart
.Worksheet.Elements<SheetData>().First();
Row row = sheetData.Elements<Row>().First();
Cell cell = row.Elements<Cell>().First();
string value = cell.CellValue.Text;
if (cell.DataType != null)
{
if (cell.DataType.Value == CellValues.SharedString)
{
value = stringTable
.SharedStringTable
.ElementAt(int.Parse(value)).InnerText;
}
}
A1Value.Add(value);
}
return A1Value;
}
您已经学会了如何使用Open XML SDK读取 Excel 文件。Open XML SDK非常强大。它不仅可以读取 Excel 文件,还可以创建新的 Excel 文件和更新现有的文件。它还可以用于创建、读取和更新 Word 和 PowerPoint 文件。
现在,让我们快速了解一下本章将要构建的项目。
创建 Excel 读取器项目
本章我们将构建的 Blazor WebAssembly 应用程序是一个 Excel 文件读取器。我们将使用InputFile组件上传 Excel 文件。然后,我们将使用Open XML SDK遍历 Excel 文件中某个工作表的行。最后,我们将使用Virtualize组件在 HTML 表中渲染 Excel 文件中的数据。
以下是完成的应用程序截图:

图 9.6:Excel 读取器应用
此项目的构建时间大约为 45 分钟。
项目概述
将使用 Microsoft 的Blazor WebAssembly App Empty项目模板创建一个空的 Blazor WebAssembly 项目来创建ExcelReader项目。首先,我们将向项目中添加Open XML SDK。然后,我们将添加一个模型来捕获我们从 Excel 文件中读取的信息。我们将使用InputFile组件上传 Excel 文件。我们将使用Open XML SDK读取 Excel 文件。最后,我们将使用Virtualize组件显示 Excel 文件中的数据。
创建 Excel 读取器项目
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击创建新项目按钮。
-
按Alt+S键进入搜索模板文本框。
-
输入
Blazor并按Enter键。以下截图显示了Blazor WebAssembly App Empty项目模板:
![图形用户界面、文本、应用程序、聊天或文本消息描述自动生成]()
图 9.7:Blazor WebAssembly App Empty 项目模板
-
选择Blazor WebAssembly App Empty项目模板并点击下一步按钮。
-
在项目名称文本框中输入
ExcelReader并点击下一步按钮。这是配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 9.8:配置新项目对话框
TIP
在前面的例子中,我们将
ExcelReader项目放置在E:/Blazor文件夹中。然而,这个项目的位置并不重要。 -
选择.NET 7.0作为要使用的Framework版本。
-
选择Configure for HTTPS复选框。
-
取消选择ASP.NET Core Hosted复选框。
-
取消选择Progressive Web Application复选框。
这是我们用于收集有关新项目额外信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 9.9:额外信息对话框
-
点击Create按钮。
我们创建了一个空的ExcelReader Blazor WebAssembly 项目。我们将使用Open XML SDK来读取 Microsoft Excel 文件。
安装 Open XML SDK
我们需要将Open XML SDK安装到我们的 web 应用中。我们这样做如下:
-
从 Visual Studio 菜单中选择Tools,NuGet Package Manager,Package Manager Console以打开Package Manager Console。
-
输入以下命令:
Install-Package DocumentFormat.OpenXml -
按下Enter键。
Open XML SDK现在已安装。
现在,我们需要添加一个类来包含我们从 Excel 文件中读取的信息。
添加 Medals 类
我们将要上传的 Excel 文件将包括自 1896 年以来每个夏季奥运会每个国家获得的奖牌数量。我们需要添加一个Medals类来收集这些信息。我们这样做如下:
-
右键点击ExcelReader项目,从菜单中选择Add,New Folder选项。
-
将新文件夹命名为
Models。 -
右键点击Models文件夹,从菜单中选择Add,Class选项。
-
将新类命名为
Medals。 -
点击Add按钮。
-
将以下属性添加到
Medals类中:public int Year { get; set; } public string? Country { get; set; } public int Gold { get; set; } public int Silver { get; set; } public int Bronze { get; set; }
我们已经添加了Medals类来捕获 Excel 文件中的数据。接下来,我们需要添加上传我们想要读取的 Excel 文件的能力。
上传 Excel 文件
我们将使用InputFile组件来选择和上传 Excel 文件。我们这样做如下:
-
打开
Pages/Index.razor文件。 -
删除
h1元素。 -
添加以下
using语句:@using Microsoft.AspNetCore.Components.Forms; -
添加以下标记:
<PageTitle>Excel Reader</PageTitle> <InputFile OnChange="@SelectFile" accept=".xlsx" /> @if (file != null) { if (errorMessage == null) { <p> <div>File Name: @file.Name</div> <div> File Size: @file.Size.ToString("N0") bytes </div> <div>Content type: @file.ContentType</div> </p> <button type="button">Read file</button> } <p>@errorMessage</p> } @code { }上述标记包括一个
InputFile组件和一个if语句。如果file不为空,则显示文件名、大小和内容类型。 -
将以下代码添加到代码块中:
IBrowserFile? file; int MAXFILESIZE = 50000; string? errorMessage; private void SelectFile(InputFileChangeEventArgs e) { file = e.File; errorMessage = null; if ((file.Size >= MAXFILESIZE) || (file.ContentType != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) { errorMessage = "Invalid file"; } }上述代码检查文件的大小和内容类型,如果它们无效,则显示错误消息。始终设置最大文件大小是一个好习惯,因为文件越大,处理它所需的资源就越多。
-
按Ctrl+F5以无调试模式启动应用程序。
-
点击Choose file按钮。
-
选择
SummerOlympics.xlsx文件,并点击Open按钮。以下截图显示了结果:
![图形用户界面,文本自动生成描述]()
图 9.10:选择 SummerOlympics.xlsx
重要提示
您必须从 GitHub 存储库下载
SummerOlympics.xlsx文件。它包含两个工作表:olympic_hosts和olympic_medals。我们将使用olympic_medals工作表。它包括夏季奥运会的按国家和年份的奖牌计数。 -
点击读取文件按钮。
-
验证没有任何操作发生。
我们已成功上传 Excel 文件,但当我们点击读取文件按钮时,没有任何操作。现在我们需要编写代码来从 Excel 文件中读取数据。
读取 Excel 文件
我们将使用SpreadsheetDocument类从 Excel 文件中读取数据。我们这样做如下:
-
返回 Visual Studio。
-
打开
Pages\Index.razor文件。 -
添加以下
using语句:@using DocumentFormat.OpenXml; @using DocumentFormat.OpenXml.Packaging; @using DocumentFormat.OpenXml.Spreadsheet; -
在代码块中添加以下字段:
bool loaded; int rowCount;loaded字段将用于切换用于显示数据的标记,而rowCount字段将用于存储 Excel 文件中的行数。 -
在代码块中添加以下
ReadFile方法:private async Task ReadFile() { try { var stream = new MemoryStream(); await file.OpenReadStream(MAXFILESIZE) .CopyToAsync(stream); SpreadsheetDocument doc = SpreadsheetDocument.Open(stream, false); WorkbookPart wbPart = doc.WorkbookPart; var sheet = wbPart.Workbook .Descendants<Sheet>() .Where(s => s.Name == "olympic_medals") .FirstOrDefault(); WorksheetPart wsPart = (WorksheetPart)(wbPart.GetPartById(sheet.Id)); SheetData sheetData = wsPart.Worksheet.Elements<SheetData>().First(); rowCount = sheetData.Elements<Row>().Count(); loaded = true; } catch (Exception) { errorMessage = "Invalid Excel file"; } }前面的代码将
rowCount的值设置为olympic_medals工作表的行数。 -
将读取文件按钮更新为以下内容:
<button type="button" @onclick="ReadFile"> Read file </button>当点击读取文件按钮时,将调用
ReadFile方法。 -
在读取文件按钮之后添加以下
if语句:if (!loaded) { <p><em>Loading...</em></p> } else { <p>Rows: @rowCount</p> }前面的代码将在文件加载后显示
rowCount。 -
按Ctrl+F5以无调试模式启动应用程序。
-
点击选择文件按钮。
-
选择
SummerOlympics.xlsx文件并点击打开按钮。 -
点击读取文件按钮。
-
验证是否显示了正确数量的行。
我们已读取SummerOlympics.xlsx文件中olympic_medals工作表的行数。接下来,我们将通过遍历工作表中的每一行来填充奖牌集合。
填充奖牌集合
我们需要遍历所有行以填充奖牌集合。我们这样做如下:
-
返回 Visual Studio。
-
打开
Pages\Index.razor文件。 -
添加以下
using语句:@using ExcelReader.Models; @using System.Collections.ObjectModel; -
在代码块中添加以下字段:
Collection<Medals> allMedals = new();allMedals字段将用于存储从 Excel 文件中读取的数据。 -
在设置
rowCount的代码之后,在ReadFile方法中添加以下代码:var stringTable = wbPart .GetPartsOfType<SharedStringTablePart>() .FirstOrDefault(); foreach (Row r in sheetData.Elements<Row>()) { if (r.RowIndex! == 1) { continue; }; int col = 1; var medals = new Medals(); foreach (Cell c in r.Elements<Cell>()) { string value = c.InnerText; if (c.DataType != null) { if (c.DataType.Value == CellValues.SharedString) { value = stringTable.SharedStringTable .ElementAt(int.Parse(value)) .InnerText; } } switch (col) { case 1: medals.Year = int.Parse(value); break; case 2: medals.Country = value; break; case 3: medals.Gold = int.Parse(value); break; case 4: medals.Silver = int.Parse(value); break; case 5: medals.Bronze = int.Parse(value); break; default: break; } col = col + 1; } allMedals.Add(medals); }前面的代码首先加载
stringTable。然后,它确定单元格的value并根据正在读取的列更新Medals对象的相应属性。如果单元格是SharedString类型,它将使用stringTable确定其值。 -
按Ctrl+F5以无调试模式启动应用程序。
-
点击选择文件按钮。
-
选择
SummerOlympics.xlsx文件并点击打开按钮。 -
点击读取文件按钮。
-
验证是否仍然显示了正确数量的行。
我们正在将所有行读入奖牌的集合中,但我们没有渲染它们。接下来,我们需要将它们渲染到屏幕上。
渲染奖牌集合
我们需要将Virtualize组件添加到Home页面以渲染数据。我们这样做如下:
-
打开
Pages\Index.razor文件。 -
添加以下
using语句:@using Microsoft.AspNetCore.Components.Web.Virtualization -
在显示
rowCount的p元素下方添加以下标记:<div style="height:200px;overflow-y:scroll" tabindex="-1"> <table width="450"> <thead style="position: sticky; top: 0; background-color: silver"> <tr> <th>Year</th> <th width="255">Country</th> <th>Gold</th> <th>Silver</th> <th>Bronze</th> </tr> </thead> <tbody> <Virtualize Items="@allMedals" SpacerElement="tr"> <tr> <td align="center">@context.Year</td> <td>@context.Country</td> <td align="center">@context.Gold</td> <td align="center"> @context.Silver </td> <td align="center"> @context.Bronze </td> </tr> </Virtualize> </tbody> </table> </div>上述标记使用
Virtualize组件来显示集合中的每个对象。 -
按Ctrl+F5启动应用程序而不进行调试。
-
点击选择文件按钮。
-
选择
SummerOlympics.xlsx文件并点击打开按钮。 -
点击读取文件按钮。
-
滚动项目列表。
我们已经添加了使用Virtualize组件显示集合中所有项的功能。
摘要
现在,你应该能够将文件上传到你的 Blazor WebAssembly 应用中。你应该能够在处理大量数据集时使用虚拟化来更快地渲染你的页面。最后,你应该对与 Microsoft Excel 文件一起工作感到舒适。
在本章中,我们解释了如何上传文件以及如何调整图像文件的大小。我们还介绍了如何使用虚拟化。最后,我们介绍了Open XML SDK并解释了如何使用它来读取 Excel 文件。
之后,我们使用Blazor WebAssembly App Empty项目模板创建了一个新项目,并将Open XML SDK添加到项目中。接下来,我们添加了一个InputFile组件来上传 Excel 文件到应用中。我们使用Open XML SDK读取特定工作表中的行数。然后,我们遍历所选工作表中的所有行,并将它们的值存储在一个集合中。最后,我们使用Virtualize组件来显示集合中的所有项目。
现在你已经知道如何将文件上传到你的 Web 应用中,你的用户可以用各种不同的格式向你的应用程序提供数据。在这个项目中,我们使用了 Excel 文件,但你很容易将你学到的知识应用到其他类型的文件上。
在下一章中,我们将使用 SQL Server 和 ASP.NET Web API 构建一个任务管理器。
问题
以下问题供你思考:
-
SummerOlypics.xlsx文件包含两个工作表。在显示之前,你如何将两个工作表中的数据合并? -
调整图像大小有哪些好处?
-
在使用
Virtualize组件时始终包含Placeholder是否是一个好的做法? -
Open XML SDK能否用来创建一个新的 Excel 文件? -
在使用
InputFile组件时,如何避免将整个文件读入内存?
进一步阅读
以下资源提供了有关本章涵盖主题的更多信息:
-
关于上传文件时的安全最佳实践,请参阅
learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads. -
关于 GitHub 上的
Virtualize组件源代码,请参阅github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Virtualization/Virtualize.cs. -
关于
Open XML SDK的更多信息,请参阅learn.microsoft.com/en-us/office/open-xml/open-xml-sdk. -
关于 Open XML 规范的更多信息,请参阅
www.ecma-international.org/publications-and-standards/standards/ecma-376/.
第十章:使用 Azure Active Directory 保护 Blazor WebAssembly 应用程序
安全性很重要。大多数应用程序在用户可以访问应用程序提供的所有功能之前,都需要用户提供其凭据。管理用户名、密码、角色和组可能会很繁琐且复杂。使用Azure Active Directory(Azure AD)可以使其变得简单。Azure AD 是云中的身份提供者。
本章中我们将创建的项目将允许用户查看在用户通过 Azure AD 进行身份验证后,Azure AD 返回的令牌所提供的声明。我们将使用Microsoft 身份验证库(MSAL)从 Azure AD 获取JSON Web 令牌(JWTs)。我们将使用Open ID Connect(OIDC)端点进行用户身份验证。OIDC 是在行业标准 OAuth 2.0 协议之上构建的一个简单的身份层。它允许客户端根据身份提供者(如 Duende Identity Server 或 Azure AD)执行的认证来验证用户的身份。
在本章中,我们将学习身份验证和授权之间的区别。我们将学习如何使用RemoteAuthenticationView组件来处理身份验证每个阶段所需的各项操作。我们还将学习如何使用CascadingAuthenticationState组件将其身份验证状态与其每个子组件共享。最后,我们将学习如何通过使用Authorize属性和AuthorizeView组件来控制呈现给用户的内容。
本章中我们将创建的项目将是一个声明读取器。它将允许属于 Azure AD 中特定组的用户查看在身份验证后 Azure AD 返回的令牌的内容。如果用户未进行身份验证或不属于适当的组,他们将收到警告消息。
你是谁?你有什么
你是否有权限执行?
您的身份。
在本章中,我们将涵盖以下主题:
-
理解身份验证和授权之间的区别
-
与身份验证一起工作
-
使用授权控制用户界面
-
创建声明查看器项目
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 免费社区版的说明,请参阅第一章,Blazor WebAssembly 简介。由于我们将使用 Azure AD 进行身份验证,您需要在 Microsoft Azure 上有一个账户。如果您没有 Microsoft Azure 账户,请参阅第一章,Blazor WebAssembly 简介,以创建一个免费账户。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter10.
代码在行动视频在此处可用:packt.link/Ch10。
理解认证和授权之间的区别
认证和授权是同一枚安全硬币的两面。认证是从用户那里获取凭证以验证用户身份的过程。授权是检查用户访问特定资源的权限的过程。
认证始终先于授权。

图 10.1:认证与授权对比
前面的图像说明了认证和授权之间的区别。图像的左侧显示了一个用于确定用户身份的示例登录屏幕。图像的右侧显示了用户属于或不属于的组或角色列表,这用于确定用户可以做什么。
认证
Blazor 提供了 RemoteAuthenticatorView 组件,以简化创建各种认证页面的过程。此组件在认证操作之间持久化和控制状态。
这是对认证工作原理的高级解释:
-
匿名用户尝试登录或请求带有
Authorize属性的页面。 -
用户将被重定向到
/authentication/login页面。 -
用户输入他们的凭证。
-
如果他们已认证,他们将被重定向到
/authentication/login-callback页面。 -
然而,如果他们未认证,他们将被重定向到
/authentication/login-failed页面。
这是依赖于 RemoteAuthenticatorView 组件处理各种认证操作的示例 Authentication 组件的代码:
Authentication.razor
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action">
<LoggingIn>
Please sign in to your account ...
</LoggingIn>
</RemoteAuthenticatorView>
@code{
[Parameter] public string? Action { get; set; }
}
在前面的代码中,Action 属性由路由确定,而 LoggingIn 属性用于定义用户登录时渲染的 UI。LoggingIn 属性是一个 RenderFragment。我们不需要提供 LoggingIn 属性,因为 RemoteAuthenticatorView 组件已经定义了一个默认值。我们只将其包括作为示例。RemoteAuthenticatorView 组件中所有类型为 RenderFragment 的属性都由框架提供了默认值。
在前面的代码中,RemoteAuthenticatorView 组件只定义了两个属性。然而,还有许多其他属性可用。
这些是 RemoteAuthenticatorView 类的属性:
-
Action: 当前操作。选项包括LogIn、LogInCallback、LogInFailed、LogOut、LogOutCallback、LogOutFailed、LogOutSucceeded、Profile和Register。 -
ApplicationPaths:各种认证页面的路径。由于我们将使用每个路径的默认值,因此我们不会使用此属性。 -
AuthenticationState: 认证状态。在认证操作期间保持持久。它是TAuthenticationState类型。 -
CompletingLoggingIn: 当处理LogInCallback时显示的 UI。它是一个RenderFragment。 -
CompletingLogOut: 当处理LogOutCallback时显示的 UI。它是一个RenderFragment。 -
LoggingIn: 当处理LogIn时显示的 UI。它是一个RenderFragment。 -
LogInFailed: 当处理LogInFailed时显示的 UI。它是一个RenderFragment。 -
LogOut: 当处理LogOut时显示的 UI。它是一个RenderFragment。 -
LogOutFailed: 当处理LogOutFailed时显示的 UI。它是一个RenderFragment。 -
LogOutSucceeded: 当处理LogOutSucceeded时显示的 UI。它是一个RenderFragment。 -
OnLogInSucceeded: 当登录操作成功时调用的回调事件。 -
OnLogOutSucceeded: 当注销操作成功时调用的回调事件。 -
Registering: 当处理Register时显示的 UI。它是一个RenderFragment。 -
UserProfile: 当处理Profile时显示的 UI。它是一个RenderFragment。
使用 RemoteAuthenticatorView 组件可以轻松处理认证过程。
授权
在 Blazor WebAssembly 应用程序中,授权检查都在客户端处理。由于恶意用户可以更改客户端代码的行为,我们的授权检查可能会受到损害。因此,我们只会使用授权来处理根据用户权限而变化的用户界面差异。
提示
永远不要相信客户端!
只有通过使用后端服务器,才能强制执行真实的安全。我们无法在客户端认证我们的用户,也无法在客户端可靠地授权他们的操作。
认证和授权协同工作以保护我们的应用程序。认证用于确定用户是谁,而授权用于确定他们可以做什么。
除了 RemoteAuthenticatorView 组件之外,Blazor WebAssembly 还提供了一些其他内置组件,以帮助我们处理认证。
与认证一起工作
CascadingAuthenticationState 组件和 AuthorizeRouteView 组件协同工作,使认证更简单。CascadingAuthenticationState 组件负责将用户的认证状态级联到所有后代。它通常用于包装 Router 组件。
在以下示例中,App 组件依赖于 CascadingAuthenticationState 组件,为所有可路由组件提供用户的认证状态:
重要提示
有时我们将可路由组件称为页面。
App.razor
@inject NavigationManager Navigation
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing here.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
在前面的标记中,Found 属性包含以下 AuthorizeRouteView 组件:
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated != true)
{
Navigation.NavigateToLogin($"authentication/login");
}
else
{
<p>
ERROR: You are not authorized to access
this page.
</p>
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
在前面的代码中,Router 组件用于路由请求。如果找到有效的路由,它将使用 AuthorizeRouteView 组件而不是 RouteView 组件来确定用户是否有权查看页面。如果他们没有授权并且尚未认证,它将重定向用户到 /authentication/login 页面。然而,如果他们没有授权并且已经认证,它将渲染错误信息。
由于 App 组件正在使用 CascadingAuthenticationState 组件,因此 Task<AuthenticationState> 级联参数被提供给每个页面。以下代码使用它来填充 userName 字段:
[CascadingParameter]
private Task<AuthenticationState> authStateTask { get; set; }
private string userName;
protected override async Task OnInitializedAsync()
{
var authState = await authStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
userName = user.Identity.Name;
};
}
在前面的代码中,authStateTask 参数用于将 CascadingAuthenticationState 组件的 AuthenticationState 值级联传递。
Blazor WebAssembly 使用名为 AuthenticationStateProvider 的内置 DI 服务来确定用户是否已登录。AuthenicationStateProvider 类提供了关于当前用户认证状态的信息。AuthenicationStateProvider 的 User 属性提供了当前用户的 ClaimsPrincipal。ClaimsPrincipal 简单来说就是基于声明的用户身份。
通过使用内置组件,我们可以确认用户的身份。接下来,我们需要确定用户被授权执行的操作。
使用授权控制用户界面
用户认证后,授权规则用于控制用户可以看到和执行的操作。Authorize 属性和 AuthorizeView 组件用于控制用户界面。
Authorize 属性
Authorize 属性用于要求用户有权查看带有该属性的页面。它应该只用于可路由组件。以下组件包含 Authorize 属性:
Secure.razor
@page "/secure"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<h2>Secure Page</h2>
Congratulations, you have been authenticated!
当未认证的用户尝试导航到带有 Authorize 属性的页面时,他们将被自动重定向到 /authentication/login 页面。
提示
您可以通过在 _Imports.razor 文件中添加 Authorize 属性来要求每个页面进行认证。但是,如果您这样做,您必须将 AllowAnonymous 属性添加到 Authentication 组件,否则您的用户将无法登录。
Authorize 组件支持基于角色的授权和基于策略的授权。如果用户已经认证并且尝试导航到包含基于角色或基于策略授权的页面,并且不符合要求,他们将收到 App 组件提供的信息。在前面的示例 App 组件中,信息如下:
ERROR: You are not authorized to access this page
此示例使用 Roles 参数进行基于角色的授权:
@page "/secure"
@attribute [Authorize(Roles = "admin, siteadmin")]
在前面的示例中,只有处于 admin 或 siteadmin 角色的用户才能访问此页面。
此示例使用 Policy 参数进行基于策略的授权:
@page "/secure"
@attribute [Authorize(Policy = "content-admin")]
在前面的示例中,只有满足content-admin策略要求的用户才能访问该页面。
应仅在可路由组件上使用Authorize属性,因为页面内的子组件不会执行授权。
例如,如果我们创建一个名为Secure的可路由组件并使用基于角色或基于策略的授权来保护它,那么如果用户的凭据不符合要求,用户将无法导航到该页面。然而,如果我们将相同的组件放置在用户被授权查看的另一个页面上,他们可以看到Secure组件的内容。
要仅授权显示页面的一定部分,请使用AuthorizeView组件。
AuthorizeView组件
AuthorizeView组件用于控制基于用户授权可以查看的内容的用户界面部分。
重要提示
默认情况下,未经认证的用户无权查看任何内容。
AuthorizeView类具有以下属性:
-
Authorized:当用户被授权时渲染的内容。它是一个RenderFragment。 -
Authorizing:在用户进行认证时渲染的内容。它是一个RenderFragment。 -
NotAuthorized:当用户未授权时渲染的内容。它是一个RenderFragment。 -
Policy:确定内容是否可以渲染的策略。 -
Roles:允许渲染内容的角色的逗号分隔列表。
与RemoteAuthenticatorView组件不同,框架没有为AuthorizeView组件使用的RenderFragments提供默认值。
以下代码使用AuthorizeView组件创建LoginDisplay组件:
LoginDisplay.razor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
<AuthorizeView>
<Authorized>
Hello, @context.User.Identity?.Name!
<button @onclick="BeginLogout">Log out</button>
</Authorized>
<NotAuthorized>
<a href="authentication/login">Log in</a>
</NotAuthorized>
<Authorizing>
Please be patient. We are trying to authorize you.
</Authorizing>
</AuthorizeView>
@code{
private void BeginLogout(MouseEventArgs args)
{
Navigation.NavigateToLogout("authentication/logout");
}
}
前面的示例提供了Authorized模板和NotAuthorized模板。如果用户被授权,则显示其姓名,并渲染注销按钮。如果用户未授权,则渲染登录链接。
AuthorizeView组件支持基于角色和基于策略的授权。如果用户已经认证并且他们尝试导航到一个包含基于角色或基于策略的授权的页面,并且他们满足要求,则渲染Authorized模板中的 UI;否则,渲染NotAuthorized模板中的 UI。
本例使用Roles参数进行基于角色的授权:
<AuthorizeView Roles="admin, siteadmin">
<p>
You can only view this content if you are an admin or
siteadmin.
</p>
</AuthorizeView>
在前面的示例中,只有属于admin或siteadmin角色的用户才会渲染指示的文本。
本例使用Policy参数进行基于策略的授权:
<AuthorizeView Policy="content-admin">
<p>
You can only view this content if you satisfy
the "content-admin" policy.
</p>
</AuthorizeView>
在前面的示例中,只有满足content-admin策略要求的用户才会渲染指示的文本。
AuthorizeView 组件可以在 NavMenu 组件中使用。然而,尽管组件没有出现在 NavMenu 中,但这并不能阻止用户直接导航到该组件。因此,您必须在组件级别始终设置授权规则。
提示
不要依赖 NavMenu 组件来隐藏未授权用户无法访问的组件。
我们可以使用 Authorize 属性和 AuthorizeView 组件强制用户进行身份验证并隐藏用户界面的部分。
现在,让我们快速概述一下本章将要构建的项目。
创建声明查看器项目
在本章中,我们将构建的 Blazor WebAssembly 应用程序是一个声明查看器。首先,我们将应用程序添加到 Azure AD。添加应用程序后,我们将向 Azure AD 添加一个组和用户。我们将添加所需的 NuGet 包并配置项目以使用 MSAL 身份验证。接下来,我们将添加身份验证和登录显示组件。我们还将添加以下可路由组件:Secure 和 WhoAmI。最后,我们将添加并测试一个身份验证策略。
以下是从完成的应用程序中截取的 WhoAmI 组件的截图:

图 10.2:Claims Viewer 中的 WhoAmI 组件
此项目的构建时间大约为 60 分钟。
项目概述
使用 Microsoft 的 Blazor WebAssembly App Empty 项目模板创建一个空的 Blazor WebAssembly 项目,ClaimsViewer 项目将通过这种方式创建。创建完我们的项目后,我们将通过添加应用程序、一个新的组和一个新的用户到我们的 Azure AD 租户中来配置 Azure AD。然后,我们将添加所需的 NuGet 包并更新项目设置。接下来,我们将添加一个身份验证组件和一个登录显示组件。我们还将添加一个用于显示声明内容的组件。最后,我们将添加并测试一个身份验证策略。
重要提示
由于 Microsoft 持续更新 Azure Portal,Azure Portal 中的一些屏幕可能不再与本章中的信息匹配。
创建声明查看器项目
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们可以这样做:
-
打开 Visual Studio 2022。
-
点击 创建新项目 按钮。
-
按 Alt+S 进入 搜索模板 文本框。
-
输入
Blazor并按 Enter 键。以下截图显示了 Blazor WebAssembly App Empty 项目模板:
![图形用户界面、文本、应用程序、聊天或文本消息 自动生成的描述]()
图 10.3:Blazor WebAssembly App Empty 项目模板
-
选择 Blazor WebAssembly App Empty 项目模板并点击 下一步 按钮。
-
在 项目名称 文本框中输入
ClaimsViewer并点击 下一步 按钮。这是用于配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.4:Blazor WebAssembly App 空项目模板
提示
在前面的示例中,我们将
ClaimsViewer项目放置在E:/Blazor文件夹中。然而,此项目的位置并不重要。 -
选择.NET 7.0作为要使用的
Framework版本。 -
选择配置 HTTPS复选框。
-
取消选择ASP.NET Core 托管复选框。
-
取消选择渐进式 Web 应用程序复选框。
这是用于收集有关我们新项目额外信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.5:额外信息对话框
-
点击创建按钮。
我们已创建了一个空的ClaimsViewer Blazor WebAssembly 项目。我们将使用 Azure AD 来提供身份服务。
将应用程序添加到 Azure AD
我们需要在 Azure AD 中注册应用程序并向项目中添加一个appsettings.json文件。我们这样做如下:
-
右键点击
wwwroot文件夹,从菜单中选择添加,新建项选项。 -
在搜索框中输入
json。 -
选择应用程序设置文件。
-
将新项目命名为
appsettings.json。 -
点击添加按钮。
-
将文件中的所有文本替换为以下内容:
{ "AzureAd": { "Authority": "https://login.microsoftonline.com/{Directory (tenant) ID}", "ClientId": "{Application (client) ID}", "ValidateAuthority": true } }在我们将应用程序添加到 Azure AD 之后,我们将替换前面代码中使用的
{Directory (tenant) ID}和{Application (client) ID}占位符。 -
导航到 Azure 门户,
portal.azure.com。 -
打开您的
Azure Active Directory资源。重要提示
当您注册 Microsoft 云服务订阅时,会自动创建一个
Azure Active Directory实例。 -
从添加菜单中选择应用程序注册。

图 10.6:Azure AD 中的应用程序注册
-
对于面向用户的显示名称,输入
ClaimsViewer。 -
对于支持的账户类型,选择仅此组织目录中的账户选项。
-
对于重定向 URI,选择单页应用程序(SPA)作为平台,并输入以下文本作为 URI:
https://localhost:5001/authentication/login-callback以下截图显示了完成的注册应用程序对话框:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.7:注册应用程序对话框
-
点击注册按钮。
以下截图突出了我们需要复制到我们项目中
appsettings.json文件中的信息:![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.8:Azure AD 中的 ClaimsViewer 应用程序
-
返回 Visual Studio。
-
打开
appsettings.json文件。 -
将 {目录(租户)ID} 占位符替换为从 Azure AD 获取的
目录(租户)ID的值。 -
将 {应用程序(客户端)ID} 占位符替换为从 Azure AD 获取的
应用程序(客户端)ID的值。
为了测试我们的应用程序,我们需要添加至少一个用户。此外,在本项目的后期,我们还需要使用一个组来启用基于策略的认证。因此,让我们添加一个用户和一个组。
将用户和组添加到 Azure AD
在我们退出 Azure Portal 之前,让我们创建一个新的组并向该组添加一个新用户。我们这样做如下:
-
返回您的
Azure Active Directory租户。 -
从菜单中选择 组。
-
从顶部菜单中选择 新建组。
-
对于 组类型,保留 安全 选中。
-
对于 组名,输入
ViewAll。 -
对于 组描述,输入
Memberscanviewalloftheclaims。以下截图显示了完成的 新组 对话框:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.9:Azure AD 中的新组对话框
-
点击 创建 按钮。
您现在应该看到新组。如果您看不到新组,请从顶部菜单中选择 刷新 选项。以下截图突出显示了我们需要完成此项目的信息:
![图形用户界面,文本,应用程序 自动生成的描述]()
图 10.10:Azure AD 中的组
-
复制您新组的 对象 ID 并保存以备后用。
当我们在项目中添加认证策略时,我们需要该组的
对象 ID。 -
返回您的
Azure Active Directory租户。 -
从菜单中选择 应用程序注册。
-
点击 ClaimsViewer 应用程序。
-
从菜单中选择 令牌配置。
-
点击 添加组声明 选项。
-
选择 安全组 复选框。
-
点击 添加 按钮。
-
返回您的 Azure Active Directory 资源。
-
从菜单中选择 用户。
-
从 新用户 顶部菜单中选择 创建新用户,如图所示:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.11:在 Azure AD 中创建新用户
-
为新用户输入 用户名 和 姓名。
-
输入 密码。
-
添加 ViewAll 组。
-
点击 创建 按钮。
您现在应该看到您的新用户。如果您看不到您的新用户,请从顶部菜单中选择 刷新 选项。
您已向 Azure AD 添加了一个新用户,该用户是 ViewAll 组的成员。我们已经完成了 Azure AD 的设置。现在我们可以返回 Visual Studio。
添加所需的 NuGet 包
我们需要向我们的应用程序添加三个 NuGet 包。我们这样做如下:
-
返回 Visual Studio。
-
从 Visual Studio 菜单中选择 工具、NuGet 包管理器 和 包管理器控制台 以打开 包管理器控制台。
-
输入以下命令:
Install-package Microsoft.AspNetCore.Authorization -
按下 Enter 键。
您已安装了 ASP.NET Core 授权类。
-
输入以下命令:
Install-package Microsoft.AspNetCore.Components.Authorization -
按下 Enter 键。
您已为 Blazor 应用程序安装了认证和授权类。
-
输入以下命令:
Install-package Microsoft.Authentication.WebAssembly.Msal -
按下 Enter 键。
您已安装了
Microsoft Authentication Library。它用于从 Microsoft 身份平台获取安全令牌。这些令牌可以用于认证用户和访问 Web API。 -
打开
_Imports.razor文件。 -
添加以下
using语句:@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
所有必需的 NuGet 包都已安装。现在我们需要更新一些项目设置。
启用认证
我们需要更新一些项目文件以启用认证。我们这样做如下:
-
打开
wwwroot/index.html文件。 -
在现有的
script元素上方添加以下script元素:<script src="img/AuthenticationService.js"> </script> -
打开
Properties/launchSettings.json文件。 -
在
iisSettings中添加以下内容:"windowsAuthentication": false, "anonymousAuthentication": true, -
打开
Program.cs文件。 -
在文件的最后一行上方添加以下代码:
builder.Services.AddMsalAuthentication(options => { builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); options.ProviderOptions.LoginMode = "redirect"; });在前面的代码中,应用程序被指示引用
appsetting.json文件的 AzureAd 部分,以获取认证应用程序所需的参数。LoginMode的值可以是popup或redirect。我们使用redirect,因为弹出登录对话框不是模态的,并且很容易被其他窗口隐藏。 -
打开
App.Razor文件。 -
添加以下指令:
@inject NavigationManager Navigation -
将
Router元素用以下CascadingAuthenticationState元素包围:<CascadingAuthenticationState> </CascadingAuthenticationState> -
将
RouteView元素替换为以下AuthorizeRouteView元素:<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> @if (context.User.Identity?.IsAuthenticated != true) { Navigation.NavigateTo($"authentication/login"); } else { <p> You are not authorized to access this resource. </p> } </NotAuthorized> </AuthorizeRouteView>
在前面的代码中,如果用户未经授权且未进行认证,他们将被重定向到 authentication/login 页面。然而,如果他们未经授权但已进行认证,将渲染错误消息。
我们已在应用程序中启用了认证。现在我们需要添加一个 Authentication 组件。
添加一个认证组件
我们需要添加一个 Authentication 组件来处理我们的认证操作。我们这样做如下:
-
右键单击
Pages文件夹,从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
Authentication。 -
用以下内容替换文件中的所有文本:
@page "/authentication/{action}" @using Microsoft.AspNetCore.Components.WebAssembly.Authentication <RemoteAuthenticatorView Action="@Action" /> @code { [Parameter] public string? Action { get; set; } }
通过利用 RemoteAuthenticatorView 组件的强大功能,我们仅用几行代码就创建了一个 Authentication 组件。现在让我们添加一种让用户登录和注销我们应用程序的方法。
添加一个 LoginDisplay 组件
我们将添加一个 LoginDisplay 组件来登录和注销我们的应用程序。我们这样做如下:
-
右键单击
ClaimsViewer项目,从菜单中选择 添加,新建文件夹 选项。 -
将新文件夹命名为
Shared。 -
右键单击
Shared文件夹,从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
LoginDisplay。 -
用以下内容替换文件中的所有文本:
@inject NavigationManager Navigation <AuthorizeView> <Authorized> <button @onclick="BeginLogout"> Log out of Claims Viewer </button> Welcome, @context.User.Identity?.Name! </Authorized> <NotAuthorized> <button @onclick="BeginLogin"> Log in to Claims Viewer </button> </NotAuthorized> </AuthorizeView> <hr /> @code { }前面的标记包括一个
AuthorizeView组件。如果用户已经认证,则渲染退出 Claims Viewer按钮。如果用户尚未认证,则渲染登录到 Claims Viewer按钮。 -
将以下代码添加到代码块中:
private void BeginLogin(MouseEventArgs args) { Navigation .NavigateToLogin($"authentication/login"); } private void BeginLogout(MouseEventArgs args) { Navigation .NavigateToLogout($"authentication/logout", $"/"); }BeginLogin方法和BeginLogout方法都将用户重定向到Authentication页面。BeginLogin方法将操作参数设置为login,而BeginLogout方法将操作参数设置为logout并包含一个ReturnUrl的值。在此示例中,ReturnUrl是Home页面。 -
打开
_Imports.razor文件。 -
添加以下
using语句:@using ClaimsViewer.Shared -
打开
MainLayout.razor文件。 -
将
main元素更新为以下内容:<main style="padding:10px"> <LoginDisplay /> <a href="/secure">[Secure Page]</a> <a href="/whoami">[Who Am I?]</a> @Body </main>
由于我们已经将LoginDisplay组件添加到MainLayout组件中,它将出现在我们应用程序的每个页面上。
现在用户可以登录和登出,让我们通过向可路由组件添加Authorize属性来测试我们的应用程序。
添加一个 Secure 组件
我们将添加可路由的Secure组件来演示Authorize属性。我们这样做如下:
-
右键单击
Pages文件夹,从菜单中选择添加,Razor 组件选项。 -
将新组件命名为
Secure。 -
将文件中的所有文本替换为以下内容:
@page "/secure" @attribute [Authorize] <h2>Secure Page</h2> Congratulations, you have been authenticated! -
打开
Pages/Index.razor页面。 -
将
h1元素的内容替换为以下内容:Welcome to the Claims Viewer app. -
按Ctrl+F5启动应用程序,不进行调试。
-
点击登录到 Claims Viewer按钮。
-
输入您的凭据并点击登录按钮。
第一次登录时,您将收到以下对话框:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.12:请求权限对话框
-
点击接受按钮。
点击接受按钮后,您将收到以下对话框:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 10.13:安全默认值对话框
-
点击稍后跳过链接。
重要提示
如果您不想使用 Microsoft Authenticator,请不要点击前面的对话框中的下一步按钮。
-
当询问您是否想保持登录状态时,点击是按钮。
您现在已认证。请注意,
LoginDisplay已更新以显示退出 Claims Viewer按钮和用户名。 -
点击Secure Page链接。
重要提示
如果您点击Secure Page链接时尚未认证,您将被强制登录,因为该页面包含一个
Authenticate属性。 -
点击退出 Claims Viewer按钮。
-
选择要登出的账户。
-
关闭浏览器。
我们已经演示了Authorize属性。现在让我们再次使用AuthorizeView组件。
添加一个 WhoAmI 组件
我们需要创建一个 WhoAmI 组件,用于显示有关用户的信息。我们将使用 AuthorizeView 组件根据用户的身份验证状态渲染不同的信息。我们这样做如下:
-
返回 Visual Studio。
-
右键单击
Pages文件夹,从菜单中选择 添加,Razor 组件 选项。 -
将新组件命名为
WhoAmI。 -
将文件中的所有文本替换为以下内容:
@page "/whoami" @using System.Security.Claims; <h1>Who Am I?</h1> <AuthorizeView> <NotAuthorized> <div> <b>WARNING: You are not authenticated!</b> You must log in to Claims Viewer. </div> </NotAuthorized> <Authorized> I am @myName </Authorized> </AuthorizeView> @code { }上述代码在用户已认证的情况下渲染用户的名字。如果用户尚未认证,则渲染警告信息。
-
将以下代码添加到代码块中:
[CascadingParameter] private Task<AuthenticationState>? authStateTask { get; set; } private string? myName; private List<Claim>? myClaims; protected override async Task OnInitializedAsync() { var authState = await authStateTask!; var user = authState.User; if (user.Identity!.IsAuthenticated) { myName = user.Identity.Name; myClaims = user.Claims.ToList(); }; }在前面的代码中,
authStateTask的值是从App组件级联下来的。 -
按 Ctrl+F5 以无调试模式启动应用程序。
-
点击 我是谁 链接。
AuthorizeView组件正在渲染NotAuthorized元素中的文本。 -
点击 登录到声明查看器 按钮。
-
提供您的凭据并完成登录过程。
AuthorizeView组件正在渲染用户的名字。 -
点击 从声明查看器注销 按钮。
上述代码将 myClaims 的值设置为用户的声明列表。但我的应用程序是如何获取声明列表的呢?声明来自 Azure AD 发送的 ID 令牌。我们可以通过使用浏览器的开发者工具来查看 ID 令牌。ID 令牌是一个 JSON Web Token (JWT)。
查看 JSON Web Token (JWT)
我们将查看从 Azure AD 发送到我们 Web 应用的 ID 令牌。ID 令牌使用 JWT 在服务器和客户端之间共享安全信息。我们这样做如下:
-
按 F12 打开浏览器的开发者工具。
-
选择 网络 选项卡。
-
点击 登录到声明查看器 按钮。
-
提供您的凭据并完成登录过程。
-
点击 token 并选择 预览 选项卡,如图下所示:

图 10.14:示例令牌
-
将
id_token的值复制到剪贴板。导航到
jwt.ms/。 -
将剪贴板的内容粘贴到空文本区域中。
在您粘贴令牌内容后,将渲染解码后的令牌。这是将在我们的 WhoAmI 页面上显示的信息。在 解码令牌 选项卡旁边是 声明 选项卡。
-
点击 声明 选项卡以了解令牌中每个声明的详细信息。
-
关闭浏览器。
现在我们已经知道我们的 WhoAmI 页面上将有什么,让我们完成它。
添加身份验证策略
我们希望将用户声明的列表访问权限限制为仅限于 Azure AD 中的 ViewAll 组成员。为此,我们将添加一个身份验证策略。我们这样做如下:
-
返回 Visual Studio。
-
打开
Program.cs文件。 -
在文件的最后一行上方添加以下代码:
builder.Services.AddAuthorizationCore(options => { options.AddPolicy("view-all", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == "groups" && c.Value.Contains("{Object ID}")))); }); -
打开
Pages/Secure.razor页面。 -
将 view-all 策略添加到
Authorize属性中,如下所示:@attribute [Authorize(Policy = "view-all")]上述代码将阻止不符合
view-all策略所有要求的用户查看页面。 -
打开
Pages/WhoAmI.razor页面。 -
在现有的
AuthorizeView组件下方添加以下标记:<h2>My Claims</h2> <AuthorizeView Policy="view-all"> <NotAuthorized> <div> <b>WARNING: You are not authorized!</b> You must be a member of the ViewAll group in Azure AD. </div> </NotAuthorized> <Authorized> <ul> @foreach (Claim item in myClaims!) { <li>@item.Type: @item.Value</li> } </ul> </Authorized> </AuthorizeView>上述代码将阻止不符合
view-all策略所有要求的用户查看声明列表。 -
按Ctrl+F5启动应用程序而不进行调试。
-
点击登录到声明查看器按钮。
-
点击安全页面链接。
您无权查看此页面,因为用户不符合策略的要求。此消息来自
App组件。 -
点击我是谁?链接。
您无权查看您的声明。此消息直接来自
WhoAmI组件。 -
返回 Visual Studio。
-
打开
Program.cs文件。 -
将
{Object ID}占位符替换为 Azure AD 中ViewAll组的对象 ID的值。重要提示
在添加用户和组到 Azure AD步骤中添加ViewAll组后,你保存了组的对象 ID的值。
-
从构建菜单中选择构建解决方案。
-
返回浏览器。
由于用户现在符合策略的要求,你现在可以查看他们的声明列表。
-
点击安全页面链接。
同样,由于用户现在符合策略的所有要求,你现在可以查看安全页面。
我们已经创建了一个安全的应用程序,允许认证用户(他们是ViewAll组的成员)查看由 Azure AD 提供的 ID 令牌中的声明。
摘要
你现在应该能够通过将身份管理委托给 Azure AD 来渲染已认证用户的声明列表。
在本章中,我们学习了认证和授权之间的区别。我们还学习了如何与认证组件一起工作。最后,我们学习了如何通过使用Authorize属性和AuthorizeView组件来控制用户界面。
之后,我们使用了Blazor WebAssembly App Empty项目模板来创建一个新的项目。接下来,我们使用 Azure Portal 配置我们的 Azure AD 租户以添加一个新应用程序。然后我们在新应用程序中添加了一个组,并将用户添加到该组。我们使用 Azure AD 中的客户端 ID 和租户 ID 来更新项目中appsettings.json文件。我们添加了所需的 NuGet 包,并完成了配置应用程序以使用认证。我们添加了Authentication、LoginDisplay、Secure和WhoAmI组件。最后,我们使用策略来限制对声明列表的访问。
在下一章中,我们将使用 SQL Server 和 ASP.NET Web API 构建一个任务管理器。
问题
以下问题供您考虑:
-
认证和授权之间有什么区别?
-
如果你在主页上添加一个
Secure组件,用户是否需要认证才能渲染它? -
如何在不使用每个可路由组件上的
Authorize属性的情况下保护 Blazor WebAssembly 应用? -
JSON Web Token(JWT)中包含哪些声明?
进一步阅读
以下资源提供了有关本章涵盖主题的更多信息:
-
有关当前认证状态更多信息,请参阅
learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.authorization。 -
有关
ClaimsPrincipal类更多信息,请参阅learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal。 -
有关 Microsoft 身份平台更多信息,请参阅
learn.microsoft.com/en-us/azure/active-directory/develop。 -
GitHub 上的
RemoteAuthenticatorViewCore源代码,请参阅github.com/dotnet/aspnetcore/blob/600eb9aa53c052ec7327e2399744215dbe493a89/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs。 -
要解码 JSON Web Token(JWT),请参阅
jwt.ms。
加入我们的 Discord 社区
加入我们的 Discord 空间,与作者和其他读者进行讨论:

第十一章:使用 ASP.NET Web API 构建任务管理器
大多数网站都不是孤岛独立存在的。它们需要一个服务器。它们在数据访问和安全性等众多服务上依赖于服务器。
在本章中,我们将学习如何创建一个托管的 Blazor WebAssembly 应用程序。我们将学习如何使用 HttpClient 服务调用 Web API,并且我们还将学习如何使用 JSON 辅助方法 向 Web API 发送请求以读取、添加、编辑和删除数据。
本章中我们将创建的项目将是一个 任务管理器。我们将使用多项目架构将 Blazor WebAssembly 应用程序与 ASP.NET Web API 端点分离。托管的 Blazor WebAssembly 应用程序将使用 JSON 辅助方法读取、添加、编辑和删除存储在 SQL Server 数据库中的任务。一个 ASP.NET 核心项目将托管 Blazor WebAssembly 应用程序并提供 ASP.NET Web API 端点。第三个项目将用于定义其他两个项目共享的类。
孤岛独立存在。
大多数网站都不是孤岛 -
它们需要一个服务器。
在本章中,我们将介绍以下主题:
-
理解托管应用程序
-
使用
HttpClient服务 -
使用 JSON 辅助方法
-
创建任务管理器项目
技术要求
要完成此项目,您需要在您的电脑上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 的免费社区版的说明,请参阅 第一章,Blazor WebAssembly 简介。您还需要访问 SQL Server 的某个版本。有关如何安装 SQL Server 2022 的免费版的说明,请参阅 第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter11。
代码实战视频在此处可用:packt.link/Ch11。
理解托管应用程序
当我们使用 Microsoft 的 Blazor WebAssembly App Empty 项目模板创建一个新的 Blazor WebAssembly 项目时,我们可以通过勾选 ASP.NET Core 托管 复选框来创建一个托管的 Blazor WebAssembly 应用程序。
以下截图突出了 ASP.NET Core 托管 复选框:

图 11.1:Blazor WebAssembly App Empty 项目模板
由 Blazor WebAssembly App Empty 项目模板创建的托管 Blazor WebAssembly 应用程序包括以下三个项目:
-
客户端项目
-
服务器项目
-
共享项目
客户端项目
客户端项目是一个客户端 Blazor WebAssembly 项目。它与我们在 第二章,构建您的第一个 Blazor WebAssembly 应用程序 中创建的独立 Blazor WebAssembly 应用几乎相同。唯一的重大区别在于数据访问方式。在客户端项目中,示例数据是通过 Web API 端点从服务器项目访问的,而不是静态文件。由于服务器项目既托管客户端项目,又通过 ASP.NET Web API 提供数据服务,因此它不会遇到任何 CORS 问题。
服务器项目
服务器项目是一个 ASP.NET Core 项目。该项目负责提供客户端应用程序。除了托管客户端应用程序外,服务器项目还提供了 Web API 端点。
重要提示
在这种情况下,由于 ASP.NET Core 项目正在提供 Blazor WebAssembly 应用,因此它必须在解决方案中设置为启动项目。
共享项目
共享项目也是一个 ASP.NET Core 项目。它包含在另外两个项目之间共享的应用程序逻辑。在过去,我们不得不在客户端和服务器上编写验证代码。我们必须为客户端编写 JavaScript 验证代码,为服务器编写 C# 验证代码。不出所料,有时这两个验证模型并不匹配。共享项目解决了这个问题,因为所有验证代码都使用单一语言在单个位置维护。
通过使用多项目解决方案,我们可以创建一个更健壮的应用程序。共享项目定义了类,而客户端项目使用 HttpClient 服务从服务器项目请求数据。
使用 HttpClient 服务
HTTP 不仅用于提供网页服务,还可以用于提供数据。以下是本章我们将使用的 HTTP 方法:
-
GET:此方法用于请求一个或多个资源。 -
POST:此方法用于创建新的资源。 -
PUT:此方法用于更新指定的资源。 -
DELETE:此方法用于删除指定的资源。
HttpClient 服务是一个预配置的服务,用于从 Blazor WebAssembly 应用向服务器发送 HTTP 请求。它在 Client/Program.cs 文件中配置。以下代码用于配置它:
builder.Services.AddScoped(sp => new HttpClient {
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
使用 依赖注入(DI)将 HttpClient 服务添加到页面中。要在组件中使用 HttpClient 服务,您必须通过使用 @inject 指令或 Inject 属性来注入它。有关依赖注入的更多信息,请参阅 第七章,使用应用程序状态构建购物车。
以下代码演示了将 HttpClient 服务注入到组件中的两种不同方法:
@inject HttpClient Http[Inject]
public HttpClient Http { get; set; }
第一个示例用于 .razor 文件,而第二个示例用于 .razor.cs 文件。在我们将 HttpClient 服务注入到组件后,我们可以使用 JSON 辅助方法向 Web API 发送请求。
使用 JSON 辅助方法
有三种 JSON 辅助方法。一个用于读取数据,一个用于添加数据,一个用于更新数据。由于没有用于删除数据的方法,我们将使用 HttpClient.DeleteAsync 方法来删除数据。
以下表格显示了 JSON 辅助方法与 HTTP 方法之间的关系:
| JSON 辅助方法 | HTTP 方法 | 操作 |
|---|---|---|
GetFromJsonAsync |
GET | 读取 |
PostAsJsonAsync |
POST | 创建 |
PutAsJsonAsync |
PUT | 更新 |
HttpClient.DeleteAsync |
DELETE | 删除 |
表 11.1:HTTP 方法与 JSON 辅助方法之间的关系
提示
您还可以使用 HttpClient 服务和 JSON 辅助方法来调用外部 Web API 端点。例如,请参阅 第六章,构建作为渐进式 Web 应用 (PWA) 的天气应用。
在以下代码示例中,我们将参考 TaskItem 类。这是 TaskItem 类:
public class TaskItem
{
public int TaskItemId { get; set; }
public string? TaskName { get; set; }
public bool IsComplete { get; set; }
}
GetFromJsonAsync
GetFromJsonAsync 方法用于读取数据。它执行以下操作:
-
向指定的 URI 发送
HTTP GET请求。 -
将 JSON 响应体反序列化为指定的对象。
以下代码使用 GetFromJsonAsync 方法返回一个 TaskItem 对象的集合:
IList<TaskItem>? tasks;
string requestUri = "api/TaskItems";
tasks = await Http.GetFromJsonAsync<IList<TaskItem>>(requestUri);
在前面的代码中,GetFromJsonAsync 方法返回的对象类型是 IList<TaskItem>。
我们还可以使用 GetFromJsonAsync 方法来获取单个对象。以下代码使用 GetFromJsonAsync 方法返回一个单个 TaskItem 对象,其中 id 是对象的唯一标识符:
TaskItem? task;
string requestUri = "api/TaskItems/{id}";
task = await Http.GetFromJsonAsync<TaskItem>(requestUri);
在前面的代码中,GetFromJsonAsync 方法返回的对象类型是 TaskItem。
PostAsJsonAsync
PostAsJsonAsync 方法用于添加数据。它执行以下操作:
-
向指定的 URI 发送
HTTP POST请求。请求包含用于创建新数据的 JSON 编码内容。 -
返回一个包含状态码和数据的
HttpResponseMessage实例。
以下代码通过使用 PostAsJsonAsync 方法创建一个新的 TaskItem 对象:
TaskItem newTaskItem = new() { TaskName = "Buy Milk"};
string requestUri = "api/TaskItems";
var response =
await Http.PostAsJsonAsync(requestUri, newTaskItem);
if (response.IsSuccessStatusCode)
{
var task =
await response.Content.ReadFromJsonAsync<TaskItem>();
}
else
{
// handle error
};
在前面的代码中,如果 HTTP 响应返回成功状态码,则使用 ReadFromJsonAsync 方法将新的 TaskItem 对象反序列化。
提示
ReadFromJsonAsync 方法返回反序列化后的内容。它包含在由微软提供的 System.Text.Json 库中。System.Text.Json 库包括用于将 JSON 文本序列化和反序列化到对象的高性能、低分配方法。
PutAsJsonAsync
PutAsJsonAsync 方法用于更新数据。它执行以下操作:
-
向指定的 URI 发送
HTTP PUT请求。请求包含用于更新数据的 JSON 编码内容。 -
返回一个包含状态码和数据的
HttpResponseMessage实例。
以下代码使用 PutAsJsonAsync 方法通过更新现有的 TaskItem 对象:
string requestUri = $"api/TaskItems/{updatedTaskItem.TaskItemId}";
var response =
await Http.PutAsJsonAsync<TaskItem>
(requestUri, updatedTaskItem);
if (response.IsSuccessStatusCode)
{
var task =
await response.Content.ReadFromJsonAsync<TaskItem>();
}
else
{
// handle error
};
在前面的代码中,如果 HTTP 响应返回成功状态码,则使用 ReadFromJsonAsync 方法将更新的 TaskItem 从响应反序列化。
HttpClient.DeleteAsync
HttpClient.DeleteAsync 方法用于删除数据。它执行以下操作:
-
向指定的 URI 发送
HTTP DELETE请求。 -
返回一个包含状态码和数据的
HttpResponseMessage实例。
以下代码使用 Http.DeleteAsync 方法删除现有的 TaskItem 对象:
string requestUri = $"api/TaskItems/{taskItem.TaskItemId}";
var response = await Http.DeleteAsync(requestUri);
if (!response.IsSuccessStatusCode)
{
// handle error
};
在前面的代码中,删除了具有指定 TaskItemId 的 TaskItem。
JSON 辅助方法使得消费 Web API 变得容易。我们使用它们来读取、创建和更新数据。我们使用 HttpClient.DeleteAsync 来删除数据。
现在,让我们快速了解一下本章将要构建的项目。
创建 TaskManager 项目
在本章中,我们将构建一个托管 Blazor WebAssembly 应用程序来管理任务。我们将能够查看、添加、编辑和删除任务。任务将存储在 SQL Server 数据库中。
这是完成的应用程序的截图:

图 11.2:任务管理器项目
此项目的构建时间大约为 60 分钟。
项目概述
将使用 Microsoft 的 Blazor WebAssembly App Empty 项目模板创建 TaskManager 项目,以创建一个托管 Blazor WebAssembly 项目。首先,我们将添加一个 TaskItem 类和一个 TaskItemsController 类。接下来,我们将使用 Entity Framework 迁移在 SQL Server 中创建数据库。我们将向项目中添加 Bootstrap 和 Bootstrap icons 以样式化我们的 UI。最后,我们将演示如何使用 HttpClient 服务读取数据、更新数据、删除数据和添加数据。
创建 TaskManager 项目
我们需要创建一个新的托管 Blazor WebAssembly 应用程序。我们这样做如下:
-
打开 Visual Studio 2022。
-
点击 创建新项目 按钮。
-
按下 Alt+S 进入 搜索模板 文本框。
-
输入
Blazor并按 Enter 键。以下截图显示了 Blazor WebAssembly App Empty 项目模板:
![图形用户界面,文本,应用程序,聊天或文本消息 自动生成的描述]()
图 11.3:Blazor WebAssembly App Empty 项目模板
-
选择 Blazor WebAssembly App Empty 项目模板并点击 下一步 按钮。
-
在 项目名称 文本框中输入
TaskManager并点击 下一步 按钮。这是配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 11.4:配置新项目对话框
提示
在前面的示例中,我们将
TaskManager项目放置在E:/Blazor文件夹中。然而,此项目的位置并不重要。 -
选择 .NET 7.0 作为要使用的
Framework版本。 -
勾选配置为 HTTPS复选框。
-
勾选ASP.NET Core 托管复选框。
-
取消勾选渐进式 Web 应用复选框。
这是用于收集有关我们新项目附加信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 11.5:附加信息对话框
-
点击创建按钮。
我们已创建一个托管的TaskManager Blazor WebAssembly 项目。
TaskManager解决方案包含三个不同的项目。以下 Visual Studio 的解决方案资源管理器截图显示了解决方案中的三个项目:
![图形用户界面,文本,应用程序 自动生成的描述]()
图 11.6:解决方案资源管理器
-
右键点击TaskManager.Server项目,从菜单中选择设置为启动项目选项。
我们已创建TaskManager解决方案并将TaskManager.Server项目设置为启动项目。现在我们需要向TaskManager.Shared项目添加一个共享类。
添加 TaskItem 类
我们需要添加TaskItem类。我们这样做如下:
-
右键点击TaskManager.Shared项目,从菜单中选择添加,类选项。
-
将新类命名为
TaskItem。 -
点击添加按钮。
-
通过添加
public修饰符使类公开:**public** class TaskItem -
向
TaskItem类添加以下属性:public int TaskItemId { get; set; } public string? TaskName { get; set; } public bool IsComplete { get; set; } -
从构建菜单中选择构建解决方案选项。
我们已添加了TaskItem类。接下来,我们需要为TaskItem类添加一个API 控制器。API 控制器将处理来自 Blazor WebAssembly 客户端的传入 HTTP 请求,并将响应发送回它。
添加 TaskItem API 控制器
我们需要添加一个TaskItemsController类。我们这样做如下:
-
右键点击TaskManager.Server项目,从菜单中选择添加,新建文件夹选项。
-
将新文件夹命名为
Controllers。 -
右键点击
TaskManager.Server.Controllers文件夹,从菜单中选择添加,控制器选项。 -
选择使用 Entity Framework 添加具有操作的 API 控制器选项:

图 11.7:添加新模板项对话框
-
点击添加按钮。
-
将模型类设置为TaskItem (TaskManager.Shared)。
-
点击添加数据上下文按钮以打开添加数据上下文对话框:

图 11.8:添加 API 控制器(使用 Entity Framework)对话框
- 点击添加按钮以接受默认值。

图 11.9:添加数据上下文对话框
- 在添加 API 控制器(使用 Entity Framework)对话框中点击添加按钮。
我们已经创建了TaskItemsController类。现在我们需要设置 SQL Server。
设置 SQL Server
我们需要在 SQL Server 上创建一个新的数据库并添加一个包含任务的表。我们这样做如下:
-
打开
TaskManager.Server/appsettings.json文件。 -
将连接字符串更新为以下内容:
"ConnectionStrings": { "TaskManagerServerContext": "Server={Server name}; Database=TaskManager; Trusted_Connection=True; Encrypt=False;" } -
将
{服务器名称}占位符替换为你的 SQL Server 名称。
重要提示
虽然我们使用的是 SQL Server 2022 Express,但对于这个项目来说,你使用什么版本的 SQL Server 都无关紧要。
-
从工具菜单中选择NuGet 包管理器,包管理控制台选项。
-
在包管理控制台中,使用下拉列表将默认项目更改为TaskManager.Server。
-
在包管理控制台中执行以下命令:
Add-Migration Init Update-Database前面的命令使用
Entity Framework迁移来更新 SQL Server。 -
从视图菜单中选择SQL Server 对象资源管理器。
-
如果你没有看到用于此项目的 SQL Server 实例,请单击添加 SQL Server按钮来连接它:

图 11.10:SQL Server 对象资源管理器
-
导航到任务管理器数据库。
提示
如果你没有在数据库下看到TaskManager数据库,请右键单击数据库并选择刷新选项。
-
导航到任务管理器,表格,dbo.TaskItem:

图 11.11:TaskManager 数据库
-
右键单击dbo.TaskItem并选择查看数据选项。
-
通过完成任务名称字段并将IsComplete字段设置为False来输入几个任务:

图 11.12:示例数据
-
按Ctrl+F5键以无调试模式启动应用程序。
-
将
/api/taskitems添加到地址栏并按Enter键。以下截图显示了
TaskItemsController返回的 JSON:![图形用户界面,文本 自动生成的描述]()
图 11.13:TaskItem API 控制器返回的 JSON
-
关闭浏览器。
我们已经证明TaskItemsController可以工作。现在我们可以开始工作在TaskManager.Client项目上了。我们将使用Bootstrap来美化我们的 UI,并使用Bootstrap icons在删除按钮上提供垃圾桶图像。
安装 Bootstrap
我们需要在我们的 Web 应用中安装Bootstrap和Bootstrap icons。我们这样做如下:
-
返回 Visual Studio。
-
按Ctrl+Alt+L键查看解决方案资源管理器。
-
右键单击
TaskManager.Client/wwwroot/css文件夹,从菜单中选择添加,客户端库选项。 -
在库搜索文本框中输入
bootstrap并按Enter键。 -
选择选择特定文件。
-
如以下截图所示,仅选择 css 文件:
![图形用户界面,应用程序,电子邮件,自动生成描述]()
图 11.14:添加客户端库对话框
提示
尽管前面的截图选择了 Bootstrap 的 5.2.3 版本,但你可以使用任何版本的 Bootstrap 5 来完成此项目。
-
点击 安装 按钮。
重要提示
安装
Bootstrap后,将在wwwroot/css文件夹中添加一个新文件夹。这个新文件夹包含Bootstrap所需的所有 CSS 文件。在本项目中,我们只将使用bootstrap.min.css文件。 -
打开
TaskManager.Client/wwwroot/index.html文件。 -
在链接到
css/app.css样式表的head元素之前添加以下标记:<link href="css/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> -
右键点击
TaskManager.Client/wwwroot/css文件夹,从菜单中选择 添加,客户端库 选项。 -
在 库 搜索文本框中输入
bootstrap-icons并按 Enter 键。 -
选择 选择特定文件。
-
如以下截图所示,仅选择 字体 文件:

图 11.15:添加客户端库对话框
-
打开
TaskManager.Client/wwwroot/index.html文件。 -
在链接到
css/app.css样式表的head元素之前添加以下标记:<link href="css/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet" />
我们已安装了 Bootstrap 和 Boostrap icons。现在我们将更新 主页 以显示存储在 SQL Server 中的任务。
显示任务
我们需要获取任务列表并将其显示给用户。我们这样做如下:
-
右键点击
TaskManager.Client.Pages文件夹,从菜单中选择 添加,类 选项。 -
将新类命名为
Index.razor.cs。 -
点击 添加 按钮。
-
将
partial修饰符添加到类中。public **partial** class Index -
添加以下
using语句:using Microsoft.AspNetCore.Components; -
将以下属性添加到
Index类中:[Inject] public HttpClient Http { get; set; }前面的代码将
HttpClient服务注入到组件中。 -
将以下代码添加到
Index类中:private IList<TaskItem>? tasks; private string? error; protected override async Task OnInitializedAsync() { try { string requestUri = "api/TaskItems"; tasks = await Http.GetFromJsonAsync<IList<TaskItem>> (requestUri); } catch (Exception) { error = "Error Encountered"; }; }前面的代码使用
GetFromJsonAsync方法返回TaskItem对象的集合。 -
确认 Visual Studio 已自动添加以下
using语句:using System.Net.Http.Json; using TaskManager.Shared; -
打开
TaskManager.Client.Pages/Index.razor页面。 -
删除
h1元素。 -
添加以下
@if语句:@if (tasks == null) { <p><em>Loading...</em></p> } else { @foreach (var taskItem in tasks) { } }前面的标记在
tasks的值为null时显示加载消息。否则,它将遍历tasks中TaskItem对象的集合。 -
在
@foreach循环中添加以下标记:<div class="d-flex col-md-6 mx-auto border-bottom" @key="taskItem"> <div class="p-2 flex-fill"> <input type="checkbox" checked="@taskItem.IsComplete" /> <span> @taskItem.TaskName </span> </div> <div class="p-1"> <button type="button" class="btn btn-outline-danger btn-sm" title="Delete task"> <span class="bi bi-trash"></span> </button> </div> </div>前面的标记为每个
TaskItem类显示一个复选框、TaskName字段和一个删除按钮。 -
按 Ctrl+F5 启动应用程序,不进行调试。
以下是一个 主页 的截图:
![图形用户界面,应用程序,自动生成描述]()
图 11.16:任务列表
我们已经在 主页 上添加了一个任务列表,但点击复选框或 删除 按钮时没有任何反应。接下来,我们需要允许用户标记任务为完成。
完成任务
我们将允许用户通过点击任务名称旁边的复选框来标记任务为完成。我们这样做如下:
-
返回 Visual Studio。
-
右键点击
TaskManager.Client/Pages文件夹,从菜单中选择 添加,新建项 选项。 -
在 搜索 框中输入
css。 -
选择 样式表。
-
将文件命名为
Index.razor.css。 -
点击 添加 按钮。
-
将默认文本替换为以下样式:
.completed-task { text-decoration: line-through; }上述样式将渲染一个穿过
completed-task类任务的线条。 -
打开
TaskManager.Client/wwwroot/index.html文件。 -
取消注释以下链接元素:
<link href="TaskManager.Client.styles.css" rel="stylesheet" /> -
打开
Index.razor文件。 -
更新用于显示任务名称的
span元素如下:<span class="@((taskItem.IsComplete? "completed-task" : ""))"> @taskItem.TaskName </span>上述标记将在任务通过勾选与任务关联的复选框完成时,将
span元素的类设置为completed-task。 -
向复选框类型的
input元素添加以下标记:@onchange="@(()=>CheckboxChecked(taskItem))" -
打开
TaskManager.Client.Pages/Index.razor.cs文件。 -
添加以下
CheckboxChecked方法:private async Task CheckboxChecked(TaskItem task) { task.IsComplete = !task.IsComplete; string requestUri = $"api/TaskItems/{task.TaskItemId}"; var response = await Http.PutAsJsonAsync<TaskItem> (requestUri, task); if (!response.IsSuccessStatusCode) { error = response.ReasonPhrase; }; }上述代码使用
PutAsJsonAsync方法更新指定的TaskItem类。 -
按 Ctrl+F5 以无调试模式启动应用程序。
-
通过点击旁边的复选框标记其中一个任务为完成。
以下截图显示了一个已完成的任务:
![图形用户界面,应用程序描述自动生成,中等置信度]()
图 11.17:完成的任务
-
返回 Visual Studio。
-
选择 dbo.TaskItem [数据] 选项卡。
-
点击 Shift+Alt+R 来刷新数据。
-
验证标记为完成的
TaskItem的 IsComplete 字段是否已更新为 True。
当用户勾选任务旁边的复选框时,UI 将更新,SQL Server 数据库也将更新。接下来,我们需要添加删除任务的功能。
删除任务
我们需要允许用户删除任务。我们这样做如下:
-
打开
Index.razor文件。 -
通过添加高亮代码更新
button元素如下:<button type="button" class="btn btn-outline-danger btn-sm" title="Delete task" **@****onclick****=****"@(()=>DeleteTask(taskItem))"****>** <span class="bi bi-trash"></span> </button> -
打开
TaskManager.Client.Pages/Index.razor.cs文件。 -
添加以下
DeleteTask方法:private async Task DeleteTask(TaskItem taskItem) { tasks!.Remove(taskItem); StateHasChanged(); string requestUri = $"api/TaskItems/{taskItem.TaskItemId}"; var response = await Http.DeleteAsync(requestUri); if (!response.IsSuccessStatusCode) { error = response.ReasonPhrase; }; }上述代码使用
Http.DeleteAsync方法删除指定的TaskItem类。 -
按 Ctrl+F5 以无调试模式启动应用程序。
-
点击带有垃圾箱图标的按钮来删除一个任务。
-
返回 Visual Studio。
-
选择 dbo.TaskItem [数据] 选项卡。
-
点击 Shift+Alt+R 来刷新数据。
-
验证
TaskItem是否已被删除。
我们已经添加了删除任务的功能。现在我们需要添加添加新任务的功能。
添加新任务
我们需要提供一个让用户添加新任务的方法。我们这样做如下:
-
打开
Index.razor文件。 -
在
@foreach循环之前添加以下标记:<div class="d-flex col-md-6 mx-auto py-2"> <input type="text" class="form-control m-1" placeholder="Enter Task" @bind="newTask" /> <button type="button" class="btn btn-success" @onclick="AddTask"> Add </button> </div> -
打开
TaskManager.Client.Pages/Index.razor.cs文件。 -
添加以下字段:
private string? newTask; -
添加以下
AddTask方法:private async Task AddTask() { if (!string.IsNullOrWhiteSpace(newTask)) { TaskItem newTaskItem = new TaskItem { TaskName = newTask, IsComplete = false }; tasks!.Add(newTaskItem); string requestUri = "api/TaskItems"; var response = await Http.PostAsJsonAsync (requestUri, newTaskItem); if (response.IsSuccessStatusCode) { newTask = string.Empty; } else { error = response.ReasonPhrase; }; }; }上述代码使用
PostAsJsonAsync方法创建一个新的TaskItem类。 -
按 Ctrl+F5 以无调试模式启动应用程序。
-
添加几个新任务。
-
返回 Visual Studio。
-
选择 dbo.TaskItem [数据] 选项卡。
-
按 Shift+Alt+R 刷新数据。
-
验证新任务是否已添加到 SQL Server 数据库中。
我们已经添加了用户添加新任务的功能。
摘要
现在,您应该能够创建一个托管 Blazor WebAssembly 应用程序,该程序使用 ASP.NET Web API 更新 SQL Server 数据库中的数据。
在本章中,我们介绍了托管 Blazor WebAssembly 应用程序、HttpClient 服务以及用于读取、创建和更新数据的 JSON 辅助方法。我们还演示了如何使用 HttpClient.DeleteAsync 方法删除数据。
之后,我们使用 Microsoft 的 Blazor WebAssembly App Empty 项目模板创建了一个托管 Blazor WebAssembly 应用程序。我们将 TaskItem 类添加到 TaskManager.Shared 项目中,并将 TaskItem API 控制器 添加到 TaskManager.Server 项目中。接下来,我们通过更新数据库的连接字符串和使用 Entity Framework 迁移来配置 SQL Server。为了增强 UI,我们添加了 Bootstrap 和 Bootstrap icons。最后,我们使用 HttpClient 服务读取任务列表、更新任务、删除任务以及添加新任务。
我们可以将我们的新技能应用于创建一个作为多项目解决方案一部分的托管 Blazor WebAssembly 应用程序,并使用 ASP.NET Web API 读取、创建、更新和删除数据。
在下一章中,我们将使用 EditForm 组件构建一个支出跟踪器。
问题
以下问题供您参考:
-
使用托管 Blazor WebAssembly 项目与独立 Blazor WebAssembly 项目相比有哪些好处?
-
HTTP GET、HTTP POST 和 HTTP PUT 之间有什么区别?
-
在我们的项目中,我们如何通过使用
PostAsJsonAsync方法创建的TaskItem获取TaskItemId? -
您能否直接从 Visual Studio 中添加、编辑和删除 SQL Server 数据库中的数据?
进一步阅读
以下资源提供了有关本章涵盖主题的更多信息:
-
有关
HttpClient类的更多信息,请参阅learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient。 -
有关从 Blazor WebAssembly 调用 Web API 的更多信息,请参阅
learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api。 -
如需了解使用
System.Text.Json执行序列化和反序列化操作的扩展方法,请参阅learn.microsoft.com/en-us/dotnet/api/system.text.json。 -
如需了解有关
Entity Framework的更多信息,请参阅learn.microsoft.com/en-us/ef。 -
如需了解有关
Bootstrap的更多信息,请参阅getbootstrap.com。
第十二章:使用 EditForm 组件构建费用跟踪器
大多数应用程序都需要用户输入一些数据。Blazor WebAssembly 框架包含一个组件,可以轻松创建数据输入表单并验证这些表单上的数据。
在本章中,我们将学习如何使用EditForm组件和多种内置输入组件。我们还将学习如何结合数据注释使用内置输入验证组件来验证表单上的数据。最后,我们将学习如何使用NavigationLock组件来防止用户在未保存更新之前离开表单时丢失他们的编辑。
本章我们将创建的项目将是一个旅行费用跟踪器。我们将使用多项目架构将 Blazor WebAssembly 应用程序与 ASP.NET Web API 端点分离。用于添加和编辑费用的页面将使用EditForm组件以及许多内置输入组件。它还将使用内置的验证组件来验证表单上的数据。最后,我们将添加NavigationLock组件,以提醒用户在导航到另一个页面之前保存他们的数据。
正在编辑数据?
EditForm 组件
让它变得轻而易举!
在本章中,我们将涵盖以下主题:
-
创建数据输入表单
-
使用内置输入组件
-
使用验证组件
-
锁定导航
-
创建费用跟踪器项目
技术要求
要完成此项目,您需要在您的 PC 上安装 Visual Studio 2022。有关如何安装 Visual Studio 2022 免费社区版的说明,请参阅第一章,Blazor WebAssembly 简介。您还需要访问 SQL Server 的一个版本。有关如何安装 SQL Server 2022 免费版的说明,请参阅第一章,Blazor WebAssembly 简介。
本章的源代码可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Blazor-WebAssembly-by-Example-Second-Edition/tree/main/Chapter12.
“代码在行动”视频在此处提供:packt.link/Ch12.
创建数据输入表单
在本书的前几章中,我们使用了标准的 HTML form元素来收集用户输入。然而,Blazor WebAssembly 框架提供了一个增强版的 HTML form元素,称为EditForm组件。
EditForm组件不仅管理表单,还协调验证和提交事件。以下代码展示了简单的EditForm元素:
<EditForm Model="expense" OnValidSubmit="@HandleValidSubmit">
Vendor <InputText @bind-Value="expense.Vendor"
placeholder="Enter Vendor"/>
<button type="submit">
Save
</button>
</EditForm>
@code {
private Expense expense = new();
}
这是前一个EditForm组件渲染的 HTML:
<form>
Vendor <input placeholder="Enter Vendor"
class="valid" _bl_2="">
<button type="submit">
Save
</button>
</form>
在前面的 EditForm 组件中,Model 属性指定了表单的顶级模型对象。OnValidSubmit 属性指定了当表单提交且没有验证错误时将被调用的回调函数。
绑定表单
EditForm 类的 EditContext 属性用于跟踪编辑过程中的元数据。元数据包括已修改的字段和当前的验证消息。有两种方式来分配 EditContext 属性:
-
分配
EditForm类的Model属性。如果我们使用Model属性,编辑上下文将自动使用模型构建。 -
分配
EditForm类的EditContext属性。
以下代码展示了如何使用 EditContext 属性而不是 Model 属性来渲染前面的 EditForm 组件:
<EditForm EditContext="editContext"
OnValidSubmit="@HandleValidSubmit">
Vendor <InputText @bind-Value="expense.Vendor" />
<button type="submit">
Save
</button>
</EditForm>
@code {
private Expense expense = new();
private EditContext? editContext;
protected override void OnInitialized()
{
editContext = new(expense);
}
}
重要提示
如果我们尝试为 EditForm 组件的 Model 属性和 EditContext 属性赋值,将会生成运行时错误。
提交表单
EditForm 组件有三个属性与表单提交相关:
-
OnValidSubmit:当表单提交且EditContext属性有效时被调用的回调函数。 -
OnInvalidSubmit:当表单提交且EditContext属性无效时被调用的回调函数。 -
OnSubmit:当表单提交时被调用的回调函数。当我们使用此属性时,我们需要手动通过使用EditForm的EditContext属性的 Validate 方法来触发验证。
我们可以使用 OnValidSubmit 和 OnInvalidSubmit 回调一起或单独使用,或者我们可以单独使用 OnSubmit 回调。如果我们使用 OnSubmit 回调,我们负责执行表单验证。否则,表单验证将由 EditForm 组件执行。
重要提示
如果我们设置了 OnSubmit 回调,使用 OnValidSubmit 或 OnInvalidSubmit 设置的任何回调都将被忽略。
我们可以使用许多内置的输入组件与 EditForm 组件一起使用。
使用内置的输入组件
以下表格列出了内置的输入组件及其渲染的 HTML:
| 输入组件 | HTML 渲染 |
|---|---|
InputCheckbox |
<input type="checkbox"> |
InputDate<TValue> |
<input type="date"> |
InputFile |
<input type="file"> |
InputNumber<TValue> |
<input type="number"> |
InputRadio<TValue> |
<input type="radio"> |
InputRadioGroup<TValue> |
子 InputRadio<TValue> 组 |
InputSelect<TValue> |
<select> |
InputText |
<input> |
InputTextArea |
<textarea> |
表 12.1:内置输入组件
所有内置输入组件都可以在EditForm元素内接收和验证用户输入。EditForm将其EditContext级联到其子元素。此外,所有内置输入组件都支持任意属性。因此,任何不匹配组件参数的属性都将添加到组件渲染的 HTML 元素中。
InputCheckbox
InputCheckbox组件用于编辑布尔值。它不允许绑定到可空属性。
InputDate
InputDate组件用于编辑日期值。支持的日期类型是DateTime和DateTimeOffset。如果输入了不支持的数据类型,框架将创建一个验证错误。
InputFile
InputFile组件用于上传文件。
提示
本章的项目不使用InputFile组件。有关使用InputFile组件的更多信息,请参阅第九章,上传和读取 Excel 文件。
InputNumber
InputNumber组件用于编辑数值。支持的数值类型是Int32、Int64、Single、Double和Decimal。如果输入了不支持的数据类型,框架将创建一个验证错误,除非目标属性是可空的。在这种情况下,无效的输入将被视为null,并且输入框中的文本将被清除。
InputRadio
InputRadio组件用于从一组选项中选择一个值。
InputRadioGroup
InputRadioGroup组件用于对InputRadio组件进行分组。
InputSelect
InputSelect组件用于渲染下拉选择。InputSelect组件包括一个ChildContent属性,用于在select元素内部渲染内容。
如果您选择的选项没有值属性,因为其值为 null,则文本内容被视为值。这是标准的 HTML。然而,当使用 Blazor 的双向绑定时,您必须提供string.Empty作为 null 值的值,以防止文本的值被返回。
InputText
InputText组件用于编辑字符串值。InputText组件没有指定类型。这允许您使用 HTML 输入元素的所有可用输入类型,例如password、tel或color。
HTML 输入元素的默认类型是text。
InputTextArea
InputTextArea组件用于使用多行输入编辑字符串值。
通过结合使用各种内置输入组件及其父组件EditForm,我们可以轻松地将输入表单添加到 Blazor WebAssembly 应用中。
输入数据在表单提交时和数据更改时都会进行验证。为了传达输入表单的验证状态,我们可以使用内置的验证组件。
使用验证组件
输入验证是每个应用程序的重要方面,因为它可以防止用户输入无效数据。Blazor WebAssembly 框架使用数据注释进行输入验证。有超过 30 个内置的数据注释属性。以下是本项目将使用到的属性列表:
-
Required:此属性指定值是必需的。 -
Display:此属性指定错误消息中显示的字符串。 -
MaxLength:此属性指定允许的最大字符串长度。 -
Range:此属性指定最大和最小值。
以下代码演示了几个数据注释的使用:
[Required]
public DateTime? Date { get; set; }
[Required]
[Range(0, 500, ErrorMessage = "The Amount must be <= $500")]
public decimal? Amount { get; set; }
在前面的示例中,日期 字段和 金额 字段都是必需的。此外,金额 字段必须是一个介于 0 到 500(含)之间的值,否则将显示指示的错误消息。
有两个内置的验证组件:
-
ValidationMessage:此组件在EditContext中显示指示字段的全部验证消息。 -
ValidationSummary:此组件在EditContext中显示所有字段的全部验证消息。它提供了验证消息的摘要。
验证组件在页面中的位置决定了它将被渲染的位置。在以下示例中,ValidationMessage 放置在每个相关输入组件之后,而 ValidationSummary 放置在 保存 按钮之后。
这是一个示例 ValidationMessage 组件:
<ValidationMessage For="() => expense.Date" />
这是一个示例 ValidationSummary 组件:
<ValidationSummary />
EditForm 组件可以包含两种类型的验证组件。然而,要使用任何类型的验证组件,我们必须将 DataAnnotationsValidator 添加到 EditForm 组件中。
以下截图显示了 ValidationSummary 组件和单个 ValidationMesssage 组件的结果:

图 12.1:验证组件
验证组件使得向 Blazor WebAssembly 应用程序添加验证变得容易。
锁定导航
你完成表单后忘记在导航到另一页之前保存它的次数有多少?这种情况每个人都可能遇到。NavigationLock 组件可以用来通知用户他们即将离开当前页面,并允许他们取消该操作。它是通过拦截导航事件来做到这一点的。
这是一个示例 NavigationLock:
<NavigationLock ConfirmExternalNavigation="true"
OnBeforeInternalNavigation="HandleBeforeInternalNav" />
NavigationLock 类包含两个属性:
-
ConfirmExternalNavigation– 获取或设置是否应要求用户确认外部导航。默认值是false。 -
OnBeforeInternalNavigation– 获取或设置在发生内部导航事件时调用的回调。
这是一个从 OnBeforeInternalNavigation 属性调用的示例方法:
private async Task HandleBeforeInternalNav
(LocationChangingContext context)
{
if (context.IsNavigationIntercepted)
{
var confirm = await JS.InvokeAsync<bool>("confirm",
"Are you sure you are ready to leave?");
if (!confirm)
{
context.PreventNavigation();
}
}
}
在前面的代码中,IsNavigationIntercepted方法用于确定导航是否被链接拦截。如果是从链接拦截的,则显示一个 JavaScript confirm 对话框。如果用户未确认他们想要离开页面,则PreventNavigation方法将阻止导航发生。
提示
有关从 .NET 方法调用 JavaScript 函数的更多信息,请参阅第五章,使用 JavaScript 互操作性 (JS Interop) 构建本地存储服务。
现在,让我们快速了解一下本章将要构建的项目。
创建支出跟踪项目
在本章中,我们将构建一个用于跟踪旅行支出的项目。我们将能够查看、添加和编辑支出。支出将存储在 Microsoft SQL Server 数据库中。
这是已完成的应用程序的“主页”截图:

图 12.2:支出跟踪器的主页
这是已完成应用程序的“添加支出”页面截图:

图 12.3:支出跟踪器的添加支出页面
此项目的构建时间大约为 60 分钟。
项目概述
将使用 Microsoft 的Blazor WebAssembly App Empty项目模板创建ExpenseTracker项目,以创建一个托管的 Blazor WebAssembly 项目。首先,我们将添加 Bootstrap 和选项卡菜单。然后,我们将添加项目所需的类和 API 控制器。我们将在“主页”上添加一个表格来显示当前的支出列表。我们将使用EditForm组件与许多内置输入组件一起添加和编辑支出。最后,我们将添加一个NavigationLock组件以防止用户在导航到另一个页面时丢失他们的编辑。
创建支出跟踪项目
我们需要创建一个新的 Blazor WebAssembly 应用程序。我们按照以下步骤进行:
-
打开 Visual Studio 2022。
-
点击创建新项目按钮。
-
按Alt+S键进入搜索模板文本框。
-
输入
Blazor并按Enter键。以下截图显示了Blazor WebAssembly App Empty项目模板:
![图形用户界面,文本,应用程序,聊天或文本消息 自动生成的描述]()
图 12.4:Blazor WebAssembly App Empty 项目模板
-
选择Blazor WebAssembly App Empty项目模板并点击下一步按钮。
-
在项目名称文本框中输入
ExpenseTracker并点击下一步按钮。这是配置我们新项目的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 12.5:配置新项目对话框
提示
在前面的示例中,我们将
ExpenseTracker项目放置在E:/Blazor文件夹中。然而,此项目的位置并不重要。 -
选择.NET 7.0作为要使用的
Framework版本。 -
选择配置为 HTTPS复选框。
-
选择ASP.NET Core Hosted复选框。
-
取消选择渐进式 Web 应用程序复选框。
这是用于收集有关我们新项目附加信息的对话框截图:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 12.6:附加信息对话框
-
点击创建按钮。
我们现在已创建了一个托管的ExpenseTracker Blazor WebAssembly 项目。
ExpenseTracker解决方案包含三个不同的项目。以下 Visual Studio 的解决方案资源管理器截图显示了解决方案中的三个项目:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 12.7:解决方案资源管理器
-
右键单击ExpenseTracker.Server项目,从菜单中选择设置为启动项目选项。
我们已创建ExpenseTracker解决方案,并将ExpenseTracker.Server项目设置为启动项目。现在我们需要处理我们的 UI。我们将使用Bootstrap来设计我们的控件。
安装 Bootstrap
我们需要在我们的 Web 应用程序中安装Bootstrap。我们这样做如下:
-
右键单击
ExpenseTracker.Client/wwwroot/css文件夹,从菜单中选择添加,客户端库选项。 -
在库搜索文本框中输入
bootstrap并按Enter键。 -
选择选择特定文件。
-
只选择如图所示的css文件:
![图形用户界面,应用程序,电子邮件 自动生成的描述]()
图 12.8:添加客户端库对话框
提示
虽然前面的截图选择了 Bootstrap 的 5.2.3 版本,但您可以使用 Bootstrap 5 的任何版本来完成此项目。
-
点击安装按钮。
重要提示
安装
Bootstrap后,将在wwwroot/css文件夹中添加一个新文件夹。这个新文件夹包含所有用于Bootstrap的 CSS 文件。在本项目中,我们只将使用bootstrap.min.css文件。 -
打开
ExpenseTracker.Client/wwwroot/index.html文件。 -
在链接到
css/app.css样式的head元素之前添加以下标记:<link href="css/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
我们已安装Bootstrap。现在我们将添加类来存储费用信息。
添加以下类
我们需要添加一个ExpenseType类和一个Expense类。我们这样做如下:
-
右键单击
ExpenseTracker.Shared文件夹,从菜单中选择添加,类选项。 -
将新类命名为
ExpenseType。 -
点击添加按钮。
-
通过添加
public修饰符使类公开:**public** class ExpenseType -
向
ExpenseType类添加以下属性:public int Id { get; set; } public string? Type { get; set; } -
右键单击
ExpenseTracker.Shared文件夹,从菜单中选择添加,类选项。 -
将新类命名为
Expense。 -
点击添加按钮。
-
通过添加
public修饰符使类公开:**public** class Expense -
添加以下
using语句:using System.ComponentModel.DataAnnotations; -
将以下属性添加到
Expense类中:public int Id { get; set; } [Required] public DateTime? Date { get; set; } [Required] [MaxLength(100)] public string? Vendor { get; set; } public string? Description { get; set; } [Required] [Display(Name = "Expense Type")] public int? ExpenseTypeId { get; set; } [Required] [Range(0, 500, ErrorMessage = "The {0} field must be <= {2}")] public decimal? Amount { get; set; } public bool Paid { get; set; }在前面的代码中,我们使用了数据注释来添加一些简单的数据验证。
Date、Vendor、ExpenseTypeId和Amount都是必需的。Vendor的最大长度为 100 个字符。ExpenseTypeId的显示名称为ExpenseType。费用的Amount上限为 500。 -
从构建菜单中选择构建解决方案选项。
我们现在已添加了ExpenseType类和Expense类,并构建了我们的应用程序。现在我们需要配置 ASP.NET Web API 端点。
添加 API 控制器
我们需要为每个新类添加一个 API 控制器。我们这样做如下:
-
右键单击
ExpenseTracker.Server项目,从菜单中选择添加,新建文件夹选项。 -
将新文件夹命名为
Controllers。 -
右键单击
ExpenseTracker.Server.Controllers文件夹,从菜单中选择添加,控制器选项。 -
选择使用 Entity Framework 的 API 控制器和操作选项。
以下截图显示了添加新模板项对话框,其中使用 Entity Framework 的 API 控制器和操作选项被突出显示:
![图形用户界面,应用程序,团队 自动生成的描述]()
图 12.9:添加新模板项对话框
-
点击添加按钮。
-
将模型类设置为ExpenseType (ExpenseTracker.Shared)。
-
点击添加数据上下文按钮以打开添加数据上下文对话框:

图 12.10:添加 API 控制器和操作,使用 Entity Framework 对话框
- 点击添加按钮以接受默认值。

图 12.11:添加数据上下文对话框
-
在添加 API 控制器和操作,使用 Entity Framework对话框中点击添加按钮。
我们已创建了
ExpenseTypeController类。现在我们需要重复前面的步骤来创建ExpenseController类。 -
右键单击
ExpenseTracker.Server.Controllers文件夹,从菜单中选择添加,控制器选项。 -
选择使用 Entity Framework 的 API 控制器和操作选项。
-
点击添加按钮。
-
将模型类设置为Expense (ExpenseTracker.Shared)。
-
点击添加按钮。
我们已添加了两个新的控制器,以提供应用程序将使用的 API 端点。有关使用 ASP.NET Web API 的更多信息,请参阅第十一章,使用 ASP.NET Web API 构建任务管理器。
接下来,我们需要创建 SQL Server 数据库。
创建 SQL Server 数据库
我们需要创建 SQL Server 数据库并为支出和支出类型添加一个表。我们这样做如下:
-
打开
ExpenseTracker.Server/appsettings.json文件。 -
更新连接字符串如下:
"ConnectionStrings": { "ExpenseTrackerServerContext": "Server={Server name}; Database=ExpenseTracker; Trusted_Connection=True; Encrypt=False;" } -
将
{Server name}占位符替换为你的 SQL Server 名称。重要提示
虽然我们正在使用 SQL Server Express 2022,但对于此项目来说,你使用什么版本的 SQL Server 都无关紧要。
-
打开
ExpenseTracker.Server.Data/ExpenseTrackerServerContext.cs文件。 -
添加以下
OnModelCreating方法:protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBuilder.Entity<ExpenseType>().HasData( new ExpenseType { Type = "Airfare", Id = 1 }, new ExpenseType { Type = "Lodging", Id = 2 }, new ExpenseType { Type = "Meal", Id = 3 }, new ExpenseType { Type = "Other", Id = 4 } ); }之前的代码将为
ExpenseType表添加种子数据。 -
从 工具 菜单中选择 NuGet 包管理器,包管理控制台 选项。
-
在 包管理控制台 中,验证 默认项目 是否设置为 ExpenseTracker.Server。
-
在 包管理控制台 中执行以下命令:
Add-Migration Init Update-Database之前的命令使用
Entity Framework迁移来更新 SQL Server。 -
按 Ctrl+F5 组合键以不带调试模式启动应用程序。
-
在地址栏中添加
/api/expensetypes并按 Enter 键。以下截图显示了
ExpenseTypesController返回的 JSON:![包含文本的图片 描述由自动生成]()
图 12.12:ExpenseTypes API 控制器返回的 JSON
-
关闭浏览器。
我们已经在 SQL Server 上创建了一个新数据库,添加了两个表,并使用种子数据填充了一个表。在完成设置 SQL Server 后,我们测试了 ExpenseTypesController 是否正常工作。最后,我们准备创建一个组件来显示存储在 SQL Server 中的支出。
查看支出
我们需要在 主页 中添加一个表格来显示支出列表。我们这样做如下:
-
返回 Visual Studio。
-
打开
ExpenseTracker.Client.Pages/Index.razor页面。 -
删除
h1元素。 -
添加以下代码:
@using ExpenseTracker.Shared @inject HttpClient Http <h2>Expenses</h2> @if (expenses == null) { <p><em>Loading…</em></p> } else if (expenses.Count == 0) { <div>None Found</div> } else { } @code { List<Expense>? expenses; }之前的代码将
expenses定义为List<Expense>并检查它是否为空或为空。如果是空的,它将渲染加载信息;如果是空的,它将渲染 未找到 信息。 -
向代码块中添加以下
OnInitializedAsync方法:protected override async Task OnInitializedAsync() { expenses = await Http.GetFromJsonAsync <List<Expense>>("api/expenses"); }之前的代码通过使用
HttpClient的GetFromJsonAsync方法来填充expenses对象。有关HttpClient的更多信息,请参阅 第十一章,构建任务管理器用户 ASP.NET Web API。 -
在
else语句中添加以下table元素:<table class="table"> </table> -
向
table元素中添加以下thead元素:<thead> <tr> <th></th> <th>#</th> <th>Date</th> <th>Vendor</th> <th class="text-right">Amount</th> </tr> </thead> -
在
thead元素之后添加以下tbody元素到table元素中:<tbody> @foreach (var item in expenses) { <tr class="@(item.Paid ? "" : "table-danger")"> <td> <a href="/expense/@item.Id">Edit</a> </td> <td>@item.Id</td> <td>@item.Date!.Value.ToShortDateString()</td> <td>@item.Vendor</td> <td class="text-right">@item.Amount</td> </tr> } </tbody>之前的代码遍历集合中的每个
Expense对象,并将它们作为表格中的行显示。如果支出尚未支付,则该行将使用table-danger类突出显示为红色。 -
按 Ctrl+F5 组合键以不带调试模式启动应用程序。
这是我们的应用程序的截图:
![图形用户界面、文本、应用程序、聊天或文本消息 描述自动生成]()
图 12.13:ExpenseTracker 的主页
-
关闭浏览器窗口。
我们已经添加了在 Home 页面上以表格形式显示费用的功能。接下来,我们需要添加添加费用的功能。
添加编辑费用组件
我们需要添加一个组件以使我们能够添加和编辑费用。我们这样做如下:
-
返回 Visual Studio。
-
打开
ExpenseTracker.Client.MainLayout.razor页面。 -
在
main元素之前添加以下标记:<ul class="nav nav-tabs bg-secondary bg-opacity-10"> <li class="nav-item"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> Home </NavLink> </li> <li class="nav-item"> <NavLink class="nav-link" href="expense"> Add Expense </NavLink> </li> </ul> -
上述标记使用
Bootstrap渲染具有两个选项的选项卡界面:主页 和 添加费用。 -
更新主元素如下以添加一些填充到渲染元素:
<main **class****=****"p-3"**> @Body </main> -
右键单击
ExpenseTracker.Client.Pages文件夹,并从菜单中选择 添加、Razor 组件 选项。 -
将新组件命名为
ExpenseEdit。 -
点击 添加 按钮。
-
更新标记如下:
@page "/expense" @page "/expense/{id:int}" @using ExpenseTracker.Shared @using Microsoft.AspNetCore.Components.Forms @inject HttpClient Http @inject NavigationManager Nav @if (id == 0) { <h2>Add Expense</h2> } else { <h2>Edit Expense</h2> } @if (!ready) { <p><em>Loading...</em></p> } else { <EditForm Model="expense" OnValidSubmit="HandleValidSubmit"> </EditForm> <div>@error</div> } @code { }上述代码在组件准备就绪时显示
EditForm。它使用id参数的值来确定表单是在执行添加操作还是编辑操作。 -
将以下代码添加到代码块中:
[Parameter] public int id { get; set; } private bool ready; private string? error; private Expense? expense = new(); private List<ExpenseType>? types; -
将以下
OnInitializedAsync方法添加到代码块中:protected override async Task OnInitializedAsync() { types = await Http.GetFromJsonAsync<List<ExpenseType>> ("api/ExpenseTypes"); if (id > 0) { try { } catch (Exception) { Nav.NavigateTo("/"); } } ready = true; }上述代码初始化了
types对象和expense对象。一旦它们都被初始化,ready的值被设置为true。 -
将以下代码添加到
try块中:var result = await Http.GetFromJsonAsync<Expense> ($"api/Expenses/{id}"); if (result != null) { expense = result; }上述代码初始化了费用对象。
-
将以下
HandleValidSubmit方法添加到代码块中:private async Task HandleValidSubmit() { HttpResponseMessage response; if (expense!.Id == 0) { response = await Http.PostAsJsonAsync ("api/Expenses", expense); } else { string requestUri = $"api/Expenses/{expense.Id}"; response = await Http.PutAsJsonAsync (requestUri, expense); }; if (response.IsSuccessStatusCode) { Nav.NavigateTo("/"); } else { error = response.ReasonPhrase; }; }
上述代码通过使用 PostAsJsonAsync 方法添加新费用,并通过使用 PutAsJsonAsync 方法更新现有费用。如果相关方法成功,用户将被返回到 主页。否则,将显示错误消息。
我们已经完成了此组件的代码,但 EditForm 仍然为空。我们现在需要向 EditForm 添加一些标记。
添加输入组件
我们需要在 EditForm 元素中添加输入组件。我们这样做如下:
-
将以下标记添加到
EditForm中以输入Date属性:<div class="row mb-3"> <label> Date <InputDate @bind-Value="expense.Date" class="form-control" /> </label> </div> -
将以下标记添加到
EditForm中以输入Vendor属性:<div class="row mb-3"> <label> Vendor <InputText @bind-Value="expense.Vendor" class="form-control" /> </label> </div> -
将以下标记添加到
EditForm中以输入Description属性:<div class="row mb-3"> <label> Description <InputTextArea @bind-Value="expense.Description" class="form-control" /> </label> </div> -
将以下标记添加到
EditForm中以输入ExpenseTypeId属性:<div class="row mb-3"> <label> Expense Type <InputSelect @bind-Value="expense.ExpenseTypeId" class="form-control"> <option value=""></option> @foreach (var item in types!) { <option value="@item.Id"> @item.Type </option> } </InputSelect> </label> </div> -
将以下标记添加到
EditForm中以输入Amount属性:<div class="row mb-3"> <label> Amount <InputNumber @bind-Value="expense.Amount" class="form-control" /> </label> </div> -
将以下标记添加到
EditForm中以输入Paid属性:<div class="row mb-3"> <label> Paid? <InputCheckbox @bind-Value="expense.Paid" class="form-check-input mx-1" /> </label> </div> -
将以下标记添加到
EditForm的Submit按钮中:<div class="pt-2 pb-2"> <button type="submit" class="btn btn-primary mr-auto"> Save </button> </div> -
将以下标记添加到
EditForm中以添加验证摘要:<DataAnnotationsValidator /> <ValidationSummary /> -
打开
ExpenseTracker.Client.wwroot/css/app.css文件。 -
添加以下样式:
.invalid { outline: 1px solid red; } .validation-message { color: red; } h2 { color: darkblue; }上述样式为相关元素提供验证样式,并将
h2元素的颜色更改为深蓝色。 -
按 Ctrl+F5 以不带调试启动应用程序。
-
选择 添加费用 链接。
-
点击 保存 按钮。
以下截图显示了验证错误:
![图形用户界面,文本,应用程序,电子邮件 自动生成的描述]()
图 12.14:ExpenseEdit 组件的数据验证
测试编辑支出组件
-
添加一个有效的支出。
-
点击保存按钮。
如果支出有效,点击保存按钮将支出保存到 SQL Server 数据库,并将用户返回到主页。
-
点击新支出旁边的编辑链接。
-
修改支出。
-
点击保存按钮。
-
点击添加支出链接。
-
添加另一个有效的支出。
-
点击保存按钮。
-
点击添加支出链接。
-
添加另一个有效的支出,但不要点击保存按钮。
-
点击主页链接。
-
点击添加支出链接。
有效的支出已消失。
如果用户在点击保存按钮之前离开页面,他们所有的数据输入都会丢失。为了防止这种情况发生,我们可以通过使用NavigationLock组件来锁定他们的导航。
锁定导航
我们需要添加一个NavigationLock组件。我们这样做如下:
-
返回 Visual Studio。
-
打开
ExpenseTracker.Client.Pages/ExpenseEdit.razor页面。 -
添加以下
@inject指令:@inject IJSRuntime JS -
在
@inject指令下方添加以下NavigationLock:<NavigationLock ConfirmExternalNavigation="true" OnBeforeInternalNavigation="HandleBeforeInternalNav" /> -
将以下代码添加到代码块中:
private async Task HandleBeforeInternalNav (LocationChangingContext context) { if (context.IsNavigationIntercepted) { var confirm = await JS.InvokeAsync<boo>("confirm", "Are you sure you are ready to leave?"); if (!confirm) { context.PreventNavigation(); } } }上述代码使用 JavaScript 在用户使用链接离开当前页面时显示确认对话框。
-
按Ctrl+F5启动应用程序,不进行调试。
-
选择添加支出链接。
-
添加一个有效的支出。
-
点击主页链接。
将显示以下
确认对话框:![图形用户界面,文本,应用程序,Word 自动生成的描述]()
图 12.15:确认对话框
-
点击取消按钮取消导航。
我们已经完成了支出跟踪项目。
摘要
现在,您应该能够使用EditForm组件与内置输入组件一起创建输入数据表单。您还应该熟悉内置验证组件。最后,您应该了解如何锁定导航。
在本章中,我们介绍了内置的EditForm组件、各种输入组件和验证组件。我们还介绍了一个可以用来锁定用户导航的组件。之后,我们使用Blazor WebAssembly App项目模板创建了一个多项目解决方案。我们添加了一些类和一些 API 控制器。接下来,我们通过更新数据库的连接字符串和使用Entity Framework迁移来配置 SQL Server。我们更新了Home页面以显示支出列表。我们添加了一个包含EditForm组件和许多内置输入组件的新页面,用于输入、验证和提交支出。最后,我们添加了一个NavigationLock组件。
我们可以将我们的新技能应用到任何 Blazor WebAssembly 应用程序中,以添加数据输入和验证。
下一步是开始构建您自己的 Web 应用程序。为了保持最新状态并了解更多关于 Blazor WebAssembly 的信息,请访问 blazor.net,并阅读 devblogs.microsoft.com/dotnet/category/aspnet/ 上的 ASP.NET 博客。
我们希望您喜欢这本书,并祝您一切顺利!
问题
以下问题供您考虑:
-
使用内置输入组件的优点是什么?
-
你会如何更新
HandleBeforeInternalNav方法,以便只有在存在未保存的更改时才显示确认对话框? -
EditForm组件的目的是什么?
进一步阅读
以下资源提供了关于本章主题的更多信息:
-
关于 ASP.NET Core 组件表单的更多信息,请参阅
learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.forms。 -
关于数据注释的更多信息,请参阅
learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations。 -
关于路由的更多信息,请参阅
learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.routing。
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

























































































浙公网安备 33010602011771号