Blazor-和--NET-Web-开发-全-
Blazor 和 .NET Web 开发(全)
零、前言
直到现在,创建交互式网页意味着使用 JavaScript。但是有了 Blazor,微软的新创造方式.NET web 应用,开发人员可以使用 C#轻松构建交互式和丰富的 web 应用。这本书将引导你通过最常见的场景,当你开始你的 Blazor 之旅。
首先,您将发现如何利用 Blazor 的功能,并了解如何利用服务器端和 WebAssembly。通过向您展示所有元素实际上是如何协同工作的,这本书将帮助您解决开发人员面临的一些常见障碍。随着学习的深入,您将学习如何创建服务器端 Blazor 和 Blazor WebAssembly 项目,Razor 语法如何工作,以及如何验证表单和创建自己的组件。然后,这本书向您介绍了使用 Blazor 进行网络开发所涉及的关键概念,您将能够立即将其付诸实践。
在本 Blazor 书的最后,您将获得创建和部署生产就绪 Blazor 应用的信心。
这本书是给谁的
这本书是为那些想要探索 Blazor 来学习如何构建动态 web UIs 的 web 开发人员和软件开发人员准备的。这本书假设熟悉 C#编程和 web 开发概念。
这本书涵盖了什么
第一章你好 Blazor ,将教你服务器端和客户端 Blazor 的区别。您将获得该技术如何工作的概述,以及 Blazor 来源的简史。了解托管模型之间的结构和差异对于理解技术至关重要。
第二章创建你的第一个 Blazor App ,帮助你了解如何安装和设置你的开发环境。您将创建第一个 Blazor 应用(服务器端和客户端),并了解项目模板的结构。
第三章介绍实体框架核心,教你如何创建你的数据库,你将在其中存储你的数据(博客文章、类别和标签)。您将使用 dotnet 工具创建一个新项目,以获得对该工具的感觉。
第四章了解基本 Blazor 组件,深入挖掘组件、生命周期事件、添加参数、组件间共享参数。在本章中,您还将创建可重用组件。
第 5 章创建高级 Blazor 组件更深入地挖掘组件,添加子组件、级联参数和值等功能,并介绍如何使用动作和回调。
第六章用验证构建表单,看一看表单,如何验证表单,如何构建自己的验证机制。本章将介绍处理表单时最常见的用例,如文件上传、文本、数字,以及勾选复选框时的触发代码。
第七章创建 API ,看一下创建 API。当使用 Blazor WebAssembly 时,我们需要一个 API 来获取数据。
第八章身份验证和授权,着眼于为 Blazor 添加身份验证和授权,确保重定向到登录页面等导航按预期工作。
第 9 章共享代码和资源,教你如何通过将你需要的所有东西添加到一个共享库中,在客户端和服务器端 Blazor 项目之间共享代码。在本章中,您将构建一个共享库,该库可以打包为一个 NuGet 包并与其他人共享。
第 10 章JavaScript Interop探讨了在使用 Blazor 时如何利用 JavaScript 库,从 C#调用 JavaScript。您还将了解 JavaScript 如何在我们的 Blazor 应用中调用 C#函数。
第 11 章管理状态探讨了管理状态(持久化数据)的不同方式,例如使用 LocalStorage 或者仅仅通过依赖注入将数据保存在内存中。您不仅将介绍数据库中的持久数据,还将介绍依赖注入如何在不同的项目类型上工作。
第 12 章调试,教你如何调试应用,添加扩展日志,弄清楚你的应用有什么问题。您不仅会看到传统的调试,还会看到直接从 web 浏览器中调试 C#代码。
第 13 章测试,着眼于自动化测试,这样你就可以确保你的组件正常工作(并继续这样做)。没有内置的方法来测试 Blazor 应用,但是有一个非常好的社区项目叫做 bUnit。
第 14 章部署到生产,将带您了解在生产中运行 Blazor 需要考虑的不同事情。
第 15 章从这里去哪里,是一个短章,有行动号召,有可以利用的资源,还有压轴。
为了充分利用这本书
我建议您阅读前几章,以确保您对 Blazor 的基本概念有所了解。我们正在创建的项目适合现实世界的使用,但有些部分被遗漏了,例如正确的错误处理。但是,您应该很好地掌握 Blazor 的构造块。
本书重点介绍使用 Visual Studio 2019 尽管如此,请随意使用任何支持 Blazor 的版本。

如果您正在使用本书的数字版本,我们建议您自己键入代码或通过 GitHub 存储库访问代码(下一节中提供了链接)。这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。
我很乐意让你在阅读这本书的时候或者在 Blazor 开发的时候分享你的进步。发推给我@EngstromJimmy。
我希望你读这本书和我写这本书一样开心。
下载示例代码文件
可以从https://GitHub . com/packt publishing/Web-Development-with-Blazor下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:https://static . packt-cdn . com/downloads/9781800208728 _ color images . pdf。
使用的约定
本书通篇使用了许多文本约定。
Code in text:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“在这种情况下,当页面被下载时,它将触发blazor.webassembly.js文件的下载。”
代码块设置如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
}
当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示:
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">MyBlogServerSide</a>
<button class="navbar-toggler"
@onclick="ToggleNavMenu">
任何命令行输入或输出都编写如下:
dotnet new blazorserver -o BlazorServerSideApp
cd MyBlog.Data
粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“从管理面板选择系统信息
提示或重要注意事项
像这样出现。
取得联系
我们随时欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在留言主题中提及书名,并通过customercare@packtpub.com发邮件给我们。
勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请联系我们在copyright@packt.com与材料的链接。
如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com。
分享你的想法
一旦你阅读了Blazor 的网络开发,我们很想听听你的想法!扫描下面的二维码,直接进入这本书的亚马逊评论页面,分享你的反馈。

https://www.amazon.in/review/create-review/error?asin=1-800-20872-3 &
您的评论对我们和技术社区非常重要,将帮助我们确保提供优质内容。
一、你好 Blazor
感谢您用 Blazor 获取您的网络开发副本。这本书旨在让你尽可能快速、无痛苦地开始,一章一章,而不必在开始阅读之前从头到尾阅读这本书。
这本书将从指导您开始 Blazor 之旅时遇到的最常见场景开始,还将深入探讨一些更高级的场景。这本书的目标是向你展示什么是 Blazor——Blazor 服务器和 Blazor 网络组装——它是如何实际工作的,并帮助你避免一路上的任何陷阱。
人们普遍认为 Blazor 是 WebAssembly,但 WebAssembly 只是运行 Blazor 的一种方式。Blazor 上的许多书籍、研讨会和博客文章都非常关注网络组装。这本书将涵盖网络组装和服务器端。Blazor 服务器和 Blazor WebAssembly 之间有一些区别,我将在后面指出这些区别。
这第一章将探究 Blazor 从何而来,是什么技术使 Blazor 成为可能,以及运行 Blazor 的不同方式。我们还将讨论哪种类型最适合您。
在本章中,我们将涵盖以下主题:
- Blazor 之前
- 介绍网络组装
- 介绍.NET 5
- 介绍 Blazor
技术要求
建议您了解.NET,因为这本书的目标是.NET 开发人员,他们希望利用自己的技能来制作交互式网络应用。然而,你很有可能会捡到一些。如果你是. NET 世界的新手,一路上会遇到很多技巧
在 Blazor 之前
你可能没有读到这本书关于 JavaScript 的内容,但是记住我们来自一个 Blazor 时代之前的 T2 是有帮助的。我记得那个时代——黑暗时代。Blazor 中使用的许多概念与许多 JavaScript 框架中使用的概念相差不远,因此我将首先简要概述我们面临的挑战。
作为开发人员,我们有许多不同的平台可以开发,包括桌面、移动、游戏、云(或服务器端)、人工智能,甚至物联网。所有这些平台都有许多不同的语言可供选择,但当然还有一个平台:在浏览器内部运行的应用。
我已经做了很长时间的 web 开发人员,我已经看到代码从服务器上转移,这样它就可以在浏览器中运行。它改变了我们开发应用的方式。Angular、React、Aurelia 和 Vue 等框架已经改变了网络,从必须重新加载整个页面到动态更新页面的一小部分。这个新的动态更新方法使得页面加载更快,因为感知的加载时间降低了(不一定是整个页面加载)。
但是对于很多开发者来说,这是一个全新的技能集需要学习;也就是说,在服务器(很可能是 C#,如果你正在读这本书的话)和用 JavaScript 开发的前端之间切换。数据对象在后端用 C#编写,然后序列化为 JSON,通过 API 发送,然后在前端反序列化为另一个用 JavaScript 编写的对象。
JavaScript 使用在不同的浏览器中以不同的方式工作,jQuery 试图通过拥有一个通用的 API 来解决这个问题,该 API 被翻译成网络浏览器可以理解的东西。现在,不同 web 浏览器之间的差异要小得多,这使得 jQuery 在许多情况下已经过时。
例如,JavaScript 与其他语言有点不同,因为它不是面向对象的或类型化的。2010 年,安德斯·海尔斯伯格(以 C#、Delphi 和 Turbo Pascal 的原始语言设计者而闻名)开始着手于 TypeScript 的工作,这是一种可以编译成 JavaScript 的面向对象语言。
您可以将 Typescript 与 Angular、React、Aurelia 和 Vue 一起使用,但最终运行实际代码的是 JavaScript。简而言之,要使用 JavaScript/TypeScript 创建当今的交互式网络应用,您需要在语言之间切换,并且还要选择和跟上不同的框架。
在这本书里,我们将以另一种方式看待这个问题。尽管我们将讨论 JavaScript,但我们的主要重点将是开发主要使用 C#的交互式网络应用。
现在,我们知道了一些关于 JavaScript 的历史。由于 WebAssembly,JavaScript 不再是唯一可以在浏览器中运行的语言,我们将在下一节中介绍它。
介绍网络组装
在本节中,我们将了解网络组件的工作原理。运行 Blazor 的一种方法是使用 WebAssembly,但是现在,让我们专注于什么是 WebAssembly。
WebAssembly 是一种经过编译的二进制指令格式,因此更小。它是为本机速度而设计的,这意味着在速度方面,它更接近 C++而不是 JavaScript。加载 JavaScript 时,会下载、解析、优化和 JIT 编译 JS 文件(或内联文件);当涉及到 WebAssembly 时,大多数这些步骤都是不需要的。
WebAssembly 有一个非常严格的安全模型,可以保护用户免受错误或恶意代码的侵害。它在一个沙箱中运行,如果不通过适当的 API,就无法逃离该沙箱。如果你想在 WebAssembly 之外进行通信,例如,通过更改文档对象模型 ( DOM )或从 web 下载文件,你将需要通过 JavaScript interop 来实现(稍后会有更多相关内容,别担心——Blazor 会为我们解决这个问题)。
为了更熟悉网络组装,让我们看一些代码。
在这一部分,我们将创建一个应用,将两个数字相加并返回结果,用 C 语言编写(老实说,这大约是我所熟悉的 C 语言水平)。
我们可以通过几个简单的步骤将 C 编译成 WebAssembly:
-
添加以下代码:
int main() { return 1+2; } -
按建立,然后运行。
您将看到数字3显示在页面底部的输出窗口中,如下图所示:

图 1.1–WasmFiddle
WebAssembly 是一种堆栈机器语言,这意味着它使用堆栈来执行其操作。
请考虑以下代码:
1+2
大多数编译器(包括我们刚才用的那个)都是去优化代码,简单的返回3。
但是让我们假设所有的指令都应该被执行。这是网络组装的方式:
- 首先将
1推到堆栈上(instruction: i32.const 1),然后将2推到堆栈上(instruction: i32.const 2)。此时,堆栈包含1和2。 - 然后,我们必须执行加法指令(
i32.add),它将从堆栈中弹出(get)两个顶值(1和2),将它们相加,并将新值推送到堆栈中(3)。
这个演示展示了我们可以从 C 代码构建 WebAssembly。现在,我们有了在浏览器中运行的编译成网络程序集的 C 代码。
其他语言
一般只有低级语言才能编译成 WebAssembly(比如 C 或 Rust)。然而,有太多的语言可以运行在网络汇编之上。这里有大量这些语言的集合:https://github.com/appcypher/awesome-wasm-langs。
WebAssembly 的性能非常高(接近本地速度)——如此高的性能以至于游戏引擎已经为此采用了这项技术。Unity,以及虚幻引擎,都可以编译成 WebAssembly。
这里有几个在网络组件上运行的游戏的例子:
- 愤怒的机器人(Unity):https://beta.unity3d.com/jonas/AngryBots/
- doom:https://wam . continuation-labs . com/D3 demo/
这是一个惊人的不同网络组装项目列表:https://github.com/mbasso/awesome-wasm。
这一部分触及了网络组装的工作原理,在大多数情况下,您不需要知道更多。我们将在本章后面深入探讨 Blazor 如何使用这项技术。
要编写 Blazor 应用,我们必须利用的力量.NET 5,我们接下来会看它。
介绍.NET 5
要构建 Blazor 应用,我们必须使用.NET 5 。那个.NET 团队多年来一直在努力为我们开发人员收紧一切。他们一直在使一切变得更简单、更小、跨平台和开源,更不用说更容易利用您现有的知识.NET 开发。
.NET core 是迈向更统一的. NET 之旅的一步,它让微软重新审视了整个系统.NET 平台,并以全新的方式进行构建。
有三种不同的类型.NET 运行时:
- 。. NET 框架(完整版.NET)
- .NET Core
- 猴子/洗发精
不同的运行时有不同的功能和性能。这也意味着创建一个. NET Core 应用(例如)需要安装不同的工具和框架。
.NET 5 是我们迈向单一. NET 之旅的开始。有了这个统一的工具链,创建、运行等体验将在所有不同的项目类型中保持一致。.NET 5 仍然是以我们习惯的类似方式模块化的,所以我们不必担心合并所有不同的.NET 版本将导致一个臃肿的. NET
多亏了.NET 平台,您将能够只使用 C#并使用相同的工具就可以到达我们在本章开头谈到的所有平台(web、桌面、移动、游戏、云(或服务器端)、AI,甚至 IoT)。
现在您已经了解了一些周围的技术,在下一节中,是时候介绍这本书的主角:Blazor 了。
介绍 Blazor
Blazor 是一个开源 web UI SPA 框架。在同一句话中有很多流行语,但简单地说,这意味着您可以使用 HTML、CSS 和 C#创建交互式 SPA 网络应用,完全支持绑定、事件、表单和验证、依赖注入、调试等。我们将看看这本书。
2017 年,史蒂夫·桑德森(以创建淘汰赛 JavaScript 框架而闻名,他在微软的 ASP.NET 团队工作)即将参加一个名为的会议,网络应用不可能真的做到这一点,对吗?在 NDC 奥斯陆的开发者大会上。
但是史蒂夫想展示一个很酷的演示,所以他对自己说,在 WebAssembly 中运行 C#可能吗?他在 GitHub 上发现了一个旧的不活跃的项目,名为 Dot Net Anywhere ,它是用 C 语言编写的,并且使用工具(类似于我们刚刚做的)将 C 代码编译成 WebAssembly。
他在浏览器中运行了一个简单的控制台应用。对大多数人来说,这将是一个惊人的演示,但史蒂夫想更进一步。他想,有没有可能在此基础上创建一个简单的 web 框架?,接着看他是否也能让工装正常工作。
到了他的会议时间,他有一个工作示例,在这里他可以创建一个新项目,创建一个具有强大工具支持的待办事项列表,然后在浏览器中运行该项目。
达米安·爱德华兹(the.NET 团队)和大卫·福勒(the.NET 团队)也参加了 NDC 会议。史蒂夫向他们展示了他将要演示的东西,他们描述了他们的头爆炸和下巴落下的事件。
Blazor 的原型就是这样产生的。
Blazor 这个名字来自于 Browser 和 Razor 的结合(这是一种用于将代码和 HTML 结合起来的技术)。增加一个 L 让名字听起来更好,但除此之外,它没有真正的含义或首字母缩写。
有几种不同风格的 Blazor 服务器,包括 Blazor WebAssembly、WebWindow 和移动绑定。不同版本有一些优点和缺点,我将在接下来的章节中介绍所有这些。
Blazor 服务器
Blazor 服务器使用 SignalR 在客户端和服务器之间进行通信,如下图所示:

图 1.2–Blazor 服务器概述
signor是一个开源的实时通信库,可以在客户端和服务器之间建立连接。SignalR 可以使用许多不同的方式传输数据,并根据您的服务器和客户端功能自动为您选择最佳的传输协议。SignalR 将一直尝试使用 WebSockets,这是一个内置在 HTML5 中的传输协议。如果由于任何原因没有启用网络套接字,它将优雅地退回到另一个协议。
Blazor 是用名为组件的可重用 UI 元素构建的(更多关于 第三章引入实体框架核心中的组件)。每个组件都包含 C#代码、标记,甚至可以包含另一个组件。如果你愿意,你可以使用 Razor 语法混合标记和 C#代码,甚至用 C#做任何事情。组件可以通过用户交互(按下按钮)或触发器(如计时器)来更新。
这些组件被渲染到一个渲染树中,这是一个包含对象状态和任何属性或值的二进制表示。渲染树将跟踪与前一个渲染树相比的任何变化,然后使用二进制格式仅发送通过 SignalR 更改的内容来更新 DOM。
在客户端,JavaScript 将接收到更改并相应地更新页面。如果我们将其与传统的 ASP.NET 进行比较,我们只呈现组件本身,而不是整个页面,并且我们只发送对 DOM 的实际更改,而不是整个页面。
当然,Blazor 服务器有一些缺点:
- 您需要始终连接到服务器,因为渲染是在服务器上完成的。如果你有一个坏的互联网连接,该网站可能无法工作。与非 Blazor 服务器站点相比,最大的区别在于非 Blazor 服务器站点可以传递一个页面,然后断开连接,直到它请求另一个页面。使用 Blazor,必须始终连接该连接(信号员)(轻微断开也可以)。
- 没有离线/PWA 模式,因为它需要连接。
- 每次点击或页面更新都必须往返于服务器,这可能会导致更高的延迟。重要的是要记住,Blazor 服务器将只发送已更改的数据。我没有经历过任何缓慢的响应时间。
- 因为我们必须连接到服务器,所以服务器上的负载会增加,并且难以扩展。要解决这个问题,您可以使用 Azure SignalR 集线器,它将处理持续的连接,并让您的服务器专注于传递内容。
- 为了能够运行它,您必须将其托管在支持 ASP.NET Core 的服务器上。
然而,Blazor 服务器也有优势:
- 它只包含足够的代码来建立下载到客户端的连接,这样站点的占用空间就很小。
- 由于我们在服务器上运行,应用可以充分利用服务器的功能。
- 该网站将在不支持网络组装的旧网络浏览器上运行。
- 代码在服务器上运行并留在服务器上;没有办法对代码进行反编译。
- 由于代码是在您的服务器(或云中)上执行的,您可以直接调用组织内的服务和数据库。
在我的工作场所,我们已经有了一个大型网站,所以我们决定在我们的项目中使用 Blazor Server。我们有一个客户门户和一个内部客户关系管理工具。我们的方法是一次获取一个组件,并将其转换为 Blazor 组件。
我们很快意识到,在大多数情况下,在 Blazor 中重新构建组件比继续使用 ASP.NET MVC 并在此基础上添加功能更快。随着我们的转换,终端用户的用户体验 ( UX )变得更好了。
页面加载得更快,我们可以根据需要重新加载页面的一部分,而不是整个页面,等等。
不过,我们确实发现 Blazor 引入了一个新问题:页面变得太快了。我们的用户不明白数据是否已经保存,因为什么都没发生;事情确实发生了,但是太快了,用户没有注意到。突然之间,我们不得不更多地考虑 UX,以及如何通知用户发生了变化。在我看来,这当然是 Blazor 非常积极的副作用。
Blazor 服务器并不是运行 Blazor 的唯一方法——您也可以使用 WebAssembly 在客户端(在 web 浏览器中)上运行它。
Blazor WebAssembly
还有一个选择:不用在服务器上运行 Blazor,你可以在你的网页浏览器里面使用 WebAssembly 运行它。
正如我们之前提到的,目前还没有办法将 C#编译成 WebAssembly。取而代之的是,微软采用了 mono 运行时(用 C 语言编写),并将其编译成了 WebAssembly。
Blazor 的 WebAssembly 版本与服务器版本非常相似,如下图所示。我们已经将所有内容从服务器上移除,现在它正在我们的网络浏览器中运行:

图 1.3–Blazor 尔网组件概述
渲染树仍然被创建,并且不再在服务器上运行 Razor 页面,而是在我们的网络浏览器中运行。由于 WebAssembly 没有直接的 DOM 访问,Blazor 用直接的 JavaScript 互操作来更新 DOM,而不是 SignalR。
编译到网络汇编中的 mono 运行时叫做dotnet . wasm。该页面包含一小段 JavaScript,将确保加载dotnet.wasm。然后,它会下载blazor.boot.json,这是一个 JSON 文件,包含应用需要能够运行的所有文件,以及应用的入口点。
如果我们看一下在 Visual Studio 中启动一个新的 Blazor 项目时创建的默认示例站点,Blazor.boot.json文件包含 63 个需要下载的依赖项。所有依赖项都被下载,应用启动。
正如我们之前提到的,dotnet.wasm是编译到网络组件中的单声道运行时。它跑了.NET 动态链接库–您已经编写的动态链接库,以及.NET 框架(运行你的应用需要它)——在你的浏览器里面。
当我第一次听到这个的时候,我的嘴里有一点不好的味道。它正在运行整个.NET 运行时?!但是,过了一会儿,我意识到这有多神奇。你可以用任何一种.NET 标准动态链接库,并在您的网络浏览器中运行它们。
在下一章中,我们将看看当一个 WebAssembly 应用启动时,到底发生了什么,以及代码执行的顺序。
最大的担忧是网站的下载量。简单的文件新的示例应用的大小约为 1.3 MB,如果你在下载大小上投入大量精力,这是相当大的。不过,你应该记住的是,这更像是一个单页应用(SPA)——已经下载到客户端的是整个网站。我把规模和网上一些知名网站做了对比;然后我只包含了这些站点的 JS 文件,但也包含了 Blazor 的所有 dll 和 JavaScript 文件。
以下是我的发现图表:

图 1.4–热门网站的 JavaScript 下载量
即使其他站点比 Blazor 示例站点大,您也应该记住 Blazor DLLs 是编译的,应该比 JavaScript 文件占用更少的空间。WebAssembly 也比 JavaScript 快。
Blazor WebAssembly 有一些缺点:
- 即使我们将其与其他大型网站进行比较,Blazor WebAssembly 的占地面积也很大,并且有大量文件需要下载。
- 要访问任何现场资源,您需要创建一个网络应用编程接口来访问它们。您不能直接访问数据库。
- 代码在浏览器中运行,这意味着它可以被反编译。这是所有应用开发人员都习惯的事情,但对于网络开发人员来说,这可能并不常见。
当然,Blazor WebAssembly 也有一些优势:
- 由于代码在浏览器中运行,因此很容易创建进步网络应用 ( PWA )。
- 因为我们没有在服务器上运行任何东西,所以我们可以使用任何类型的后端服务器,甚至文件共享(后端不需要. NET 兼容的服务器)。
- 没有往返意味着你可以更快地更新屏幕(这就是为什么有使用网络组装的游戏引擎)。
我想把最后的优势付诸实践!当我 7 岁的时候,我得到了我的第一台电脑,一台辛克莱 ZX 频谱。我记得我坐下来写了以下内容:
10 PRINT "Jimmy"
20 GOTO 10
那是我的代码;我让电脑一遍又一遍地在屏幕上写我的名字!
那一刻,我决定我想成为一名开发人员,这样我就可以让计算机做事情。
成为一名开发人员后,我想重温我的童年,并决定建立一个 ZX 频谱模拟器。在很多方面,当我遇到新技术的时候,模拟器已经成为我的测试项目,而不是简单的 Hello World 。我已经在 Gadgeteer、Xbox One 甚至全息镜头(仅举几个例子)上运行过它。
但是有可能在 Blazor 中运行我的模拟器吗?
我只花了几个小时就通过利用我已经构建的来让模拟器与 Blazor WebAssembly 一起工作.NET 标准动态链接库;我只需要编写特定于这个实现的代码,比如键盘和图形。这是 Blazor(服务器和 WebAssembly)如此强大的原因之一:它可以运行已经制作好的库。您不仅可以利用您的 C#知识,还可以利用大型生态系统和.NET 社区。
你可以在这里找到模拟器:https://zxspectrum.azurewebsites.net/.这是我最喜欢的工作项目之一,因为我一直在寻找优化和改进模拟器的方法。
构建这种类型的网络应用过去只能通过 JavaScript 实现。现在,我们知道可以使用 Blazor WebAssembly 和 Blazor Server,但是这些新选项中哪一个是最好的呢?
Blazor WebAssembly 对 Blazor 服务器
我们应该选择哪一个?答案是,一如既往,这取决于。你已经看到了两者的优缺点。
如果你有一个当前站点,你想移植到 Blazor,我会选择服务器端;一旦您移植了它,您就可以对是否也要使用 WebAssembly 做出新的决定。
如果您的站点运行在移动浏览器或其他不可靠的互联网连接上,您可能需要考虑使用 Blazor WebAssembly 进行离线(PWA)场景,因为 Blazor Server 需要持续的连接。
WebAssembly 的启动时间有点慢,但是有一些方法可以将两种宿主模型结合起来,这样就可以两全其美了。我们将在 第 9 章共享代码和资源中介绍这一点。
在这个问题上没有灵丹妙药,但是仔细阅读优点和缺点,看看它们如何影响您的项目和用例。
我们可以在服务器端和客户端运行 Blazor,但是桌面和移动应用呢?通过使用网络窗口和移动浏览器绑定,也有解决方案。
网络窗口
有一项实验性技术叫做网络窗口,这是史蒂夫·桑德森的一个开源项目。它使我们能够使用 Blazor 创建 Windows 应用。
WebWindow 不在本书的讨论范围之内,但我还是想提一下它,因为它展示了这项技术的真正强大,以及 Blazor 的无限可能性。
你可以在这里找到并阅读更多关于这个项目的信息:https://github.com/SteveSandersonMS/WebWindow。
Blazor 移动绑定
另一个不在本书范围内但仍值得一提的项目例子是Blazor 移动绑定。这是一个利用 Blazor 为 iOS 和 Android 创建移动应用的项目。
Blazor 移动绑定像 Blazor 一样使用 Razor 语法;但是,组件完全不同。
虽然微软同时支持 Blazor 和 Blazor Mobile Bindings,但我们实际上无法在不同的 web 版本(WebAssembly、Server 或 WebWindow)之间共享代码。
你可以在这里找到并阅读更多关于这个项目的信息:https://docs.microsoft.com/en-us/mobile-blazor-bindings/。
如你所见,你可以用 Blazor 做很多事情,而这只是开始。
总结
在这一章中,向您概述了可以与 Blazor 一起使用的不同技术,例如服务器端、客户端(WebAssembly)、桌面和移动。这个概述应该有助于您对下一个项目选择什么技术做出明智的决定。
然后我们讨论了 Blazor 是如何创建的,以及它的底层技术,比如 SignalR 和 WebAssembly。您还了解了渲染树以及 DOM 是如何更新的,从而让您了解 Blazor 在幕后是如何工作的。
在接下来的章节中,我将向您介绍各种场景,让您具备处理从升级旧的/现有的站点、创建新的服务器端站点到创建新的 WebAssembly 站点等一切事情的知识。
在下一章中,我们将通过配置我们的开发环境以及创建和检查我们的第一个 Blazor 应用来弄脏我们的手。
进一步阅读
作为一名. NET 开发人员,您可能会对 Uno 平台(https://platform.uno/)感兴趣,它使得在 XAML 创建 UI 并将其部署到许多不同的平台(包括 WebAssembly)成为可能。
如果你想看看 ZX 频谱模拟器是如何搭建的,可以在这里下载源代码:https://github.com/EngstromJimmy/ZXSpectrum。
二、打造你的第一款 Blazor 应用
在本章中,我们将设置我们的开发环境,以便我们可以开始开发 Blazor 应用。我们将创建我们的第一个 Blazor 应用,并浏览项目结构,突出 Blazor Server 和 Blazor WebAssembly 项目之间的区别。
到本章结束时,您将拥有一个工作开发环境,并且已经创建了一个 Blazor Server 应用和一个 Blazor WebAssembly 应用。
在本章中,我们将介绍以下内容:
- 设置您的开发环境
- 创建我们的第一个 Blazor 应用
- 使用命令行
- 弄清楚项目结构
技术要求
我们将创建一个新项目(博客引擎),并将在整本书中继续致力于该项目。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 02找到本章最终结果的源代码。
设置您的开发环境
在本书中,的重点将是 Windows 开发,任何截图都将来自 Visual Studio(除非另有说明)。但是自从.NET 5 是跨平台的,我们将介绍如何在 Windows、macOS 和 Linux 上设置您的开发环境。
所有平台的进入链接可以在https://visualstudio.microsoft.com/找到。
从网页,我们可以下载 Visual Studio,Visual Studio Code,或者 Visual Studio for Mac。
窗户
在 Windows 上,我们有许多不同的选项来开发 Blazor 应用。Visual Studio 2019 是我们可以使用的最强大的工具。
有三个不同的版本,如下所示:
- 社区 2019
- 专业 2019
- 企业 2019
简而言之,社区版是免费的,而其他版本需要花钱。社区版确实有一些局限性,我们可以在这里比较不同的版本:https://visualstudio.microsoft.com/vs/compare/。
对于这本书,我们可以使用这些版本中的任何一个。采取以下步骤:
- 从https://visualstudio.microsoft.com/vs/下载 Visual Studio 2019。选择适合你的版本。
- 安装 Visual Studio,安装时一定要选择ASP.NET 和 web 开发,如图图 2.1 :

图 2.1–在 Windows 上安装 Visual Studio 2019
我们也可以使用 Visual Studio Code 在 Windows 上开发 Blazor,但是对于 Windows 的安装过程就不说了。
柔软
在 macOS 上,我们也有一些选项。Visual Studio for Mac 是我们能使用的最强大的 IDE。
从 https://visualstudio.microsoft.com/vs/mac/下载 Visual Studio for Mac,如下所示:
- 点击下载 Visual Studio for Mac 按钮。
- 打开下载的文件。
- 确保选择。净芯,如图图 2.2 :

图 2.2–用于苹果电脑安装屏幕的 Visual Studio
既然 Visual Studio Code 是一个跨平台软件,我们在这里也可以使用。
Linux(或 macOS 或 Windows)
Visual Studio Code 是跨平台的,也就是说我们可以在 Linux、macOS 或者 Windows 上使用。
不同的版本在https://code.visualstudio.com/Download有售。
安装后,我们还需要添加两个扩展:
- 打开 Visual Studio Code,按Ctrl+Shift+X。
- 搜索
C# for Visual Studio Code (powered by OmniSharp)点击安装。 - 搜索
JavaScript Debugger (Nightly)点击安装。
要创建项目,我们可以使用.NET CLI,我们将在本书中从头到尾回顾它,但我们不会深入探讨.NET 命令行界面。
现在我们已经设置好了一切,让我们创建我们的第一个应用。
创建我们的第一个 Blazor 应用
纵观全书,我们将创建一个博客引擎。不会有很多业务逻辑需要你去学习;这个应用很容易理解,但会基于构建 Blazor 应用时将面临的许多技术和领域。
该项目将允许访问者阅读博客文章并进行搜索。它还将有一个管理网站,在那里你可以写博客文章,这将是密码保护的。
我们将为 Blazor Server 和 Blazor WebAssembly 制作同一个应用,我将向您展示您需要为每个平台做的不同的步骤。
重要说明
本指南将从现在开始使用 Visual Studio 2019,但其他平台也有类似的项目创建方式。
创建 Blazor 服务器应用
首先,我们将创建一个 Blazor Server 应用并使用它:
-
Start Visual Studio 2019 and you will see the following screen:
![Figure 2.3 – Visual Studio startup screen]()
图 2.3–Visual Studio 启动屏幕
-
按新建项目,在搜索栏中输入
blazor。 -
Select Blazor App from the search results and press Next:
![Figure 2.4 – The Visual Studio Create a new project screen]()
图 2.4–Visual Studio 创建新项目屏幕
-
Now name the project (this is the hardest part of any project but fear not, I have done that already!). Name the application
MyBlogServerSide, change the solution name toMyBlog, and press Create:![Figure 2.5 – The Visual Studio Configure your new project screen]()
图 2.5–Visual Studio 配置您的新项目屏幕
-
Next, choose what kind of Blazor app we should create. Select .NET 5.0 (Current) from the drop-down menu and press Create:
![Figure 2.6 – Visual Studio screen for creating a new Blazor app]()
图 2.6–用于创建新 Blazor 应用的 Visual Studio 屏幕
-
现在按 Ctrl + F5 运行 app(我们也可以在调试 | 启动T9】下找到,无需调试)。
恭喜你!您刚刚创建了第一个 Blazor 服务器应用。该站点应该类似于图 2.7 中的:

图 2.7–一个新的 Blazor 服务器端应用
稍微浏览一下站点,导航到计数器和获取数据来感受一下加载时间,看看示例应用做了什么。
示例应用有一些示例数据准备好供我们测试。
这是一个 Blazor 服务器项目,这意味着对于每一个触发器(例如,按下一个按钮),一个命令将通过 SignalR 发送到服务器。服务器将重新提交组件,并将更改发送回客户端,并更新用户界面。
在浏览器中按 F12 (进入开发者工具),切换到网络选项卡,然后重新加载页面( F5 )。您将看到下载到浏览器中的所有文件。
在图 2.8 中,可以看到一些下载的文件:

图 2.8–微软边缘中的网络选项卡
浏览器下载页面,一些 CSS,然后blazor.server.js,负责设置 SignalR 连接回服务器。然后它调用negotiate端点(建立连接)。
对_blazor?id=(后面跟着一串字母)的调用是 WebSocket 调用,这是客户端和服务器通过开放的连接进行通信。
如果导航到计数器页面,按下点击我按钮,您会注意到页面不会重新加载。触发器(点击事件)通过信号发送到服务器,页面在服务器上重新呈现,并与呈现树进行比较,只有实际的更改通过网络套接字推回。
对于按钮点击,正在进行三次呼叫:
- 页面触发事件(例如,按钮点击)。
- 服务器响应这些更改。
- 该页面随后发回响应,确认文档对象模型 ( DOM )已经更新。
总共有 490 个字节来回发送一个按钮点击。
现在,我们已经创建了一个解决方案和一个 Blazor Server 项目,并对其进行了测试。接下来,我们将向该解决方案添加一个 Blazor WebAssembly 应用。
创建网络组装应用
现在是时候看一下一个 WebAssembly 应用了。我们将创建一个新的 Blazor WebAssembly 应用,并将其添加到与我们刚刚创建的 Blazor Server 应用相同的解决方案中:
-
右键点击我的博客解决方案,选择添加 | 新项目。
-
Search for
Blazor, select Blazor WebAssembly App in the search results, and press Next:![Figure 2.9 – The Visual Studio Add a new project screen]()
图 2.9–Visual Studio 添加新项目屏幕
-
Name the app
MyBlogWebAssembly. Leave the location as is (Visual Studio will put it in the right folder by default) and press Create:![Figure 2.10 – The Visual Studio Configure your new project screen]()
图 2.10–Visual Studio 配置您的新项目屏幕
-
在下一屏,选择。下拉列表中的. NET 5.0(当前)。
-
In this dialog box, two new choices appear that were not available in the Blazor Server template. The first option is ASP.NET Core hosted, which will create an ASP.NET backend project and will host the WebAssembly app, which is good if you want to host web APIs for your app to access; you should check this box.
第二个选项是渐进式网络应用,它将创建一个
manifest.json文件和一个service-worker.js文件,使您的应用作为渐进式网络应用 ( PWA )可用。对于此项目,不选中它,然后按创建:![Figure 2.11 – Visual Studio screen for creating a new Blazor app]()
图 2.11–用于创建新 Blazor 应用的 Visual Studio 屏幕
-
Right-click on the MyBlogWebAssembly.Server project and select Set as Startup Project.
注意:
这个项目名称中也有服务器可能会比较混乱。
由于我们在创建项目时选择了ASP.NET Core 托管,所以我们在我的博客浏览器中托管客户端(网络组装)的后端。服务器与 Blazor 服务器无关。
请记住,如果你想运行网络组装应用,你应该运行我的博客组装。服务器项目;这样,我们知道后端 ASP.NET Core 项目也将运行。
-
按 Ctrl + F5 运行应用(无需调试即可启动)。
恭喜你!您刚刚创建了第一个 Blazor WebAssembly 应用,如图图 2.12 :

图 2.12–一个新的 Blazor 网络组装应用
点击计数器和获取数据链接,浏览网站。该应用的行为应该与 Blazor 服务器版本相同。
在浏览器中按 F12 (进入开发者工具),切换到网络选项卡,重新加载页面(F5);您将看到下载到浏览器中的所有文件。
在图 2.13 中,可以看到一些下载的文件:

图 2.13–微软边缘中的网络选项卡
在这种情况下,当页面被下载时,它将触发blazor.webassembly.js文件的下载。然后,blazor.boot.json被下载。图 2.14 展示了部分blazor.boot.json的例子:

图 2.14–blazor . boot . JSON 文件的一部分
blazor.boot.json包含的最重要的东西是入口程序集,这是浏览器应该开始执行的 DLL 的名称。它还包含应用运行所需的所有框架 dll。现在,我们的应用知道启动需要什么了。
然后 JavaScript 会下载dotnet.5.0.*.js,它会下载blazor.boot.json中提到的所有资源:这是你编译成. NET Standard DLL 的混合代码,微软.NET 框架代码,以及您可能使用的任何社区或第三方 dll。然后,JavaScript 下载dotnet.wasm,将 Mono 运行时编译到 WebAssembly,现在将开始启动你的应用。
如果你仔细观察,当你重新加载你的页面时,你可能会看到一些文字说正在加载。在加载出现和页面完成加载之间,JSON 文件、JavaScript、网络组件和动态链接库被下载,一切都启动了。根据微软 Edge 的说法,在调试模式和未优化的代码下运行需要 1.8 秒。
现在我们已经有了项目的基础,包括一个 Blazor WebAssembly 版本和一个 Blazor Server 版本。在本书中,我们将使用 Visual Studio,但是还有其他方法来运行您的 Blazor 站点,例如使用命令行。命令行是一个超级强大的工具,在下一节中,我们将使用命令行运行我们的 Blazor 应用。
使用命令行
和.NET 5,你得到一个超级强大的工具叫做dotnet.exe。使用过.NET Core 之前就已经会熟悉这个工具了,但是用.NET 5,它不再是专为.NET Core 开发人员。
它可以做很多 Visual Studio 可以做的事情,例如,创建项目、添加和创建 NuGet 包等等。在下一个例子中,我们将创建一个 Blazor 服务器项目。
使用命令行创建 Blazor 服务器项目
下面的步骤只是为了演示使用命令行的威力。我们不会在本书的后面使用这个项目,所以如果你不想尝试,请跳过这一部分。要创建新的 Blazor 服务器项目,可以使用以下命令:
dotnet new blazorserver -o BlazorServerSideApp
这里,dotnet是命令,要创建一个新项目,可以使用new参数。
blazorserver是模板的名称,-o是输出文件夹(在这种情况下,项目将在名为BlazorServerSideApp的子文件夹中创建)。
您可以使用Dotnet命令运行 Blazor 应用。启动 PowerShell 并导航至MyBlogServerSide文件夹,然后键入以下命令:
Dotnet run
它将编译代码并启动运行您的应用的 web 服务器:
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path:
D:\Source\B16009\Ch2\MyBlog\MyBlogServerSide
如果你启动一个网络浏览器并导航到http://localhost:5000,你将会看到你的网站。
请注意.NET 命令行界面
这个想法是你应该能够从命令行做任何事情。如果您更喜欢使用命令行,您应该检查.NET 命令行界面;你可以阅读更多关于。这里是:https://docs.microsoft.com/en-us/dotnet/core/tools/。
让我们回到 Blazor 模板,它已经为我们添加了很多文件。在下一节中,我们将看看 Visual Studio 为我们生成了什么。
搞清楚项目结构
现在是时候看看不同的文件以及它们在不同项目中的不同之处了。看看我们刚刚创建的两个项目中的代码(在创建我们的第一个 Blazor 应用部分)当我们浏览它们的时候。
程序. cs
Program.cs是被调用的第一个类。它也不同于 Blazor 服务器和 Blazor 网络组件。
网络组装程序
在MyBlogWebAssembly.Client项目中,有一个名为Program.cs的文件,它是这样的:
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault (args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient {
BaseAddress = new Uri (builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
}
Main方法是第一个被调用的方法;它将添加app作为根组件,整个单页应用站点将在App组件内部呈现(我们将在本章后面回到该组件)。
它添加HttpClient作为范围依赖。在 第三章介绍实体框架核心中,我们将深入挖掘依赖注入,但目前来说,这是一种通过注入对象(依赖)来抽象对象和类型的创建的方法,因此您不会在页面内部创建对象。相反,对象被传递到页面/类中,这将使测试更容易,并且类没有我们不知道的任何依赖关系。
WebAssembly 版本是在浏览器中运行的,因此它获取数据的唯一方式是通过外部调用(例如,对服务器的调用);因此,我们需要能够访问HttpClient。WebAssembly 不允许进行任何直接调用来下载数据,因此HttpClient是 WebAssembly 的一个特殊实现,它将进行 JavaScript interop 调用来下载数据。
正如我之前提到的,WebAssembly 是在沙箱中运行的,为了能够在这个沙箱之外进行通信,它需要通过适当的 JavaScript/浏览器 API。
Blazor 服务器程序
Blazor 服务器项目看起来有点不同(但是做的几乎一样)。在MyBlogServerSide项目中,Program.cs文件如下所示:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
就像 WebAssembly 一样,Main方法是第一个被调用的东西。它将调用CreateDefaultBuilder方法,将把一切交给Startup类。您会注意到我们在这里没有注册任何服务;而是在Startup班完成。
启动
启动文件负责挂接所有服务,配置 app 它只在 Blazor Server 项目中可用(在 Blazor WebAssembly 中不可用)。在启动文件中,有几个方法,我们将逐一介绍。
在MyBlogServerSide项目中,我们有Startup.cs文件:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
Startup方法是一个接受IConfiguration对象的构造函数。使用Configuration属性,我们可以访问我们可能需要的任何设置。
下一个方法是ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
}
ConfigureServices方法是我们在应用中添加所有需要的依赖项的地方。在这种情况下,我们添加RazorPages,这是运行 Blazor 的页面(这些是.cshtml文件)。然后我们添加ServerSideBlazor,这将使我们能够访问运行 Blazor Server 所需的所有对象。然后我们添加WeatherForcastService,当您导航到预测页面时使用。
接下来我们有Configure方法,它配置我们需要的一切:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
UseDeveloperExceptionPage将确保当我们在开发环境中运行时,我们的应用将显示更详细的开发人员异常页面,这使得调试应用更加容易。如果我们没有在开发中运行,它将重定向到一个异常处理程序,并显示一个更友好的错误消息。
它还配置 HTTP 严格传输安全 ( HSTS ,强制你的应用使用 HTTPS,并会确保你的用户不使用任何不可信的资源或证书。我们还确保网站重定向到 HTTPS,以确保网站安全。
UseStaticFiles允许下载静态文件,如 CSS 或图像。
不同的Use*方法将请求委托添加到请求管道或中间件管道。每个请求委托(DeveloperException、httpRedirection、StaticFiles等)从上到下依次调用。
这就是为什么异常处理程序是第一个被添加的。
如果随后的任何请求委托中出现异常,异常处理程序仍然能够处理它(因为请求通过管道返回),如图 2.15 所示:

图 2.15–请求中间件管道
例如,如果这些请求代理中的任何一个在静态文件的情况下处理请求,则不需要涉及路由,并且剩余的请求代理将不会被调用。如果请求是针对静态文件的,则不需要涉及路由。在某些情况下,以正确的顺序添加委托的请求非常重要。
注意:
如果你想进一步挖掘,这里有更多的信息:https://docs . Microsoft . com/en-us/aspnet/core/基本面/中间件/?view=aspnetcore-5.0 。
在Configure方法的最后,我们连接路由并添加端点。我们为 Blazor SignalR hub 创建一个端点,如果我们找不到任何要返回的东西,我们确保我们将调用_host文件,该文件将为应用处理路由。当_host触发后,应用的首页将被加载。
索引/_ 主机
接下来发生的是Index或_host文件运行。它包含加载必要的 JavaScript 的信息。
_ 主机(Blazor 服务器)
Blazor 尔服务器项目有一个位于pages文件夹中的_Host.cshtml文件。这是一个 Razor 页面,它与 Razor 组件不是一回事:
- 剃刀页面是创建视图或页面的一种方式。它可以使用 Razor 语法,但不能用作组件(组件可以用作页面的一部分,也可以在另一个组件内部使用)。
- 一个剃刀组件是一种构建可重用视图(称为组件)的方法,您可以在整个应用中使用。您可以构建一个网格组件(例如,呈现表格的组件)并在您的应用中使用它,或者将其打包为库供其他人使用。但是,通过向组件添加
@页面指令,组件可以用作页面,并且它可以被称为页面(稍后将详细介绍)。
对于大多数 Blazor 应用,你应该只有一个.cshtml页面;剩下的应该是 Razor 组件。
在页面顶部,您会发现一些@指令(如page、namespace、using、addTagHelper):
@page "/"
@namespace BlazorTestServerSide.Pages
@using MyBlogServerSide
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
这个文件有几个方面值得注意。@指令确保设置页面的网址,添加名称空间,添加标签助手,并且我们没有使用Layout页面。我们将在 第 4 章了解基本 Blazor 组件中介绍指令。
我们不需要这个页面布局的原因是布局将被加载到应用组件中。
然后我们有一些标准的 HTML 文档类型、元标签、标题和样式。Blazor 特有的唯一东西是base标签:
<base href="~/" />
它确保你的页面会找到 Blazor 信号中枢。如果您没有base标签,一旦您导航到文件夹中的页面,您的网站将会崩溃,因为相对网址不再找到 Blazor 信号中心。
接下来,我们有body标签,它包含应用组件:
<component type="typeof(App)" render-mode="ServerPrerendered" />
这是整个应用将被呈现的地方。App组件处理这一点。这也是您使用组件标签助手将 Blazor 组件添加到现有非 Blazor 应用中的方式。
它将渲染一个名为App的组件。有五种不同的渲染模式:
- 第一个是默认的
ServerPrerendered模式,当页面第一次下载时,它会在服务器上呈现所有内容,并将其作为内容的一部分进行传递。然后,它将连接 Blazor 信号中心,并确保您的更改将被推送到服务器和从服务器;但是,服务器将进行另一次渲染,并将这些更改推送到 SignalR 上。通常,您不会注意到任何事情,但是如果您在服务器上使用某些事件,例如,它们可能会被触发两次并进行不必要的数据库调用。 - 第二个选项是
Server,它将发送整个页面并为组件添加占位符。然后,它连接到 SignalR,并让服务器在它完成时(例如,当它从数据库中检索到数据时)发送更改。 - 第三个选项是
Static,会渲染组件然后断开,这意味着它不会监听事件,也不会再更新组件。对于静态数据来说,这可能是一个不错的选择。 - 第四个选项是
WebAssembly,它将为 WebAssembly 应用呈现一个标记,但不会从组件中输出任何东西。 - 第五个选项是
WebAssemblyPrerendered,它会将组件渲染成静态 HTML,然后将 WebAssembly 应用引导到那个空间。
这会让应用感觉加载速度更快。
注意:
要深入了解选项 3 至 5,请点击以下链接:https://docs . Microsoft . com/en-us/aspnet/core/blazor/components/prerendering-and-integration。
我们不会深入这些不同的选择。
ServerPrerendered从技术上来说是让你的页面在屏幕上出现的最快方式;如果你有一个快速加载的页面,那么这是一个很好的选择。如果你希望你的页面有一个感知的快速加载时间,快速显示你的内容,然后在服务器完成从数据库获取数据时加载数据,那么Server是一个更好的选择。
我更喜欢Server选项,因为网站应该感觉很快。切换到Server是我创建新 Blazor 网站时改变的第一件事;我更希望数据在几毫秒后弹出,因为页面会感觉加载得更快。
在_host文件中,有一小部分 UI 会显示是否有错误信息:
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
我建议保留这个错误 UI(或者它的变体),因为 JavaScript 涉及到更新 UI。在某些情况下,您的页面可能会崩溃,JavaScript 将停止运行,SignalR 连接将失败。如果发生这种情况,您将在 JavaScript 控制台中看到一条漂亮的错误消息。但是通过弹出错误用户界面,您将知道您需要检查控制台。
我们将在_host页面上讨论的最后一件事也是所有魔法发生的地方,负责连接一切的 JavaScript:
<script src="_framework/blazor.server.js"></script>
该脚本将创建一个到服务器的信号连接,并负责从服务器更新 DOM 并将触发器发送回服务器。
索引(网络组件)
WebAssembly 项目看起来相当大同小异。
在MyBlogWebAssembly.Client项目中,打开wwwroot/index.html文件。这个文件只是 HTML,所以没有像 Blazor Server 版本那样在顶部有指令。
就像 Blazor Server 版本一样,你会发现一个base标签:
<base href="/" />
取而代之的是一个component标签(与 Blazor 服务器一样),你会在这里找到一个div标签,在Program.cs中有一条线将App组件连接到div标签(参见之前的 Program.cs 部分):
<div id="app">Loading...</div>
如果你愿意,你可以用其他东西代替Loading…——这是应用启动时将显示的内容。
错误界面看起来也有点不同。开发和生产之间没有区别,就像我们在 Blazor Server 中一样。这里只有一种显示错误的方法:
<div id="blazor-error-UI">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
最后,我们有一个加载 JavaScript 的script标签。这确保加载了运行 WebAssembly 代码所需的所有代码:
<script src="_framework/blazor.webassembly.js"></script>
就像 Blazor 服务器的脚本如何与后端服务器和 DOM 通信一样,WebAssembly 脚本也在 WebAssembly 之间通信.NET 运行时和 DOM。
此时,应用正在启动,Blazor Server 和 Blazor WebAssembly 之间的差异已经不复存在;从现在开始都是 Razor 组件。将被加载的第一个组件是App组件。
App
对于 Blazor 网络组件和 Blazor 服务器来说,App组件与相同。它包含一个Router组件:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
该文件处理路由,找到要显示的正确组件(基于@page指令)。如果找不到路由,它会显示一条错误消息。在 第八章认证授权中,我们在实施认证的时候会对这个文件进行修改。
App组件还包括默认布局。布局可以被每个组件覆盖,但是通常情况下,你的站点会有一个布局页面。在这种情况下,默认的布局称为MainLayout。
主布局
当作为页面查看时,MainLayout包含所有组件的默认布局。主布局包含几个div标签,一个用于侧边栏,一个用于主要内容:
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
本文档中您唯一需要的是@inherits LayoutComponentBase和@Body;剩下的只是自举。@inherits指令继承自LayoutComponentBase,它包含使用布局的所有代码。@Body是组件将被渲染的地方(当作为页面查看时)。
自举
Bootstrap 是最流行的 CSS 框架之一,用于开发响应性和移动优先的网站。
我们可以在wwwroot\index.html文件中找到对 Bootstrap 的引用。
它是由推特创建并为其服务的。你可以在这里阅读更多关于 Bootstrap 的信息:https://getbootstrap.com/。
在布局的顶部,你可以看到<NavMenu>,这是一个 Razor 组件。它位于Shared文件夹中,看起来像这样:
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">MyBlogServerSide</a>
<button class="navbar-toggler"
@onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href=""
Match="NavLinkMatch.All">
<span class="oi oi-home"
aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass =>
collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
它包含左侧菜单,是标准的引导菜单。它还有三个菜单项和一个汉堡菜单的逻辑(如果在手机上看的话)。这种导航菜单通常是用 JavaScript 完成的,但是这种是用 CSS 和 C#单独完成的。
你会发现另一个组件,NavLink,它内置在框架中。它将呈现一个锚标签,但也将检查当前路线。如果您当前与导航链接在同一路线/网址上,它会自动向标签添加一个名为active的 CSS 类。
我们会遇到几个更内置的组件,这些组件会在路上帮助我们。模板中也有一些页面,但我们将暂时保留它们,并在下一章进入组件时浏览它们。
总结
在本章中,我们启动并运行了开发环境,并为 Blazor WebAssembly 和 Blazor Server 创建了我们的第一个 Blazor 应用。您学习了类、组件和布局的调用顺序,这将使代码更容易理解。我们还讨论了 Blazor 服务器项目和 Blazor 网络组装项目之间的一些区别。
在下一章中,我们将从 Blazor 中休息一下,看看实体框架 Core 5,并建立我们的数据库。如果你已经了解了实体框架,你可以跳到 第四章了解基本 Blazor 组件,在这里我们将遍历组件,深入挖掘模板中的组件,然后创建我们的第一个组件。
三、实体框架核心简介
在本章中,我们将浏览实体框架,并创建一个数据库,我们可以在其中存储我们的博客文章。由于大多数应用以这样或那样的形式使用数据,本章的目标是能够在我们的 Blazor 应用中使用来自我们数据库的数据。我们还将创建一个访问数据的应用编程接口。
到本章结束时,我们将学会如何使用?NET CLI 创建一个新项目,添加新的 NuGet 包,并创建迁移。
在本章中,我们将介绍以下内容:
- 创建数据项目
- 向 Blazor 添加
DbContext
技术要求
确保您已经阅读了前面的章节,或者使用Ch2文件夹作为起点。
在本章中,我们将使用实体框架核心 5 创建一个数据库项目。如果你对使用 Entity Framework Core 没有兴趣,可以跳过这一章,从 GitHub repo 下载Ch3文件夹,重回正轨。我们不打算深入讨论实体框架,但是我们将介绍实体框架核心 5 的一些新特性。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 03找到本章最终结果的源代码。
创建数据项目
为了保存我们的博文,我们将使用实体框架,这是微软的对象关系映射(或 ORM )。它使开发人员能够使用特定于领域的类来处理数据,而不用担心底层数据库(因为表、列和关系是从类中生成的)。
实体框架将类映射到数据库中的表。实体框架有两种使用方式:
- 数据库优先的方法:这是当我们已经有了一个现有的数据库并基于该数据库生成类的时候。
- 代码优先的方法:这个是我们第一次编写类的时候,然后生成数据库。
对于这个项目,我们将使用代码优先的方法。
让我们创建一个新的数据项目,使用命令行来感受一下dotnet命令可以做什么。
创建新项目
存储数据的方式有很多;为了简单起见,我们将在构建博客时使用一个 SQLite 数据库。数据将可以从我们的 Blazor WebAssembly 项目和 Blazor Server 项目中访问,因此我们想要创建一个新项目(不仅仅是将代码放在我们之前创建的一个项目中)。
我们也可以在 Visual Studio 中创建一个项目(老实说,我就是这么做的),但是要了解.NET 命令行界面,让我们从命令行来代替。
要创建新项目,请执行以下步骤:
-
打开 PowerShell 提示符。
-
导航至
MyBlog文件夹。 -
Create a class library (
classlib) by typing the following command:dotnet new classlib -o MyBlog.Datadotnet工具现在应该已经创建了一个名为MyBlog.Data的文件夹。 -
Add the new project to our solution by running the following command:
dotnet sln add MyBlog.Data它将在当前文件夹中寻找任何解决方案。如果出于任何原因,我们已经有了一个解决方案,我们也需要具体说明。
下一步是添加项目所需的 NuGet 包。
添加 NuGet 包
为了能够使用实体框架核心,我们需要在我们的项目中添加几个 NuGet 包:
-
打开 PowerShell,导航至
MyBlog.Data文件夹:cd MyBlog.Data -
使用以下命令将
Microsoft.EntityFrameworkCore.Tools包添加到项目中:dotnet add package Microsoft.EntityFrameworkCore.Tools -
使用以下命令将
Microsoft.EntityFrameworkCore.Sqlite包添加到项目中:dotnet add package Microsoft.EntityFrameworkCore.Sqlite
在这个项目中,我决定使用 SQLite,这样我们就不必安装微软的 SQL Server 或其他数据库引擎。当然,我们可以将这个包更改为我们选择的数据库;不管底层数据库是什么,教程的其余部分都应该是相同的。
值得一提的是,我们可以使用内置在 Visual Studio 中的 SQL Server Express LocalDB。
如果我们使用命令行创建项目(跨平台),包含身份验证的 Blazor 模板将使用 SQLite,如果我们使用 Visual Studio 创建项目,将使用 LocalDB。
在这种情况下,我们希望我们的项目是跨平台的,使用 SQLite。
下一步是创建数据类。
创建数据类
现在我们有了所有需要的包,我们需要为我们的博文创建一个类。为此,我们将回到 Visual Studio:
-
Open the MyBlog solution in Visual Studio (if it is not already open).
我们现在应该有一个名为我的博客的新项目。数据在我们的解决方案中。我们可能会看到一个弹出窗口,询问我们是否要重新加载解决方案;如果是,点击重新加载。
-
右键点击我的博客。数据项目,选择添加 | 新文件夹。命名文件夹
Interfaces。 -
接下来,我们需要创建一个接口,这样我们以后就不必重复代码了。右键点击
Interfaces文件夹,选择添加 | 类。在不同模板列表中,选择界面并命名为IMyBlogItem.cs。 -
Open
IMyBlogItem.csand replace its content with the following code:namespace MyBlog.Data.Interfaces { interface IMyBlogItem { public int Id { get; set; } } }界面只包含一个属性
Id。通过与Id属性有一个公共接口,我们可以编写通用函数来处理对象的保存。我们将把这个接口添加到我们将要创建的所有数据类中;这只是为了当我们从 API 开始时,不需要重复大量的代码。
-
现在我们需要创建三个数据类。右键点击我的博客。数据并选择添加 | 新文件夹。命名文件夹
Models。 -
Right-click on the
Modelsfolder and select Add | Class. Name the classBlogPost.csand press Add:![Figure 3.1 – Visual Studio's Add New Item dialog]()
图 3.1–Visual Studio 的“添加新项目”对话框
-
右键点击
Models文件夹,选择添加 | 类。命名类别Category.cs并按添加。 -
右键点击
Models文件夹,选择添加 | 类。命名类别Tag.cs并按添加。 -
Open
BlogPost.csand replace the content with the following code:using System; using System.Collections.Generic; using MyBlog.Data.Interfaces; namespace MyBlog.Data.Models { public class BlogPost : IMyBlogItem { public int Id { get; set; } public string Title { get; set; } public string Text { get; set; } public DateTime PublishDate { get; set; } public Category Category { get; set; } public ICollection<Tag> Tags { get; set; } } }在这堂课中,我们定义了我们博客文章的内容。我们需要一个
Id来标识博文、标题、一些文本(文章)和发布日期。我们在类中还有一个category属性,属于Category类型。在这种情况下,一篇博文只能有一个类别。一篇博文也可以包含零个或多个标签。我们用ICollection<Tag>定义Tag属性。 -
Open
Category.csand replace the content with the following code:
```cs
using System.Collections.Generic;
using MyBlog.Data.Interfaces;
namespace MyBlog.Data.Models
{
public class Category : IMyBlogItem
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<BlogPost> BlogPosts { get; set; }
}
}
```
`Category`类包含`Id`、`Name`和一组博文。我们可以有许多相同类别的博客文章,因此类别对象可以有许多博客文章与之连接。
- Open
Tag.csand replace the content with the following code:
```cs
using System;
using System.Collections.Generic;
using MyBlog.Data.Interfaces;
namespace MyBlog.Data.Models
{
public class Tag : IMyBlogItem
{
public int Id {get; set; }
public string Name { get; set; }
public ICollection<BlogPost> BlogPosts { get; set; }
}
}
```
`Tag`类包含一个`Id`、`Name`和一组博文。通过在`Tag`类中添加博客帖子的集合和在`BlogPost`类中添加标签的集合,实体框架将理解应该存在多对多的关系,并将自动创建连接两个表(`BlogPosts`和`Tags`)的参考表。
这是使代码优先成为如此优秀的技术的原因之一;作为开发人员,我们可以关注业务对象以及它们之间的关系,而 Entity Framework 可以创建数据库和表之间的关系。
现在我们已经创建了几个我们将要使用的类。自从我们在这里学习 Blazor 以来,我已经将这些类的复杂性降到了最低。
我们还需要创建一个数据库上下文,这是访问数据库中每个类(或表)的一种方式。
创建数据库上下文
数据库上下文是我们将从中访问数据库的类。我们就是这样创造的:
-
右键点击我的博客。数据项目,选择添加 | 类。命名类
MyBlogDBContext.cs。 -
Open the new
MyBlogDBContext.csfile and replace the content with the following code:using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using MyBlog.Data.Models; namespace MyBlog.Data { public class MyBlogDbContext : DbContext { public MyBlogDbContext(DbContextOptions <MyBlogDbContext> context) : base(context) { } public DbSet<BlogPost> BlogPosts { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<Tag> Tags { get; set; } } public class MyBlogDbContextFactory : IDesignTimeDbContextFactory<MyBlogDbContext> { public MyBlogDbContext CreateDbContext (string[] args) { var optionsBuilder = new DbContextOptionsBuilder<MyBlogDbContext>(); optionsBuilder.UseSqlite("Data Source = test.db"); return new MyBlogDbContext (optionsBuilder.Options); } } }这个文件里有两个类:
MyBlogDbContext和MyBlogDbContextFactory。MyBlogDbContext是我们数据库的DbContext,让我们访问数据库的类。第二类MyBlogDbContextFactory用于在我们创建迁移时配置我们的数据库(我们将在下一步中回到迁移),因此它只是在我们运行迁移时运行的代码,而不是在生产中运行的代码。重要说明
通常情况下,我不会在同一个文件中有多个类,但在这种情况下,
MyBlogDbContextFactory类仅在我们创建迁移时使用,并且是配置我们的MyBlogDbContext的代码。
在实体框架核心的早期版本中,我们必须在DbContext中手动指定多对多关系(例如用Tags),或者创建映射对象/表之间关系的类。在实体框架核心 5 中,我们甚至不需要指定这种关系,这一切都是为我们完成的。
由于BlogPost有Tags的集合,Tags有BlogPosts的集合,实体框架会自动创建包含该关系的表。
接下来,我们必须创建迁移。
创建迁移
一个迁移是一段用于建立数据库的代码,包括创建数据库和创建/更新表。我们可以在 Visual Studio 中完成这项工作(或者使用命令行,我认为这更容易):
-
启动 PowerShell 并导航到我们的
MyBlog.Data文件夹。 -
If this is the first time we start PowerShell, we might need to launch it as an administrator and run the following command:
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine这将确保我们可以在 PowerShell 中运行命令。请注意,我们也可以使用 VS 2019 命令提示符来代替 PowerShell。
-
要安装实体框架工具,请运行以下命令:
dotnet tool install --global dotnet-ef -
现在是时候创建我们的迁移了。迁移包含现在的数据库(在我们的例子中是一个空数据库)和我们在模型类和
MyBlogDbContext中所做的更改之间的差异。 -
To create a migration run the following command:
dotnet ef migrations add InitialDatabaseMigration我们也可以使用 Visual Studio 中的包管理器控制台来运行这些命令。我们可以通过查看 | 其他 窗口 | 包管理器控制台进入包管理器控制台。
我们使用
dotnet命令和我们在步骤 2 中安装的实体框架工具来添加一个名为InitialDatabaseMigration的迁移。如果我们回到 Visual Studio,我们会在我的博客中看到这一点。数据项目现在有一个Migrations文件夹,包含两个文件–MyBlogDbContextModelSnapshot.cs,一个以日期开始,一个以_InitialDatabaseMigration.cs结束。这两个文件包含为我们的模型创建数据库和表的生成代码。
现在我们已经创建了我们的数据库模型和数据模型。
下一步是创建一个简单的 API 即使我们在运行 Blazor Server 项目时可以直接访问数据库,我们也会确保在 Blazor 代码和数据库访问之间有一个很小的层。这样我们就可以为 Blazor WebAssembly 和 Blazor 服务器项目重用我们的代码。
创建界面
在本节中,我们将创建一个应用编程接口。由于我们目前正在使用 Blazor Server,我们可以直接访问数据库,因此我们在这里创建的 API 将直接连接到数据库:
-
右键点击
Interfaces文件夹,选择添加 | 类。 -
在不同模板列表中,选择界面并命名为
IMyBlogApi.cs。 -
Open
IMyBlogApi.csand replace its content with the following:using System.Collections.Generic; using System.Threading.Tasks; using MyBlog.Data.Models; namespace MyBlog.Data.Interfaces { public interface IMyBlogApi { Task<int> GetBlogPostCountAsync(); Task<List<BlogPost>> GetBlogPostsAsync(int numberofposts, int startindex); Task<List<Category>> GetCategoriesAsync(); Task<List<Tag>> GetTagsAsync(); Task<BlogPost> GetBlogPostAsync(int id); Task<Category> GetCategoryAsync(int id); Task<Tag> GetTagAsync(int id); Task<BlogPost> SaveBlogPostAsync(BlogPost item); Task<Category> SaveCategoryAsync(Category item); Task<Tag> SaveTagAsync(Tag item); Task DeleteBlogPostAsync(BlogPost item); Task DeleteCategoryAsync(Category item); Task DeleteTagAsync(Tag item); } }该界面包含我们获取、保存和删除博客文章、标签和类别所需的所有方法。
现在我们有了一个 API 的接口,我们需要用它来列出博客文章、标签和类别,以及保存(创建/更新)和删除它们。接下来,让我们实现接口。
实现接口
要实现 Blazor 服务器实现的接口,请执行以下步骤:
-
首先,我们需要创建一个类。右键点击我的博客。数据项目,选择添加 | 类,命名类
MyBlogApiServerSide.cs。 -
Open
MyBlogApiServerSide.csand replace the code with the following:using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Internal; using MyBlog.Data.Interfaces; using MyBlog.Data.Models; namespace MyBlog.Data { public class MyBlogApiServerSide : IMyBlogApi { } }我们从添加我们需要的名称空间开始,然后创建一个实现
ImyBlogApi接口的类。 -
Add the following code to
MyBlogApiServerSide.csinside of the class.IDbContextFactory<MyBlogDbContext> factory; public MyBlogApiServerSide (IDbContextFactory<MyBlogDbContext> factory) { this.factory = factory; }要访问数据,我们将添加我们的
DbContext,但不会直接添加。我们将使用一个DbContextFactory;建议 Blazor 在一个工作单元中使用数据上下文,这意味着我们应该创建数据上下文,然后为我们所做的每一次数据访问处理它。DbContext不是线程安全的(这意味着如果多个线程访问同一个DbContext,我们在一次运行多个查询时会遇到异常等问题)。幸运的是,创建一个新对象的开销很小,并且有一个类DbContextFactory,可以帮助我们实现这一点。我们首先创建一个私有的
IDbContextFactory并使用我们的数据上下文作为通用参数;这个类将帮助我们快速创建数据上下文。在构造器中,我们传入一个IDbContextFactory;这就是依赖注入机制将工厂交付给我们的地方。我们将在 第 4 章了解基本 Blazor 组件中详细讨论依赖注入。
-
Add the following code to our
MyBlogApiServerSide.csjust under the code we just added in the class:public async Task<BlogPost> GetBlogPostAsync(int id) { using var context = factory.CreateDbContext(); return await context.BlogPosts.Include (p=>p.Category).Include(p=>p.Tags). FirstOrDefaultAsync(p => p.Id == id); } public async Task<int> GetBlogPostCountAsync() { using var context = factory.CreateDbContext(); return await context.BlogPosts.CountAsync(); } public async Task<List<BlogPost>> GetBlogPostsAsync(int numberofposts, int startindex) { using var context = factory.CreateDbContext(); return await context.BlogPosts.OrderByDescending (p=>p.PublishDate).Skip(startindex).Take (numberofposts).ToListAsync(); }看一下的
GetBlogPostAsync法。首先是using var context = factory.CreateDbContext();,它使用工厂来创建我们的数据上下文的一个实例。行首的using关键字确保方法一完成就处理掉工厂。我们从数据库中获取一个 blogpost,并使用
.Include(p=>p.Category)和.Include(p=>p.Tags)来检索这些属性的相关数据。我们刚刚实现的所有
get方法看起来都差不多(它们通过Id返回所有项目或特定项目),除了GetBlogPostsAsync,它也有一个开始索引和我们想要获得的帖子数量,这样我们就可以获得一系列的帖子。 -
Add the following code under the code we just added:
public async Task<List<Category>> GetCategoriesAsync() { using var context = factory.CreateDbContext(); return await context.Categories.ToListAsync(); } public async Task<Category> GetCategoryAsync(int id) { using var context = factory.CreateDbContext(); return await context.Categories.Include(p => p.BlogPosts).FirstOrDefaultAsync(c=>c.Id==id); }这是获取类别的代码,其工作方式与获取博客文章相同。在这种情况下,我们没有办法只获得其中的一些(就像我们在博客文章中所做的那样),因为它们的数量可能非常少。
-
Next, we do the same thing for tags. Add the following code just under the code we added in step 5:
public async Task<Tag> GetTagAsync(int id) { using var context = factory.CreateDbContext(); return await context.Tags.Include(p => p.BlogPosts).FirstOrDefaultAsync(c => c.Id == id); } public async Task<List<Tag>> GetTagsAsync() { using var context = factory.CreateDbContext(); return await context.Tags.ToListAsync(); }这些步骤 4 到 6 都是从 API 获取数据的方法。由于我们刚刚添加的所有方法都是从数据库中获取一个或多个项目,所以我不会一一介绍,但我会指出一些重要的部分。
现在是时候调整方法了。
-
Add the following code just under the code we just added in step 6:
private async Task DeleteItem(IMyBlogItem item) { using var context = factory.CreateDbContext(); context.Remove(item); await context.SaveChangesAsync(); }为了避免重复所有三个数据类的删除代码,我们添加了一个助手方法,它将删除我们传递给该方法的任何对象。因为我们所有的数据类都实现了
IMyBlogItem,所以我们可以有一个delete方法来处理所有的删除。 -
We could have done the same in our API, just having one
deletemethod, but there might be a moment where we want to handle deletions differently depending on the type. In theIMyBlogApiwe have differentdeletemethods for each type.在
MyBlogApiServerSide中,在我们刚刚添加的代码下添加以下代码:public async Task DeleteBlogPostAsync(BlogPost item) { await DeleteItem(item); } public async Task DeleteCategoryAsync(Category item) { await DeleteItem(item); } public async Task DeleteTagAsync(Tag item) { await DeleteItem(item); }我们可以看到,这些方法的实现都调用
DeleteItem方法。 -
Add the following code beneath the code we just added:
private async Task<IMyBlogItem> SaveItem(IMyBlogItem item) { using var context = factory.CreateDbContext(); if (item.Id == 0) { context.Add(item); } else { if (item is BlogPost) { var post = item as BlogPost; var currentpost = await context.BlogPosts.Include (p => p.Category).Include(p => p.Tags).FirstOrDefaultAsync (p => p.Id == post.Id); currentpost.PublishDate = post.PublishDate; currentpost.Title = post.Title; currentpost.Text = post.Text; var ids = post.Tags.Select(t => t.Id); currentpost.Tags = context.Tags.Where(t => ids.Contains(t.Id)).ToList(); currentpost.Category = await context.Categories.FirstOrDefaultAsync (c => c.Id == post.Category.Id); await context.SaveChangesAsync(); } else { context.Entry(item).State = EntityState.Modified; } } await context.SaveChangesAsync(); return item; } public async Task<BlogPost> SaveBlogPostAsync(BlogPost item) { return (await SaveItem(item)) as BlogPost; } public async Task<Category> SaveCategoryAsync(Category item) { return (await SaveItem(item)) as Category; } public async Task<Tag> SaveTagAsync(Tag item) { return (await SaveItem(item)) as Tag; }如果
Id是0,我们将item添加到context,使用context.Add方法将项目添加到数据库中。如果Id不是0,我们假设该项目已经在数据库中,因此应该只连接到数据库。由于
BlogPost引用了其他数据库对象,我们也需要加载它们。在编写这个方法时,很明显这可能不是我们场景的最佳解决方案,但是我想展示这种可能性,所以我决定保持这样。当
item被保存后,它将被返回(如果item被创建,可能带有更新的Id)。
我们的数据库和数据模型完成了!
最终数据库中会有个表。我们知道三个表,因为它们对应于我们创建的类:
BlogPost,包含我们的博文Categories,包含我们的类别Tags,包含我们的标签
另外两张表如下:
BlogPostTag,包含Tags和BlogPost的关系__EFMigrationsHistory,包含并跟踪迁移
BlogPostTag表是为我们创建的,因为实体框架核心已经确定了多对多的关系。迁移使用__EFMigrationsHistory表来了解表上已经执行了哪些迁移。
下一步是添加和配置 Blazor 项目以使用数据库。
将数据库上下文添加到 Blazor
使用DbContext,我们将能够从我们的数据库访问数据。我们需要将DbContext添加到我们的 Blazor 项目中,以便能够从 Blazor 访问数据:
-
在解决方案资源管理器的
MyBlogServerSide节点下,找到依赖项。右键单击依赖关系,选择添加项目引用。 -
In the list of projects, check the MyBlog.Data project and click OK:
![Figure 3.2 – Visual Studio Reference manager]()
图 3.2–Visual Studio 参考管理器
现在我们已经准备好了所有的外部物品,包括外部 NuGet 包和对我们的我的博客的引用。资料项目。
-
在 MyBlogServerSide 项目中,打开
Startup.cs文件并添加以下using语句:using Microsoft.EntityFrameworkCore; using MyBlog.Data; using MyBlog.Data.Interfaces; using MyBlogServerSide.Data; -
Add following code to the
ConfigureServicesmethod:services.AddDbContextFactory<MyBlogDbContext>(opt => opt.UseSqlite($"Data Source=../MyBlog.db")); services.AddScoped<IMyBlogApi, MyBlogApiServerSide>();这是我们配置数据库访问的方式。我们添加一个
DbContextFactory服务并发送我们的MyBlogDbContext类,所以当我们请求IDbFactory<MyBlogDbContext>时,它将返回一个能够实例化一个MyBlogDbContext类的DbFactory实例。在 第 4 章了解基本 Blazor 组件中,我们将进一步探讨依赖注入,并将更深入地解释我们刚刚做了什么。我们还为我们的数据库添加了一个配置,在本例中,是一个 SQLite 数据库。然后我们添加
IMyBlogApi作为作用域,所以每当我们请求IMyBlogApi的依赖注入时,它都会返回一个MyBlogApiServerSide实例。 -
Finally, we need to make sure the database gets created and that the migration (the code that sets up the database) runs. In
Startup.cs, edit theConfiguremethod so it starts like this:public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IDbContextFactory<MyBlogDbContext> factory) { factory.CreateDbContext().Database.Migrate();该方法有一个
IDbContextFactory<MyBlogDbContext>参数,该参数将触发依赖注入(我们在上一步中配置的)并传递DbContextFactory<MyBlogDbContext>。然后我们创建一个我们的DbContext的实例,并执行Migrate方法;这将创建数据库(如果它不存在的话)并运行迁移(在这种情况下,创建所有的表和关系)。
现在,我们可以使用我们的应用编程接口来访问我们的 Blazor 服务器项目中的数据库。
总结
在本章中,我们学习了实体框架核心 5。我们使用 SQLite 创建了一个数据库,并使用迁移来创建该数据库。大多数应用使用某种数据,所以知道如何与 Blazor 一起使用数据库非常重要。
我们还创建了一个访问数据库的应用编程接口(当我们在 第 9 章共享代码和资源中查看项目之间的资源共享时,这将变得非常重要)。
在下一章中,我们将了解组件,特别是 Blazor 模板中的内置组件。我们还将使用本章中创建的应用编程接口和数据库创建第一个组件。
四、了解基本 Blazor 组件
在本章中,我们将了解 Blazor 模板附带的组件,以及开始构建我们自己的组件。了解用于构建 Blazor 网站的不同技术将有助于我们开始构建组件。
Blazor 将组件用于大多数事情,因此我们将在整本书中使用从本章中获得的知识。
我们将从理论开始这一章,并以创建一个组件来显示一些博客文章结束,该组件使用我们在上一章中创建的应用编程接口, 第 3 章介绍实体框架核心。
在本章中,我们将涵盖以下主题:
- 探索组件
- 学习 Razor 语法
- 因素
- 编写我们的第一个组件
技术要求
在本章中,我们将开始构建我们的组件,为此,您将需要我们在上一章中创建的代码, 第 3 章引入实体框架核心。如果你已经按照前几章的说明去做了,那么你就可以走了;如果没有,那么一定要克隆/下载 repo。本章的起点可以在ch3文件夹中找到,完成的代码可以在ch4中找到。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 04找到本章最终结果的源代码。
在本章中,我们将使用 Blazor 服务器项目,因此请确保右键单击 MyBlogServerSide 项目,并选择设置为启动项目。
探索组件
在 Blazor 中,组件是一个.razor文件,它可以包含一个小的、独立的功能(代码和标记)或者可以用作页面本身。一个组件也可以承载其他组件。本章将向我们展示组件如何工作以及如何使用它们。
有三种不同的方法我们可以创建一个组件:
- 使用 Razor 语法,代码和 HTML 共享同一个文件
- 将代码隐藏文件与
.razor文件一起使用 - 仅使用代码隐藏文件
我们将考虑不同的选择。我们接下来要浏览的模板都使用第一个选项,.razor文件,其中代码和 HTML 混合在同一个文件中。
模板中的组件如下:
counterFetchData
计数器
counter页面显示一个按钮和一个计数器;如果我们按下按钮,计数器就会增加。我们现在将这一页拆开,以便更容易理解。
页面顶部是@page指令,它使得直接路由到组件成为可能,正如我们在这段代码中看到的:
@page "/counter"
如果我们启动MyBlogServerSide项目并将/counter添加到 URL 的末尾,我们会看到我们可以通过使用组件的路由来直接访问它。我们也可以让路线采用参数,但我们过一会儿会回到这个问题。
接下来,让我们探索一下代码。要向页面添加代码,我们使用@code语句,在该语句中,我们可以添加普通的 C#代码,如图所示:
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
在前面的代码块中,我们有一个私有的currentCount变量,它被设置为0。然后我们有一个叫做IncrementCount()的方法,它将currentCount变量增加1。
我们使用@符号显示当前值。在 Razor 中,@符号表示是时候进行一些编码了:
<p>Current count: @currentCount</p>
正如我们所看到的,Razor 非常聪明,因为它理解代码何时停止,标记何时继续,所以不需要添加额外的东西来从代码过渡到标记(下一节将详细介绍)。
正如我们在前面的例子中所看到的,我们将 HTML 标签与@currentCount混合在一起,Razor 理解了其中的区别。接下来,我们有一个按钮,它是更改值的触发器:
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
这是一个带有引导类的 HTML 按钮(让它看起来更好一点)。@onclick正在将按钮的 onclick 事件绑定到IncrementCount()方法。如果我们使用onclick,没有@,它将引用 JavaScript 事件而不起作用。所以,当我们按下按钮时,它会调用IncrementCount()方法(由图 4.1 中的 1 描绘),该方法递增变量(由 2 描绘),通过改变变量,UI 会自动更新(由 3 描绘),如图图 4.1 :

图 4.1–计数器组件的流量
对于 Blazor 网络组件和 Blazor 服务器来说,counter组件的实现方式是相同的。FetchData组件有两种不同的实现方式,原因很简单,Blazor Server 项目可以直接访问服务器数据,Blazor WebAssembly 需要通过 web API 访问。
我们对我们的应用编程接口使用相同的方法,这样我们就能感受到如何利用依赖注入 ( DI )以及当我们使用 Blazor 服务器时如何直接连接到数据库。
获取数据
下一个组件我们来看看是FetchData组件。它位于Pages/FetchData.razor文件夹中。
对于 Blazor WebAssembly 和 Blazor Server 来说,FetchData组件的主要实现看起来是一样的。这两个版本中,文件的顶部行以及获取数据的方式都有所不同。对于 Blazor 服务器,它看起来像这样:
@page "/fetchdata"
@using MyBlogServerSide.Data
@inject WeatherForecastService ForecastService
它定义了一个路由,添加了一个名称空间,并注入了一个服务。我们可以在MyBlogServerSide项目的Data文件夹中找到服务。
该服务是一个创建一些随机预测数据的类;代码如下所示:
public class WeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public Task<WeatherForecast[]> GetForecastAsync (DateTime startDate)
{
var rng = new Random();
return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}).ToArray());
}
}
正如我们所看到的,它会生成摘要并对温度进行随机化。
在FetchData组件的code部分,我们将找到调用服务的代码:
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
代码将从服务中获取数据,并填充一个名为forecasts的WeatherForecast数组。
在MyBlogWebAssembly.Client项目中,情况看起来有些不同。首先,文件的前几行如下所示:
@page "/fetchdata"
@using MyBlogWebAssembly.Shared
@inject HttpClient Http
该代码使用page指令定义了一个路由,向我们的共享库中添加了一个名称空间,并注入了HttpClient而不是服务。HttpClient用来从服务器获取数据,这是一个比较现实的现实场景。
HttpClient在Program.cs文件中定义,并且具有与MyBlogWebAssembly.Server项目相同的基址,因为服务器项目托管客户端项目。
获取数据如下所示:
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync <WeatherForecast[]>("WeatherForecast");
}
代码将获取数据并填充一个名为forecasts的WeatherForecast数组。但是我们没有从服务中获取数据,而是拨打了"WeatherForecast"的网址。我们可以在MyBlogWebAddembly.Server项目中找到 web API。
控制器(Controllers/WeatherForcastController.cs)看起来像这样(与服务有很多相似之处):
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> logger;
public WeatherForecastController (ILogger<WeatherForecastController> logger)
{
this.logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
看起来和服务差不多,但是是作为网络应用编程接口实现的。由于两个版本中的数据看起来相同,因此获取数据(在两种情况下)将使用天气预报数据填充数组。
在Pages/FetchData.razor中,显示天气数据的代码在 Blazor WebAssembly 和 Blazor Server 中都是这样的:
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</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>
}
正如我们所看到的,通过使用 Razor 语法,我们可以无缝地将代码与 HTML 混合在一起。代码检查是否有任何数据——如果有,那么它将呈现该表;如果没有,将显示加载信息。我们完全控制 HTML,Blazor 不会在生成的 HTML 中添加任何内容。
有一些组件库可以让这个过程变得简单一点,我们将在下一章 第 5 章创建高级 Blazor 组件中了解一下。
现在我们知道了示例模板是如何实现的,是时候深入研究 Razor 语法本身了。
学习 Razor 语法
我喜欢 Razor 语法的一点是它很容易混合代码和 HTML 标签。这一部分将有很多理论来帮助我们了解 Razor 语法。
要从 HTML 过渡到代码(C#),我们使用@符号。有几种方法可以将代码添加到文件中:
- Razor 代码块
- 隐式 Razor 表达式
- 显式剃刀表达式
- 表达式编码
- 指令
Razor 代码块
我们已经看到了一些代码块。一个code块看起来是这样的:
@code {
//your code here
}
如果我们愿意,我们可以跳过code关键字,就像这样:
@{
//your code here
}
在这些大括号中,我们可以像这样混合 HTML 和代码:
@{
void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
RenderName("Steve Sandersson");
RenderName("Daniel Roth");
}
注意RenderName()方法是如何从代码转换到段落标记并返回代码的;这是一个隐含的转变。
如果我们想要输出没有 HTML 标签的文本,我们可以使用text标签来代替段落标签,如下例所示:
<text>Name: <strong>@name</strong></text>
这将呈现与前面代码所示相同的结果,但没有段落标记。不会渲染text标签。
隐式剃刀表达式
当我们在 HTML 标签中添加代码时,隐式 Razor 表达式就是。
我们已经在FetchData例子中看到了这一点:
<td>@forecast.Summary</td>
我们从一个<td>标签开始,然后使用@符号切换到 C#,然后回到 HTML 作为结束标签。我们可以在方法调用中使用await关键字,但是除此之外,隐式 Razor 表达式不能包含任何空格。
我们不能使用隐式表达式来调用通用的方法,因为<>会将解释为 HTML。因此,为了解决这个问题,我们可以使用显式表达式。
明确的剃刀表达式
如果我们想在代码中使用空格,我们可以使用显式 Razor 表达式。只需用@符号后跟括号( )来编写代码。所以,它看起来像这样:@()。
在这个例子中,我们从当前日期减去7天:
<td>@(DateTime.Now - TimeSpan.FromDays(7))</td>
我们还可以使用显式 Razor 表达式来连接文本;例如,我们可以这样连接文本和代码:
<td>Temp@(forecast.TemperatureC)</td>
输出将是<td>Temp42</td>。
使用显式表达式,我们可以使用以下语法轻松调用泛型方法:
<td>@(MyGenericMethod<string>())</td>
Razor 引擎知道我们是否在使用代码。它还确保在输出到浏览器时将字符串编码为 HTML,称为表达式编码。
表达编码
如果我们把 HTML 作为一个字符串,默认会被转义。以这个代码为例:
@("<span>Hello World</span>")
呈现的 HTML 如下所示:
<span>Hello World</span>
要从字符串中输出实际的 HTML(这是我们稍后要做的事情),您可以使用以下语法:
@((MarkupString)"<span>Hello World</span>")
通过使用MarkupString,输出将是 HTML,它将显示 HTML 标记跨度。在某些情况下,一行代码是不够的;然后我们可以使用代码块。
指令
有一堆指令改变了组件被解析的方式或者可以启用功能。这些是跟随@符号的保留关键字。我们将讨论最常见和最有用的。
我们可以使用代码隐藏来编写代码,以便在代码和布局之间获得更多的分离。我发现布局和代码在同一个.razor文件中是非常好的。在本章的后面,我们将看看如何使用代码隐藏,而不是使用 Razor 语法。
在下面的例子中,我们将看看如何使用代码隐藏来做同样的事情。
添加属性
要给我们的页面添加一个属性,我们可以使用attribute指令:
@attribute [Authorize]
如果我们使用代码隐藏文件,我们将使用以下语法:
[Authorize]
添加接口
为了实现一个接口(在本例中为IDisposable,我们将使用以下代码:
@implements IDisposable
然后我们将在@code{}部分实现接口需要的方法。
为了在代码隐藏场景中做同样的事情,我们将在类名之后添加接口,如下例所示:
public class SomeClass : IDisposable {}
继承
要继承另一个类,我们应该使用以下代码:
@inherits TypeNameOfClassToInheritFrom
为了在代码隐藏场景中做到这一点,我们将在类名之后添加我们想要继承的类:
public class SomeClass : TypeNameOfClassToInheritFrom {}
无商标消费品
我们可以将我们的组件定义为通用组件。
泛型让我们可以制作让我们定义数据类型的组件,所以组件可以和任何数据类型一起工作。
为了将组件定义为通用组件,我们添加了@typeparam指令;然后我们可以像这样在组件的代码中使用类型:
@typeparam TItem
@code
{
[Parameter]
public List<TItem> Data { get; set; }
}
在创建可重用组件时,泛型超级强大,我们将在 第 6 章中回到泛型,通过验证构建表单。
更改布局
如果我们想要一个页面的特定布局(不是app.razor文件中指定的默认布局),我们可以使用@layout指令:
@layout AnotherLayoutFile
这样,我们的组件将使用指定的布局文件(这仅适用于具有@page指令的组件)。
设置命名空间
默认情况下,组件的命名空间将是我们项目的默认命名空间的名称,加上文件夹结构。如果我们希望我们的组件在一个特定的名称空间中,我们可以使用以下内容:
@namespace Another.NameSpace
设置路线
我们已经触及了@page指令。如果我们希望使用网址直接访问我们的组件,我们可以使用@page指令:
@page "/theurl"
该网址可以包含参数、子文件夹等,我们将在本章稍后部分讨论这些内容。
添加 using 语句
为了给我们的组件添加一个名称空间,我们可以使用@using指令:
@using System.IO
如果我们在许多组件中使用了名称空间,那么我们可以将它们添加到_Imports.razor文件中。这样,它将在我们创建的所有组件中可用。
现在我们更了解 Razor 语法是如何工作的。别担心,我们会有足够的时间练习的。还有一个指令我在这一部分没有涉及到,那就是inject。我们已经看过几次了,但是为了涵盖所有的基础,我们首先需要了解什么是 DI 以及它是如何工作的,我们将在下一节中看到。
理解依赖注入
DI 是一种软件模式,是实现控制反转 ( IoC )的技术。
IoC 是一个通用术语,意思是我们可以指示类需要一个类实例,而不是让我们的类实例化一个对象。我们可以说我们的类要么想要一个特定的类,要么想要一个特定的接口。类的创建在别处,它将创建什么类取决于 IoC。
说到 DI,它是 IoC 的一种形式,对象(类实例)通过构造函数、参数或服务查找来传递。
在 Blazor 中,我们可以通过提供实例化对象的方法来配置 DI。在 Blazor 中,这是我们应该使用的关键架构模式。我们已经看到了一些对它的引用,例如在Startup.cs中:
services.AddSingleton<WeatherForecastService>();
这里,我们说如果任何类想要WeatherForecastService,应用应该实例化一个WeatherForecastService类型的对象。在这种情况下,我们不使用接口;相反,我们可以创建一个接口,并这样配置它:
services.AddSingleton<IWeatherForecastService ,WeatherForecastService>();
在这种情况下,如果一个类请求IWeatherForecastService的实例,应用将实例化一个WeatherForecastService对象并返回它。我们在上一章 第三章介绍实体框架核心做了这个。我们创建了一个返回MyBlogApiServerSide实例的IMyBlogApi界面;当我们实现 WebAssembly 版本时,DI 将返回另一个类。
使用 DI 有很多好处。我们的依赖关系是松散耦合的,这意味着我们不会实例化类中的另一个类。相反,我们要求一个实例,这使得编写测试以及根据平台改变实现变得更加容易。
因为我们需要将它们传递到类中,所以任何外部依赖都会更加清晰。我们还可以设置在中心位置实例化对象的方式。我们在Startup.cs(针对 Blazor 服务器)和Program.cs(针对 WebAssembly)中配置 DI。
我们可以用不同的方式配置对象的创建,例如:
- 一个
- 审视
- 短暂的
一个
当我们使用 singleton 时,我们网站的所有用户的对象都是一样的。该对象将只创建一次。
要配置单例服务,请使用以下命令:
services.AddSingleton<IWeatherForecastService ,WeatherForecastService>();
当我们想要与我们站点的所有用户共享我们的对象时,我们应该使用 singleton,但是要注意状态是共享的,所以不要存储任何与特定用户或用户偏好相关的数据,因为这会影响所有用户。
审视
当我们使用 scoped 时,将为每个连接创建一个新的对象,因为 Blazor Server 需要一个连接才能工作,所以只要用户有连接,它就是同一个对象。WebAssembly 没有作用域的概念,因为没有建立连接,所以所有代码都在用户的 web 浏览器内部运行。如果我们使用 scoped,它将像 Blazor WebAssembly 的 singleton 一样工作。如果想法是将服务范围扩大到当前用户,建议仍然使用 scoped。
要配置范围服务,请使用以下命令:
services.AddScoped<IWeatherForecastService ,WeatherForecastService>();
如果我们有属于用户的数据,我们应该使用 scoped。我们可以通过使用限定范围的对象来保持用户的状态。更多关于 第十一章管理国家。
短暂的
通过使用 transient,每次我们请求时都会创建一个新对象。
要配置临时服务,请使用以下命令:
services.AddTransient<IWeatherForecastService ,WeatherForecastService>();
如果我们不需要保持任何状态,也不介意每次请求创建的对象,那么我们应该使用 transient。
现在我们知道如何配置服务,我们需要通过注入服务来开始使用它。
注入服务
注入服务有三种方式。
我们已经在FetchData组件代码中看到了第一种方法。我们可以在 Razor 文件中使用@inject指令:
@inject WeatherForecastService ForecastService
这将确保我们可以访问组件中的WeatherForecastService。
第二种方法是通过添加Inject属性来创建属性,如果我们使用代码隐藏的话:
[Inject]
public WeatherForecastService ForecastService { get; set; }
第三种方法是,如果我们想将一个服务注入到另一个服务中,那么我们需要使用构造函数注入服务:
public class MyService
{
public MyService(WeatherForecastService
weatherForecastService)
{
}
}
现在我们知道了 DI 是如何工作的,以及为什么我们应该使用它。
在本章中,我们已经多次提到代码隐藏。在下一节中,我们将了解如何将代码隐藏与 Razor 文件一起使用,甚至完全跳过 Razor 文件。
弄清楚代码放在哪里
我们已经看到了直接在 Razor 文件中编写代码的例子。我更喜欢这样做,除非代码变得太复杂。
有四种方法可以编写组件:
- 在 Razor 文件中
- 在部分班级
- 继承一个类
- 仅代码
在 Razor 文件中
如果我们正在编写的文件没有那么复杂,那么在编写组件时不必切换文件就好了。正如我们在本章中已经介绍过的,我们可以使用@code指令将代码直接添加到我们的 Razor 文件中。如果我们想将代码移动到代码隐藏文件,那么我们只需要更改指令。对于剩下的代码,我们可以直接进入代码隐藏类。当我开始使用 Blazor 时,在同一个文件中编写代码和标记感觉很奇怪,但我建议您在开发网络应用时尝试一下。
但是许多开发人员更喜欢代码隐藏,将代码与布局分开。为此,我们可以使用分部类。
在部分班级
我们可以创建一个与 Razor 文件同名的分部类,只需添加.cs。
如果你已经下载了 第三章引入实体框架核心的源代码(或者我们可以在 GitHub 上查看代码),你可以看看MyBlogServerSide项目中的FetchDataWithCodeBehind.razor.cs。我已经将所有代码移动到代码隐藏文件中;编译时的结果将与我们将代码保存在 Razor 文件中的结果相同。这只是偏好的问题。
代码隐藏如下所示:
public partial class FetchDataWithCodeBehind
{
[Inject]
public WeatherForecastService ForecastService { get; set; }
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync (DateTime.Now);
}
}
可以看到,我们用的不是@inject,而是[Inject]。除此之外,我刚刚从 Razor 文件中复制了代码。
这不是使用代码隐藏文件的唯一方法;我们也可以从代码隐藏文件中继承。
继承一个类
我们也可以创建一个叫完全不同的东西的类(常见的是把它叫做和 Razor 文件一样的东西,最后加上Model,在我们的 Razor 文件中继承。为了做到这一点,我们需要继承ComponentBase。在部分类的情况下,类已经从ComponentBase继承,因为 Razor 文件是这样做的。
任何字段都需要受保护或公开(非私有),以便页面能够访问它们。如果我们不需要从自己的基类继承,我的建议是使用分部类。
这是代码隐藏类声明的一个片段:
public class FetchDataWithInheritsModel:ComponentBase
我们需要从ComponentBase或者从从ComponentBase继承的类继承。
在 Razor 文件中,我们将使用@inherits指令:
@inherits FetchDataWithInheritsModel
Razor 文件现在将继承我们的代码隐藏类(这是第一个可用的创建代码隐藏类的方法)。
部分和继承选项都是将代码移动到代码隐藏文件的简单方法。但是还有另一个选项可以完全跳过 Razor 文件,只使用代码。
仅代码
Razor 文件将在编译时生成代码。如果我们愿意,我们可以跳过 Razor 步骤,完全用代码编写我们的布局。
这个文件(CounterWithoutRazor.cs)在 GitHub 上有。
计数器示例如下所示:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
namespace MyBlogServerSide.Pages
{
[Route("/CounterWithoutRazor")]
public class CounterWithoutRazor: ComponentBase
{
protected override void BuildRenderTree
(RenderTreeBuilder builder)
{
builder.AddMarkupContent(0, "<h1>Counter</h1>\r\n\r\n");
builder.OpenElement(1, "p");
builder.AddContent(2, "Current count: ");
builder.AddContent(3,currentCount);
builder.CloseElement();
builder.AddMarkupContent(4, "\r\n\r\n");
builder.OpenElement(5, "button");
builder.AddAttribute(6, "class", "btn btn-primary");
builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs> (this,IncrementCount));
builder.AddContent(8, "Click me");
builder.CloseElement();
}
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
}
Razor 文件将首先被转换成看起来与前面代码大致相同的东西,然后代码被编译。它一个接一个地添加元素,最终呈现出 HTML。
代码中的数字是 Blazor 跟踪渲染树中每个元素的方式。有些人更喜欢像前面的代码块一样编写代码,而不是使用 Razor 语法;社区中甚至有人努力简化手动编写BuildRenderTree() 函数的过程。
我的建议是永远不要手动写这个,但是我一直把它保存在书中,因为它展示了 Razor 文件是如何被编译的。现在我们知道了如何使用代码隐藏,让我们来看看 Blazor 的生命周期事件以及它们何时被执行。
生命周期事件
有几个生命周期事件我们可以用来运行我们的代码。在这一节中,我们将仔细研究它们,看看何时应该使用它们。大多数生命周期事件都有两个版本——同步和异步。
初始化和初始化同步
当组件满载时,调用OnInitialized(),然后调用OnInitializedAsync()。这是一个很好的方法来加载任何数据,因为用户界面还没有呈现。如果我们正在做任何长期运行的任务(比如从数据库中获取数据),我们应该把代码放在OnInitializedAsync()方法中。
如果参数改变,这些方法将不再运行(见OnParameterSet()和OnParameterSetAsync())。
OnParametersSet 和 OnParametersSetAsync
当组件被初始化时(在OnInitialized()和OnInitializedAsync()之后),每当我们改变一个参数的值时,调用OnParameterSet()和OnParameterSetAsync()。
例如,如果我们在OnInitialized()方法中加载数据,但它确实使用了一个参数,那么如果参数改变,数据将不会被重新加载,因为OnInitialized()将只运行一次。当然,我们需要在OnParameterSet()或OnParameterSetAsync()中触发数据的重新加载,或者将加载移动到该方法。
OnAfterRender 和 OnAfterRenderAsync
组件完成渲染后,调用OnAfterRender()和OnAfterRenderAsync()方法。当方法被调用时,所有的元素都被渲染,所以如果我们想要/需要调用任何 JavaScript 代码,我们必须从这些方法中调用(如果我们试图从任何其他生命周期事件方法中进行 JavaScript 互操作,我们将会得到一个错误)。我们还可以访问一个firstRender参数,这样我们就可以确保只运行一次初始化代码(只在第一次渲染时)。
应该渲染
当我们的组件被重新渲染时ShouldRender()被称为,如果它返回false,那么组件将不再被渲染。它将总是渲染一次;只有在重新呈现时,方法才会运行。
ShouldRender()没有异步选项。
现在我们知道了不同的生命周期事件何时发生以及以什么顺序发生。一个组件也可以有参数,这样,我们可以重用它们,但是使用不同的数据。
参数
一个参数使得可以向组件发送一个值。要向组件添加参数,我们使用public属性上的[Parameter]属性:
@code {
[Parameter]
public string MyParameter { get; set; }
}
我们也可以使用代码隐藏文件来做同样的事情。我们可以使用@page指令通过在路线中指定来添加参数:
@page "/parameterdemo/{MyParameter}"
在这种情况下,我们必须指定一个与花括号内的名称同名的参数。要在@page指令中设置参数,我们只需转到网址:/parameterdemo/THEVALUE。
在某些情况下,我们希望指定另一种类型而不是字符串(字符串是默认值)。我们可以在参数名称后添加数据类型,如下所示:
@page "/parameterdemo/{MyParameter:int}"
只有当数据类型为整数时,这才会匹配路由。我们也可以使用级联参数传递参数。
级联参数
如果我们想将一个值传递给多个组件,我们可以使用级联参数。
不用[Parameter],我们可以这样用[CascadingParameter]:
[CascadingParameter]
public int MyParameter { get; set; }
To pass a value to the component, we surround it with a CascadingValue component like this:
<CascadingValue Value="MyProperty">
<ComponentWithCascadingParameter/>
</CascadingValue> @code {
public string MyProperty { get; set; } = "Test Value";
}
CascadingValue是我们传递给组件的值,CascadingParameter是接收该值的属性。
如我们所见,我们没有向ComponentWithCascadingParameter组件传递任何参数值;级联值将匹配数据类型相同的参数。如果我们有多个相同类型的参数,我们可以用级联参数在组件中指定参数的名称,如下所示:
[CascadingParameter(Name = "MyCascadingParameter")]
我们也可以对通过CascadingValue的组件这样做,如下所示:
<CascadingValue Value="MyProperty" Name="MyCascadingParameter">
<ComponentWithCascadingParameter/>
</CascadingValue>
如果我们知道值不会改变,我们可以通过使用IsFixed属性来指定:
<CascadingValue Value="MyProperty" Name="MyCascadingParameter" IsFixed="True">
<ComponentWithCascadingParameter/>
</CascadingValue>
这样,Blazor 就不会寻找变化。级联值/参数不能向上更新,只能向下更新。这意味着要更新级联值,我们需要以另一种方式实现它;从组件内部更新它不会改变层次结构中较高的任何组件。
在 第 5 章创建高级 Blazor 组件中,我们将查看单向解决级联值更新问题的事件。
唷!这是一个信息密集型的章节,但是现在我们知道了 Blazor 组件的基础知识。现在是时候建造一个了!
编写我们的第一个组件
我们将构建的第一个组件显示一个网站上的所有博客文章。平心而论,我们还没有写任何博客文章,但我们会暂时解决这个问题,这样我们就可以开始做一些有趣的事情。
在 第三章引入实体框架核心中,我们创建了一个数据库和一个 API(或接口);现在是使用它们的时候了。
我们首先想看到的是一个博文列表,所以我们希望我们的路线是"/"。索引页面已经有了这个路径,所以我们要重用这个页面。
要创建我们的第一个组件,请遵循以下说明:
-
在
MyBlogServerSide项目中,打开Pages/Index.razor。 -
Replace the contents of that file with the following code:
@page "/" @using MyBlog.Data.Interfaces @using MyBlog.Data.Models @inject IMyBlogApi api @code{ protected async Task AddSomePosts() { for (int i = 1; i <= 10; i++) { await api.SaveBlogPostAsync(new BlogPost() { PublishDate = DateTime.Now, Title = $"Blog post {i}", Text = "Text" }); } } }如果我们从顶部开始,我们可以看到一个页面指令。它将确保在路线为
"/"时显示组件。然后,我们有三个@using指令,引入名称空间,这样我们就可以在 Razor 文件中使用它们。然后我们注入我们的应用编程接口(使用数据接口)并将实例命名为api。在code部分,有一种方法是在我们的网站上添加 10 篇博文。接下来,我们应该列出博客文章。 -
Add a variable that holds all our posts. In the
codesection, add the following:protected List<BlogPost> posts = new List<BlogPost>();现在我们需要加载数据。
-
To load posts, add the following in the
codesection:protected override async Task OnInitializedAsync() { posts = await api.GetBlogPostsAsync(10, 0); await base.OnInitializedAsync(); }现在,当页面加载时,帖子也会被加载:
10帖子和页面0(第一页)。 -
Under the
@injectrow, add the following code:<button @onclick="AddSomePosts">Add some fake posts</button> <br /> <br /> <ul> @foreach (var p in posts) { <li>@p.Title</li> } </ul>我们从增加一个按钮开始,这样我们就可以触发
AddSomePosts功能。然后我们添加一个无序列表(ul),在其中,我们循环遍历blogposts并显示标题。 -
现在我们可以通过按下 Ctrl + F5 ( 调试 | 启动而不调试)来运行应用。
-
出现的页面应该只是显示一个添加一些假帖子按钮。点击此按钮。
-
由于页面不会重新加载,
OnInitializedAsync()也不会运行。我们需要重新加载我们的网络浏览器来显示数据。在现实世界的应用中,我们不希望用户必须重新加载浏览器,但是由于这一步只是暂时的,我不想让事情过于复杂。
干得好,我们已经创建了第一个组件!
总结
在这一章中,我们学习了很多关于 Razor 语法的知识,我们将在整本书中用到它。我们了解了 DI、指令和参数,当然,也创建了我们的第一个组件。这些知识将帮助我们理解如何创建组件以及如何重用组件。
在下一章中,我们将了解更高级的组件场景。
五、创建高级 Blazor 组件
在最后一章中,我们学习了创建组件的所有基础知识。在本章中,我们将学习如何将我们的组件提升到一个新的水平。
这一章将重点介绍一些将使我们的组件可重用的特性,这将使我们节省时间,并让我们了解如何使用他人制造的可重用组件。
我们还将了解一些内置组件,当您构建 Blazor 应用时,这些组件将通过添加额外的功能(与使用 HTML 标记相比)来帮助您。
在本章中,我们将涵盖以下主题:
- 探索绑定
- 添加
Actions和EventCallback - 使用
RenderFragment - 探索新的内置组件
技术要求
在本章中,您将开始构建您的组件。为此,您将需要我们在前面的 第 4 章中开发的代码了解基本 Blazor 组件。如果你已经按照前几章的说明去做了,那么你就可以走了。如果没有,请确保克隆/下载报告。本章的起点可以在ch4文件夹中找到,完成的章节可以在ch5中找到。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 05找到本章最终结果的源代码。
探索绑定
使用绑定,您可以连接组件内的变量(以便它自动更新)或通过设置组件属性。
在 Blazor 中,我们可以将值绑定到组件,有两种不同的方法可以做到这一点。
- 单向绑定
- 双向绑定
通过使用绑定,我们可以在组件之间发送信息,并确保我们可以在需要时更新一个值。
单向绑定
单向绑定是在 第四章创建基本 Blazor 组件中已经讲过的东西。让我们再次看看组件,并在本节中继续以它为基础。
在本节中,我们将结合参数和绑定。
Counter.razor的例子是这样的:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
该组件将显示当前计数和增加当前计数的按钮。这是单向绑定,即使按钮可以改变,currentcount的值也只是单向流动。
由于这一部分旨在演示功能和理论,并且不是我们正在构建的已完成项目的一部分,因此您不必编写或运行这些代码。这些组件的源代码可以在 GitHub 上找到。
我们可以给counter组件添加一个参数。然后的代码会是这样的:
@page "/counterwithparameter"
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
[Parameter]
public int IncrementAmount { get; set; } = 1;
[Parameter]
public int CurrentCount { get; set; } = 0;
private void IncrementCount()
{
CurrentCount+=IncrementAmount;
}
}
代码样本有两个参数,一个用于CurrentCount,一个用于IncrementAmount。通过给组件添加参数,我们可以改变它们的行为。这个样本当然有点傻。这样的组件可能不会有任何用处,但是它很好地说明了这个想法。
我们现在可以将该组件添加到另一个组件中。这是我们创建可重用组件并通过改变参数值来改变其行为的方法。
我们这样改变它的行为:
@page "/parentcounter"
<CounterWithParameter IncrementAmount="@incrementamount" CurrentCount="@currentcount"></CounterWithParameter>
The current count is: @currentcount
@code {
int incrementamount = 10;
int currentcount = 0;
}
在这个样本中,我们有两个变量,incrementamount和currentcount,我们将其传递到我们的CounterWithParameter组件中。
如果我们是来运行这个,我们会看到一个以10为增量计数的counter组件。然而,currentcount变量不会被更新,因为它只是单向绑定(单向)。
为了帮助我们做到这一点,我们可以实现双向绑定,这样我们的父组件将被告知任何更改。
双向绑定
双向绑定在两个方向绑定值。我们的counter组件将能够通知我们的父组件任何变化。在下一章 第六章带验证的建筑形式中,我们将更多地讨论双向绑定。
要使我们的CounterWithParameter组件在两个方向绑定,我们需要添加EventCallback。名称必须由参数名称后跟Changed组成。这样,如果值发生变化,Blazor 将确保更新该值。在我们的例子中,我们需要将其命名为CurrentCountChanged。代码如下所示:
[Parameter]
public EventCallback<int> CurrentCountChanged { get; set; }
private void IncrementCount()
{
CurrentCount += IncrementAmount;
CurrentCountChanged.InvokeAsync(CurrentCount);
}
仅仅通过使用这个命名约定,Blazor 就知道CurrentCountChanged是当CurrentCount发生变化时将被触发的事件。
EventCallback不能为空,因此没有理由进行空检查(下一节将详细介绍)。
我们也需要改变我们倾听变化的方式:
<CounterWithParameterAndEvent IncrementAmount="@incrementamount" @bind-CurrentCount="currentcount"/>
我们需要在CurrentCount绑定之前添加@bind-。您也可以使用以下语法来设置事件的名称:
<CounterWithParameterAndEvent IncrementAmount="@incrementamount" @bind-CurrentCount="currentcount" @bind-CurrentCount:event="CurrentCountChanged"/>
通过使用:event,我们可以告诉 Blazor 我们到底要使用什么事件,在本例中是CurrentCountChanged事件。
在下一章 第 6 章用验证构建表单,我们将继续研究带有输入/表单组件的绑定。
当然,我们也可以使用EventCallback创建事件。
添加动作和事件回调
要沟通的变化,我们可以使用EventCallback,如双向绑定部分所示。EventCallback<T>与我们可能在. NET 中使用的稍有不同。EventCallback<T>是一个专为 Blazor 设计的类,能够将事件回调作为组件的参数公开。
英寸 NET 一般来说,可以给一个事件添加多个监听器(多投),但是有了EventCallback<T>,就只能添加一个监听器(单投)。
值得一提的是,你当然可以用你习惯的方式使用事件.NET 也在 Blazor 中。然而,你可能想使用EventCallback<T>,因为使用EventCallback比传统的有很多优点.NET 事件。
.NET 事件使用类,EventCallback使用结构。这意味着在 Blazor 中,我们不需要在调用EventCallback之前执行空检查,因为结构不能为空。
EventCallback为异步,可以等待。调用EventCallback后,Blazor 会在消费组件上自动执行StateHasChanged,确保组件更新(如果需要更新)。
所以,如果需要多个监听器,可以使用Action<T>,否则应该使用EventCallback<T>。
有些事件类型有我们可以访问的事件参数。它们是可选的,所以在大多数情况下,您不需要添加它们。您可以通过在方法中指定它们来添加它们,也可以使用 lambda 表达式,如下所示:
<button @onclick="@((e)=>message=$"x:{e.ClientX} y:{e.ClientY}")">Click me</button>
button会将名为message的变量设置为包含鼠标坐标的字符串。λ有一个参数e,属于MouseArgs类型。但是,您不必指定类型。编译器理解参数的类型。
现在我们已经添加了动作并使用EventCallback来传达变更,我们将在下一节中看到如何执行RenderFragment。
使用渲染片段
为了使我们的组件更加可重用,我们可以为它们提供一段 Razor 语法。在 Blazor 中,可以指定RenderFragment,这是 Razor 语法的一个片段,可以执行和显示。
现在我们已经添加了动作并使用EventCallback来传达变更,我们将在下一节中看到如何执行RenderFragment。
渲染元素有两种类型,RenderFragment和RenderFragment <T>。RenderFragment只是一个没有任何输入参数的 Razor 片段,RenderFragment <T>有一个输入参数,您可以使用context关键字在 Razor 片段代码中使用。我们现在不会深入讨论如何使用这个,但是在本章后面的中,我们将讨论一个使用RenderFragment<T>的组件(Virtualize),并且在下一章 第 6 章使用验证构建表单中,我们将使用RenderFragments<T>实现一个组件。
我们可以让 RenderFragment 成为组件标签内部的默认内容,并给它一个默认值。我们接下来将探讨这一点,并使用这些特性构建一个组件。
网格组件
如果你想更深入地挖掘渲染片段,请查看 Blazm Components,它有一个大量使用RenderFragments<T>的网格组件。在我目前工作的地方,我们使用这个组件,它是使用现实场景开发的。
你可以在 GitHub 这里找到:https://github.com/EngstromJimmy/Blazm.Components。
儿童内容
通过命名渲染片段ChildContent,Blazor 将自动使用提醒标签之间的任何内容作为内容。然而,这仅在使用单个渲染片段时有效;如果您使用多个标签,您还必须指定ChildComponent标签。
默认值
我们可以为RenderFragment提供一个默认值,或者使用@符号在代码中设置它:
@<b>This is a default value</b>;
构建警报组件
为了更好地理解如何使用渲染片段,让我们构建一个警报组件。内置模板正在使用 Bootstrap,因此我们将对该组件进行同样的操作。Bootstrap 有很多易于移植到 Blazor 的组件。当与多个开发人员一起处理大型项目时,构建组件是确保团队中的每个人都以相同的方式编写代码的一种简单方法。
让我们基于 Bootstrap 构建一个简单的警报组件:
-
通过右键单击我的博客服务器项目 | 添加 | 新文件夹并命名文件夹
Components来创建文件夹。 -
右键单击组件 | 添加 | 剃刀组件并命名组件
Alert.razor,创建新的剃刀文件。 -
Replace the content with the following code in the
Alert.razorfile:<div class="alert alert-primary" role="alert"> A simple primary alert—check it out! </div>代码取自 Bootstrap 的网页http://getbootstrap.com,它显示了一个类似如下的警告:
![Figure 5.1 – The default look of a Bootstrap alert component]()
图 5.1–引导警报组件的默认外观
我们可以通过两种方式定制这个
alert组件。我们可以为消息添加一个string参数。然而,由于这是关于渲染片段的部分,我们只是要探索第二个选项,是的,你猜对了,渲染片段。 -
Add a code section with a
RenderFragmentproperty calledChildContentand replace the alert text with the new property:<div class="alert alert-primary" role="alert"> @ChildContent </div> @code{ [Parameter] public RenderFragment ChildContent { get; set; } =@<b>This is a default value</b>; }现在我们有了带有默认值的
RenderFragment,并且我们正在显示div标签之间的片段。我们还想为中的不同方式添加enum,您可以在其中设置警告框的样式。 -
在
code部分,添加包含不同可用样式的【T1:public enum AlertStyle { Primary, Secondary, Success, Danger, Warning, Info, Light, Dark } -
为
enum样式添加参数/属性:[Parameter] public AlertStyle Style { get; set; } -
最后一步是更新
div的class属性。完整的文件如下所示。更改第一行的class属性:<div class="@($"alert alert-{Style.ToString().ToLower()}")" role="alert"> @ChildContent </div> @code{ [Parameter] public RenderFragment ChildContent { get; set; } =@<b>This is a default value</b>; [Parameter] public AlertStyle Style { get; set; } public enum AlertStyle { Primary, Secondary, Success, Danger, Warning, Info, Light, Dark } } -
Right-click on the
Pagesfolder, select Add | Razor component, and name itAlertTest.razor.用以下代码片段替换代码:
@page "/alerttest" @using MyBlogServerSide.Components <Alert Style="Alert.AlertStyle.Danger"> This is a test </Alert> <Alert Style="Alert.AlertStyle.Success"> <ChildContent> This is another test </ChildContent> </Alert> <Alert Style="Alert.AlertStyle.Success"/>页面显示三个报警组件:
第一个有
Danger样式,我们没有指定为This is a test文本设置什么属性,但是按照惯例,它将使用名为ChildContent的属性。在第二个中,我们指定了
ChildContent属性。如果您在组件中使用了更多的渲染片段,您将不得不像这样设置它们,使用全名。在最后一个示例中,我们没有指定任何内容,这将为属性提供我们在组件中指定的默认呈现片段。
-
运行项目并导航至
/AlertTest查看测试页面:

图 5.2–测试页面截图
我们已经完成了我们的第一个可重用组件!
创建可重用的组件是我更喜欢创建我的 Blazor 站点的方式,因为我不必两次编写相同的代码。如果你在一个更大的团队中工作,这一点会变得更加明显。这使得所有开发人员更容易生成相同的代码和最终结果,从而获得更高的代码质量和更少的测试。
和.NET 5 带来了一些我们以前没有的新组件。在下一节中,我们将深入探讨它们是什么以及如何使用它们。
探索新的内置组件
当 Blazor 第一次出现时,有一些事情很难做,在某些情况下,我们需要使用 JavaScript 来解决这个挑战。在这一节中,我们将看一看我们引入的一些新组件.NET 5。
我们将了解以下新组件或功能:
- 设置用户界面的焦点
- 影响 HTML 头部
- 组件虚拟化
设置用户界面的焦点
我的第一篇 Blazor 博客文章是关于如何将焦点放在一个 UI 元素上,但是现在这个被构建到框架中。之前的解决方案涉及 JavaScript 调用来改变用户界面元素的焦点。
通过使用ElementReference,您现在可以将焦点设置在元素上。
让我们构建一个组件来测试这个新特性的行为:
-
右键点击
Pages文件夹,选择新增 | 剃须刀组件,命名为SetFocus.Razor。 -
打开
SetFocus.Razor并添加page指令:@page "/setfocus" -
Add an element reference:
@code { ElementReference textInput; }这正是它听起来的样子,对一个元素的引用。在这种情况下,它是一个输入文本框。
-
Add the textbox and a button:
<input @ref="textInput" /> <button @onclick="() => textInput.FocusAsync()">Set focus</button>通过使用
@ref,您可以指定对一个对象的引用,您可以使用该引用来访问输入框。button onclick方法将执行FocusAsync()方法,并将焦点设置在文本框上。 -
按 F5 运行项目,然后导航至
/setfocus。 -
按下设置焦点按钮,注意文本框如何获得焦点。
这看起来像是一个愚蠢的例子,因为这只是设置了焦点,但这是一个非常有用的特性,并且autofocus HTML 属性对 Blazor 不起作用。
在我的博文中,我有另一种方法。我的目标是在不使用代码的情况下设置元素的焦点。在接下来的章节中, 第 6 章通过验证构建表单,我们将从我的博客文章中实现autofocus功能,但使用新的.NET 特性。
的新版本.NET 5 解决了很多我们以前必须用 JavaScript 编写的东西;设置焦点是一个例子,影响 HTML 头是另一个例子。
影响 HTML 头部
有时候,我们想为我们的页面设置标题或者改变社交网络的元标签。head标签位于index.html(用于 WebAssembly)或_host.cshtml(用于服务器端)中,并且页面的该部分不被重新加载/重新渲染(仅应用组件内的组件被重新渲染)。在 Blazor 的早期版本中,您必须使用 JavaScript 自己编写代码。
酪 NET 有几个新的组件可以用来解决这个问题:
TitleLinkMeta
您只需要将这些组件添加到您的组件中,就可以更改标题、链接或元标签。
这项功能从未进入的最终版本.NET 5。它仍在预览中,但这是一个非常大的问题,所以我想把它保留在书中。
为了使用这些组件,我们将创建一个页面来查看我们的一篇博客文章。我们将使用我们学到的许多技术:
-
首先我们需要添加对微软的引用。组件扩展包。在我的博客服务器节点下的解决方案资源管理器中,右键单击依赖项并选择管理 Nuget 包。
-
搜索微软。选择,点击安装。该软件包仅在预览版可用,因此请确保选中包含预发行的选项。
-
打开
Pages/Index.razor。 -
Change the
foreachloop to look like this:<li><a href="/Post/@p.Id">@p.Title</a></li>我们在标题中添加了一个链接,这样我们就可以查看一篇博客文章。注意我们如何使用
href属性中的@符号来获取帖子的 ID。 -
右键点击
Pages文件夹,选择添加 | 剃须刀组件,命名组件Post.razor。 -
In the
codesection, add a parameter that will hold the ID of the post:[Parameter] public int BlogPostId { get; set; }这将保存来自网址的博客帖子的标识。
-
Add a
pagedirective to get the set, the URL, and the ID:@page "/post/{BlogPostId:int}"page指令会将我们博文的 URL 设置为/post/,后面跟着帖子的 ID。我们还指定了BlogPostId的类型是整数。如果 URL 包含非整数的内容,那么 Blazor 将找不到有问题的页面。 -
We don't have to add a
usingstatement to all our components. Instead, open_imports.razorand add the following namespaces:@using MyBlog.Data.Models; @using MyBlog.Data.Interfaces; @using Microsoft.AspNetCore.Components.Web.Extensions.Head这将确保我们构建的所有组件在默认情况下都会有这些名称空间。
-
Open
Post.razoragain and, just beneath the page directive, inject the API (the namespace is now supplied from_imports.razor):@inject IMyBlogApi api @inject NavigationManager navman我们的应用编程接口现在将被注入到组件中,我们可以检索我们的博客文章。我们还可以访问导航管理器。
-
In the
codesection, add a property for our blog post:
```cs
public BlogPost BlogPost { get; set; }
```
这将包含我们想要在页面上显示的博客文章。
- To load the blog post, add the following code:
```cs
protected async override Task OnParametersSetAsync()
{
BlogPost=await api.GetBlogPostAsync(BlogPostId);
await base.OnParametersSetAsync();
}
```
在这种情况下,我们使用`OnParameterSet()`方法。这只是为了确保我们从数据库中获取数据时设置了参数,以及确保参数更改时内容会更新。
- We also need to show the post and add the necessary meta tags. To do that, add the following code just above the code section:
```cs
@if (BlogPost != null)
{
<Title Value="@BlogPost.Title"></Title>
<Meta property="og:title"
content="@BlogPost.Title" />
<Meta property="og:description" content="@(new
string(BlogPost.Text.Take(100).ToArray()))" />
<Meta property="og:image" content=
"@($"{navman.BaseUri}/pathtoanimage.png")" />
<Meta property="og:url" content="@navman.Uri" />
<Meta name="twitter:card" content="@(new
string(BlogPost.Text.Take(100).ToArray()))" />
<h3>@BlogPost.Title</h3>
@((MarkupString)BlogPost.Text)
}
```
第一次加载页面时,`BlogPost`参数可以为空,所以我们首先需要检查是否应该显示内容。
通过添加`Title`组件,Blazor 将我们网站的标题设置为,在这个例子中,我们博客文章的标题。
根据我在**搜索引擎优化**上收集的信息,我们添加的元标签是脸书和推特最少的。我们没有每个博客帖子的图片,但是如果我们愿意,我们可以有一个全网站的图片(适用于所有博客帖子)。只需将`Pathtoanimage.png`改为图片名称,将图片放入`wwwroot`文件夹即可。
如果博客文章被加载,那么显示一个`H3`标签,标题和下面的文本。你可能还记得 [*第四章*](04.html#_idTextAnchor060)*中的`MarkupString`了解基本 Blazor 组件*。这将从我们的博客文章中输出字符串,而不改变 HTML(不转义 HTML)。
- 通过按下 F5 运行项目,并导航到一篇博客文章以查看标题更改:

图 5.3–博客文章截图
我们的博客开始成形了。我们有一个博客文章列表,我们可以查看一个博客文章;当然,我们还远未完成,但我们正在努力。
组件虚拟化
Virtualize是 Blazor 中的一个新组件,它将确保只渲染当前可见的组件或行。如果你有一个很大的项目列表,显示所有的项目会对记忆有很大的影响。许多第三方组件供应商提供具有相同虚拟化功能的网格组件。在我看来,这个组件是.NET 5 版本。
Virtualize组件将计算屏幕上可以容纳多少个项目(基于窗口的大小和项目的高度)。如果滚动页面,Blazor 会在内容列表前后添加一个div标签,确保滚动条显示的位置正确(即使没有渲染项目)。
Virtualize组件就像一个foreach循环一样工作。
以下是我们目前在index.razor文件中的代码:
<ul>
@foreach (var p in posts)
{
<li><a href="/Post/@p.Id">@p.Title</a></li>
}
</ul>
现在,它将在一个长列表中显示我们数据库中的所有博客文章。诚然,我们现在没有那么多,但有一天我们可能会有很多帖子。
我们可以通过将更改为以下内容来更改代码(暂时不要更改代码)以使用新的Virtualize组件:
<Virtualize Items="posts" Context="p">
<li><a href="/Post/@p.Id">@p.Title</a></li>
</Virtualize>
代替foreach循环,我们使用Virtualize组件,并添加一个渲染片段,显示每个项目应该如何渲染。Virtualize组件使用RenderFragment<T>,默认情况下,它将向渲染片段发送一个类型为T的项目。在Virtualize组件的情况下,对象将是一篇博文(因为条目是博文的List<T>)。我们用名为context的变量访问每个帖子。但是,我们可以使用Virtualize组件上的Context属性来指定另一个名称,所以我们现在使用的不是context,而是p。
Virtualize组件甚至比这更强大,我们将在下一个实现的特性中看到:
-
打开
Pages/Index.razor。 -
删除
OnInitializedAsync方法和protected List<BlogPost> posts = new List<BlogPost>();我们不需要这个。 -
Change the loading of the post to
Virtualize:<ul> <Virtualize ItemsProvider="LoadPosts" Context="p"> <li><a href="/Post/@p.Id">@p.Title</a></li> </Virtualize> </ul>在这种情况下,我们使用
ItemsProvider委托,它将负责从我们的应用编程接口获取帖子。我们传入一个名为
LoadPosts的方法,我们也需要将它添加到文件中。 -
Now, let's add the
LoadPostsmethod by adding the following code:public int totalBlogposts { get; set; } private async ValueTask<ItemsProviderResult<BlogPost>> LoadPosts(ItemsProviderRequest request) { if (totalBlogposts == 0) { totalBlogposts = await api.GetBlogPostCountAsync(); } var numblogposts = Math.Min(request.Count, totalBlogposts - request.StartIndex); var employees = await api.GetBlogPostsAsync(numblogposts, request.StartIndex); return new ItemsProviderResult<BlogPost>(employees, totalBlogposts); }我们添加了一个
totalBlogposts属性,在这个属性中我们存储了我们数据库中当前有多少帖子。LoadPost方法用ItemsProviderResult<Blogpost>返回ValueTask。该方法有ItemsProviderRequest作为参数,其中包含Virtualize组件想要的帖子数量以及想要跳过的帖子数量。如果我们不知道总共有多少帖子,我们需要通过调用
GetBlogPostCountAsync方法从我们的应用编程接口中检索这些信息。然后,我们需要弄清楚我们应该得到多少帖子;要么我们得到我们需要的尽可能多的帖子,要么我们得到所有剩余的帖子(无论哪个值最小)。然后,我们通过调用
GetBlogPostsAsync调用我们的 API 来获取实际的帖子,并返回ItemsProviderResult。
现在我们已经实现了一个Virtualize组件,它将只加载和渲染填充屏幕所需的博客文章数量。
总结
在本章中,我们看了构建组件的更高级的场景。构建组件是 Blazor 的全部。组件还使得沿途进行更改变得容易,因为只有一个点需要实现更改。我们还实现了我们的第一个可重用组件,这将帮助我们在整个团队中保持相同的标准,并减少重复的代码。
我们还在中使用了一些新功能.NET 5 为 Blazor 加载和显示数据。
在下一章中,我们将查看表单和验证,以开始构建博客的管理部分。
六、通过验证构建表单
在这一章中,我们将学习创建表单并验证它们,这是一个很好的机会来构建我们的管理界面,在这里我们可以管理我们的博客文章。我们还将构建多个可重用组件,并了解中的一些新功能.NET 5 for Blazor。
这将会是一个超级有趣的篇章,我们会用到很多我们到现在学到的东西。
在本章中,我们将涵盖以下主题:
- 探索表单元素
- 添加验证
- 自定义验证类属性
- 构建管理界面
技术要求
确保您已经阅读了前面的章节,或者使用Ch5文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 06找到本章最终结果的源代码。
探索形态元素
HTML 中有很多表单元素,我们可以在 Blazor 中全部使用。最终,Blazor 将输出的是 HTML。
Blazor 确实有可以增加功能的组件,所以我们可以也应该尝试使用这些组件来代替 HTML 元素。这将免费为我们提供强大的功能;我们将在本章的稍后部分回到这一点。
Blazor 提供以下组件:
EditFormInputBase<>InputCheckboxInputDate<TValue>InputNumber<TValue>InputSelect<TValue>InputTextInputTextAreaInputRadioInputRadioGroupValidationMessageValidationSummary
让我们从头到尾看一遍。
编辑表格
EditForm将渲染为form标签,但是它有更多的功能。
首先,我们不会有像传统form标签那样的动作或方法;Blazor 会处理所有的事情。
EditForm将创建一个EditContext实例作为级联值,这样您放入EditForm的所有组件都将访问相同的EditContext。EditContext将在编辑过程中跟踪元数据,例如哪些字段已被编辑,并跟踪任何验证消息。
您需要分配一个模型(您想要编辑的类)或一个EditContext实例。
对于大多数用例,分配一个模型是可行的方法,但是对于更高级的场景,你可能希望能够触发EditContext.Validate(),例如,验证所有连接到EditContext的控件。
EditForm有以下事件,您可以使用它们来处理表单提交:
OnValidSubmit在表单中的数据正确验证时被触发(我们稍后将回到验证)。- 如果表单验证不正确,将触发
OnInvalidSubmit。 OnSubmit在表单提交时触发,不管表单是否正确验证。如果您想自己控制验证,请使用OnSubmit。
我们来看一个例子。
考虑一个容纳一个人的类;该类有该人的姓名和年龄,如下所示:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
这个类的EditForm看起来是这样的(现在没有任何其他元素):
<EditForm Model="personmodel" OnValidSubmit="validSubmit">
...
<button type="submit">Submit</button>
</EditForm>
@code {
Person personmodel = new Person();
private Task validSubmit()
{
//Do database stuff
return Task.CompletedTask;
}
}
EditForm指定为模型(本例中为personmodel),我们正在收听对OnValidSubmit事件的报道。
提交按钮是一个普通的 HTML 按钮,不是特定的 Blazor 组件。
输入基地< >
所有的 Blazor 输入类都来自InputBase类。它有一堆我们可以用来做所有input组件的东西;我们将经历最重要的。
InputBase处理AdditionalAttributes,这意味着如果我们在标签中添加任何其他属性,它们将自动转移到输出中。这意味着从这个类派生的组件可以利用任何 HTML 属性,因为它们将是输出的一部分。
InputBase有Value的属性,我们可以绑定到这个属性,还有一个值改变时的事件回调,叫做ValueChanged。
我们还可以更改DisplayName,这样自动验证消息将反映正确的名称,而不是属性的名称,这是默认行为。
并非所有控件都支持DisplayName属性。有些属性只在组件内部使用,稍后我们将回到这些属性。
InputCheckbox
InputCheckbox组件将渲染为<input type="checkbox">。
InputDate < TValue >
InputDate组件将渲染为<input type="date">。我们可以使用DateTime和DateTimeOffset作为InputDate组件的值。
没有可以格式化日期;它将使用 web 浏览器的当前设置。这个行为是设计好的,是 HTML5 规范的一部分。
输入号 < T 值 >
InputNumber组件将渲染为<input type="number">。我们可以使用Int32、Int64、Single、Double和Decimal作为InputNumber组件的值。
InputSelect < TValue >
InputSelect组件将渲染为<select>。我们将在本章的后面创建InputSelect,所以我在此不再赘述。
输入文本
InputText组件将渲染为<input type="text">。
InputTextArea
InputSelect组件将渲染为<textarea>。在本章中,我们将构建我们自己版本的这个控件。
输入辐射
InputRadio组件会将渲染为<input type="radio">。
输入组
InputRadioGroup组件会将渲染为<Input type="radio">。
正如我们所看到的,几乎所有的 HTML 表单控件都有一个 Blazor 组件,它带有一些附加的功能,比如验证,我们将在下一节中看到。
添加验证
我们已经谈到了验证的问题;在input组件和EditForm中有一些内置的功能来处理验证。
在我们的表单中添加验证的一种方法是使用DataAnnotations。通过使用数据标注,我们不需要编写任何自定义逻辑来确保表单中的数据是正确的;相反,我们可以给数据模型添加属性,让DataAnnotationsValidator来处理剩下的事情。
里面有一堆DataAnnotations的实例.NET 已经可以使用了;我们也可以构建自己的注释。
一些内置数据注释如下:
Required:使字段成为必填字段Email:将检查输入的值是否为电子邮件地址MaxLength:将检查字符数是否超出Range:将检查数值是否在一定范围内
还有更多注释可以帮助我们验证数据。为了测试这一点,让我们向数据类添加数据注释:
-
在
MyBlog.Data项目中,打开Models/BlogPost.cs。 -
在文件顶部,添加对
System.ComponentModel.DataAnnotations:using System.ComponentModel.DataAnnotations;的引用
-
Add the
RequiredandMinLengthattributes to theTitleproperty:[Required] [MinLength(5)] public string Title { get; set; }Required属性将确保我们不能将标题留空,MinLength将确保它至少有5字符: -
Add the
Requiredattribute to theTextproperty:[Required] public string Text { get; set; }Required属性将确保Text属性不能为空,这是有道理的——我们为什么要创建一个空的博文? -
打开
Models/Category.cs,在文件顶部添加对System.ComponentModel.DataAnnotations的引用。 -
Add the
Requiredattribute to theNameproperty:[Required] public string Name { get; set; }Required属性将确保我们不能将名称留空。 -
打开
Models/Tag.cs,在文件顶部添加对System.ComponentModel.DataAnnotations的引用。 -
Add the
Requiredattribute to theNameproperty:[Required] public string Name { get; set; }Required属性将确保我们不能将名称留空。
太好了,现在我们的数据模型内置了验证功能。我们需要给我们的用户反馈验证出了什么问题。
我们可以通过使用ValidationMessage或ValidationSummary组件来做到这一点。
验证消息
ValidationMessage组件可以为我们显示特定属性的单独错误信息。我们想使用这个组件来显示表单元素下的验证错误。
要添加一个ValidationMessage组件,我们必须用我们想要显示验证错误的属性名称来指定For属性:
<ValidationMessage For="@(() => model.Name)"/>
有效性摘要
ValidationSummary组件将所有验证错误显示为整个EditContext的列表。
我更喜欢显示问题附近的错误,这样用户就能清楚问题在哪里。但是我们也可以选择使用ValidationSummary将验证错误显示为列表。
为了确保我们的输入控件与 Bootstrap 主题(或者我们可能使用的任何主题)相匹配,我们可以创建自己的自定义验证类。
自定义验证类属性
只需使用编辑表单、输入组件和DataAnnotationValidator,框架将在组件有效和无效时自动向组件添加类。
默认情况下,这些类是.valid和.invalid。英寸 NET 5 中,我们可以自己定制这些类名。
使用 Bootstrap 时,默认类名为.is-valid和.is-invalid,类名也必须有.form-control才能获得正确的样式。
我们接下来要构建的组件将帮助我们在所有表单组件上获得正确的 Bootstrap 样式。
我们将创建自己的FieldCssClassProvider来定制 Blazor 将使用的类:
-
在
MyBlogServerSide项目中,右键点击Components文件夹,选择添加类,命名类BootstrapFieldCssClassProvider。 -
Open the new class and add the following code:
using Microsoft.AspNetCore.Components.Forms; using System.Linq; namespace MyBlogServerSide.Components { public class BootstrapFieldCssClassProvider : FieldCssClassProvider { public override string GetFieldCssClass (EditContext editContext, in FieldIdentifier fieldIdentifier) { var isValid = !editContext.GetValidationMessages (fieldIdentifier).Any(); var isModified = editContext.IsModified(fieldIdentifier); return (isModified, isValid) switch { (true, true) => "form-control modified is-valid", (true, false) => "form-control modified is-invalid", (false, true) => "form-control", (false, false) => "form-control" }; } } }BootstrapFieldCssClassProvider需要一个EditContext实例才能工作。代码将检查表单(具体来说是
EditContext)是否有效,是否被修改过。基于此,它返回正确的 CSS 类。返回所有元素的表单控件;这样,我们就不必将它添加到表单的每个元素中。我们可以验证一个未被修改的表单是否有效,但是我们不希望它仅仅因为还没有被修改就显示表单是好的。
-
We need to get the
EditContextinstance from ourEditFormand then setFieldCssClassProvideronEditContextas follows:CurrentEditContext.SetFieldCssClassProvider(provider);接下来,我们将以更优雅的方式(以我的拙见)用我们接下来将创建的
CustomCssClassProvider来做这件事。
本章前面我提到EditForm正在将其EditContext暴露为CascadingValue。
这意味着我们将构建一个组件,我们可以将它放入我们的EditForm中,并以这种方式访问EditContext:
-
在
MyBlogServerSide项目中,右键点击Components文件夹,选择添加类,命名类CustomCssClassProvider。 -
Open the new file and add the following code:
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using System; namespace MyBlogServerSide.Components { public class CustomCssClassProvider<ProviderType>: ComponentBase where ProviderType: FieldCssClassProvider,new() { [CascadingParameter] EditContext? CurrentEditContext { get; set; } public ProviderType Provider { get; set; } = new ProviderType(); protected override void OnInitialized() { if (CurrentEditContext == null) { throw new InvalidOperationException ($"{nameof(DataAnnotationsValidator)} requires a cascading " + $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)}" + $"inside an EditForm."); } CurrentEditContext.SetFieldCssClassProvider (Provider); } } }这是一个接受
type值的通用组件,在本例中,是Provider的类型。我们规定
type必须从FieldCssClassProvider继承,并且必须有一个不带参数的构造函数。该组件继承自
ComponentBase,这使得将该组件放入 Blazor 组件成为可能。我们有一个
Cascading参数,将从EditForm开始填充。如果由于某种原因EditContext丢失,我们抛出一个异常(例如,如果我们将组件放在EditForm之外)。最后,我们将
FieldCssClassProvider设置为EditContext。
要使用该组件,我们只需在我们的EditForm中添加以下代码(别担心,我们很快就会创建一个EditForm:
<CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider"/>
我们只需为我们的CustomCssClassProvider组件提供正确的ProviderType BootstrapFieldCssClassProvider。
现在,我们有一个组件将使我们的表单控件看起来像 Bootstrap 控件。接下来,是时候将它付诸实践,通过构建我们的管理界面来创建几个表单了。
构建管理界面
现在是时候为我们的博客建立一个简单的管理界面了。
我们需要能够做到以下几点:
- 列出类别
- 编辑类别
- 列表标签
- 编辑标签
- 列出博客文章
- 编辑博客文章
如果我们看看前面的列表,我们可能会注意到一些事情看起来很相似——也许我们可以为它们构建组件。类别和标签非常相似;他们有名字,名字是我们唯一能编辑的东西。
让我们为此制作一个组件。组件将负责列出、添加、删除和更新对象。
由于我们使用的对象是Category或Tag,我们需要能够根据对象调用不同的 API,因此我们的组件需要是通用的:
-
在
MyBlogServerSide项目中,右键点击Components文件夹,选择添加 | 剃须刀组件,然后命名组件ItemList.razor。 -
Open the newly created file and in the
codesection, add the following lines of code:[Parameter] public List<ItemType> Items { get; set; } = new List<ItemType>(); [Parameter] public RenderFragment<ItemType> ItemTemplate { get; set; }我们需要两个参数:一个我们可以添加所有项目的列表和一个
ItemTemplate实例,我们可以用它来改变我们想要项目的显示方式。在这种情况下,我们使用
RenderFragment<T>,这将使我们能够访问模板内部的项目(一旦我们实现它,事情就会变得更加清楚)。 -
We also need a couple of events; add the following code to the
codesection:[Parameter] public EventCallback<ItemType> DeleteEvent { get; set; } [Parameter] public EventCallback<ItemType> SelectEvent { get; set; }我们增加了两个事件;首先是当我们删除一个标签或一个类别时。我们将向父组件发送事件,在那里我们可以添加删除该项所需的代码。
第二种是当我们选择一个项目时,我们可以编辑该项目。
-
Now it's time to add the UI; replace
<h3>ItemList<h3>with the following code at the top of the file:@typeparam ItemType @using System.Collections.Generic <h3>List</h3> <table> <Virtualize Items="@Items" Context="item"> <tr> <td><button class="btn btn-primary" @onclick="@(()=> {SelectEvent.InvokeAsync(item); })"> Select</button> </td> <td>@ItemTemplate(item)</td> <td><button class="btn btn-danger" @onclick="@(()=> {DeleteEvent.InvokeAsync(item);})"> Delete</button> </td> </tr> </Virtualize> </table>带
@typeparam的第一行是让组件通用,持有通用类型的变量叫做ItemType。如果我们回顾第 2 步,我们会注意到我们使用了列表和
RenderFragment的变量。然后,我们使用新的
Virtualize组件来列出我们的项目;平心而论,我们可能没有那么多类别或标签,但为什么不在可能的情况下使用它呢?我们将Items属性设置为"Items"(这是我们列表的名称),将Context参数设置为"item"。我们可以给它取任何我们想要的名字;我们将只在
Virtualize渲染模板内部使用它。我们添加了两个按钮,简单地调用我们在步骤 3 中添加的
EventCallback实例。在那些按钮之间,我们增加了@ItemTemplate(item);我们希望 Blazor 渲染模板,但我们也在循环中发送当前项目。这意味着我们可以访问模板中项目的值。
列表和编辑类别
有了我们的新组件,现在是时候创建一个组件来列出和编辑我们的类别了。
-
在
MyBlogServerSide项目中,右键点击Pages文件夹,选择添加 | 新文件夹,命名文件夹Admin。 -
右键点击
Pages/Admin文件夹,选择添加 | 剃须刀组件,然后命名组件CategoryList.razor。 -
At the top of the component, replace
<h3>CategoryList</h3>with the following code:@page "/admin/categories" @using MyBlogServerSide.Components @inject IMyBlogApi api <h3>Categories</h3>我们从
@page指令开始,告诉 Blazor 如果我们导航到 URL"admin/categories",我们将到达CategoryList.Razor组件。我们将添加一个
using语句,然后注入我们的 API。 -
The next step is to add a form where we can edit the categories. Add the following code under the code from the previous step:
<EditForm OnValidSubmit="Save" Model="Item"> <DataAnnotationsValidator /> <CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" /> <InputText @bind-Value="@Item.Name" /> <ValidationMessage For="@(()=>Item.Name)" /> <button class="btn btn-success" type="submit">Save</button> </EditForm>我们添加了
EditForm,如果表单验证通过,将执行Save方法。为了验证,我们添加了DataAnnotationsValidator,它将根据我们添加到Tag和Category类的注释来验证所提供的数据。由于我们正在使用 Bootstrap,我们希望我们的表单控件看起来相同,所以我们添加了我们在本章前面创建的
CustomCssClassProvider。我们添加了
InputText并将其连接到一个名为Item的Category对象(我们稍后将添加该对象)。在下面,我们添加了
ValidationMessage,这将显示名称属性的任何错误,然后一个提交按钮。 -
Now it's time to add our
ItemListcomponent; under the code we added in the previous step, add this code:<ItemList Items="Items" DeleteEvent="@Delete" SelectEvent="@Select" ItemType="Category"> <ItemTemplate> @{ var item = context as Category; if (item != null) { @item.Name; } } </ItemTemplate> </ItemList>我们添加了我们的组件,并将
Items属性绑定到一个项目列表(我们将在下一步创建该列表)。我们将
Select和Delete事件绑定到方法,并在ItemType属性中指定列表的类型。然后,我们有ItemTemplate。由于我们正在使用RenderFragment<T>,我们现在可以访问一个名为context的变量。我们将该变量转换为一个类别,并打印出类别的名称。这是将在列表中显示的每个项目的模板。
-
Finally, we add the following code to the
codesection:@code { private List<Category> Items { get; set; } = new List<Category>(); public Category Item { get; set; } = new Category(); protected async override Task OnInitializedAsync() { Items = await api.GetCategoriesAsync(); await base.OnInitializedAsync(); } private async Task Delete(Category category) { try { await api.DeleteCategoryAsync(category); Items.Remove(category); } catch { } } private async Task Save() { try { if (Item.Id == 0) { Items.Add(Item); } await api.SaveCategoryAsync(Item); Item = new Category(); } catch { } } private Task Select(Category category) { try { Item = category; } catch { } return Task.CompletedTask; } }我们添加了一个保存所有类别的列表和一个保存一个项目(当前正在编辑的项目)的变量。我们使用
OnInitializedAsync从 API 加载所有的类别。Delete和Save方法只是调用 API 对应的方法,Select方法获取提供的项并将其放入项变量(准备编辑)。 -
运行项目,导航至
/admin/categories。 -
尝试添加、编辑、删除一个类别,如图图 6.1 :

图 6.1–编辑类别视图
现在我们也需要一个用于列出和编辑标签的组件——这几乎是一回事,但是我们需要使用Tag而不是Category。
列表和编辑标签
我们刚刚创建了一个用于列出和编辑类别的组件,现在我们需要创建一个用于列出和编辑标签的组件。
-
右键点击
Pages/Admin文件夹,选择添加 | 剃须刀组件,然后命名组件TagList.razor。 -
At the top of the component, replace
<h3>TagList</h3>with the following code:@page "/admin/tags" @using MyBlogServerSide.Components @inject IMyBlogApi api <h3>Tags</h3>我们从
@page指令开始,该指令告诉 Blazor,如果我们导航到 URL"admin/tags",我们将到达TagList.Razor组件。我们添加一个
using语句,然后注入我们的 API。 -
The next step is to add a form where we can edit the tags. Add the following code under the code from the previous step:
<EditForm OnValidSubmit="Save" Model="Item"> <DataAnnotationsValidator /> <CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" /> <InputText @bind-Value="@Item.Name" /> <ValidationMessage For="@(()=>Item.Name)" /> <button class="btn btn-success" type="submit">Save</button> </EditForm>我们添加了
EditForm,如果表单验证通过,将执行Save方法。为了验证,我们添加了DataAnnotationsValidator,它将根据我们添加到Tag和Category类的注释来验证所提供的数据。由于我们正在使用 Bootstrap,我们希望我们的表单控件看起来相同,所以我们添加了
CustomCssClassProvider,这是我们在本章前面创建的。我们添加了
InputText并将其连接到一个名为Item的Tag对象(稍后我们将添加该对象)。下面,我们添加一个
ValidationMessage实例,它将显示名称属性的任何错误,然后添加一个提交按钮。 -
Now it's time to add our
ItemListcomponent. Under the code we added in the previous step, add this code:<ItemList Items="Items" DeleteEvent="@Delete" SelectEvent="@Select" ItemType="Tag"> <ItemTemplate> @{ var item = context as Tag; if(item!=null) { @item.Name; } } </ItemTemplate> </ItemList>我们添加了我们的组件,并将
Items属性绑定到一个项目列表(我们将在下一步创建该列表)。我们将Select和Delete事件绑定到方法,并在ItemType属性中指定List的类型。然后我们有
ItemTemplate;由于我们正在使用RenderFragment<T>,我们现在可以访问一个名为context的变量。我们将这个变量转换成一个标签,并打印出标签的名称。这是将在列表中显示的每个项目的模板。
-
Finally, we add the following code under the
codesection:@code { private List<Tag> Items { get; set; } = new List<Tag>(); public Tag Item { get; set; } = new Tag(); protected async override Task OnInitializedAsync() { Items = await api.GetTagsAsync(); await base.OnInitializedAsync(); } private async Task Delete(Tag tag) { try { await api.DeleteTagAsync(tag); Items.Remove(tag); } catch { } } private async Task Save() { try { if (Item.Id == 0) { Items.Add(Item); } await api.SaveTagAsync(Item); Item = new Tag(); } catch { } } private Task Select(Tag tag) { try { Item = tag; } catch { } return Task.CompletedTask; } }我们添加了一个保存所有标签的列表和一个保存一个项目(当前正在编辑的项目)的变量。我们使用
OnInitializedAsync从 API 加载所有标签。Delete和Save方法只需调用 API 的相应方法,Select方法获取提供的项目并将其放入Item变量(准备编辑)。 -
运行项目,导航至
/admin/tags。 -
尝试添加、编辑、删除标签,如图图 6.2 :

图 6.2–编辑标签视图
现在我们只剩下两件事:我们需要列出和编辑博客文章的方法。
列出并编辑博文
让我们从列出和编辑博文开始:
-
右键点击
Pages/Admin文件夹中的,选择添加 | 剃须刀组件,命名组件BlogPostList.razor。 -
At the top of the
BlogPostList.razorfile, replace<h3>BlogPostList</h3>with the following code:@page "/admin/blogposts" @inject IMyBlogApi api <a href="/admin/blogposts/new">New blog post</a> <ul> <Virtualize ItemsProvider="LoadPosts" Context="p"> <li>@p.PublishDate <a href="/admin/blogposts/@p.Id">@p.Title</a> </li> </Virtualize> </ul>我们添加了一个页面指令,注入了我们的 API,并使用
Virtualize组件列出了博客文章。我们还将这些帖子链接到一个带有博客帖子的
Id实例的网址。 -
Replace the
codesection with the following code:@code{ public int TotalBlogposts { get; set; } private async ValueTask<ItemsProviderResult<BlogPost>> LoadPosts(ItemsProviderRequest request) { if (TotalBlogposts == 0) { TotalBlogposts = await api.GetBlogPostCountAsync(); } var numblogposts = Math.Min(request.Count, TotalBlogposts - request.StartIndex); var posts = await api.GetBlogPostsAsync(numblogposts, request.StartIndex); return new ItemsProviderResult<BlogPost>(posts, TotalBlogposts); } }我们添加了一个可以从数据库加载帖子的方法。该代码与我们在索引页面上的代码相同。现在这一章只剩下一件事:添加我们可以编辑博文的页面。
一种非常流行的写博文的方式是使用 Markdown 我们的博客引擎将支持这一点。既然 Blazor 支持任何.NET 标准 dll,我们将添加一个名为
Markdig的现有库。这与微软用于其文档站点的引擎相同。
我们可以用不同的扩展来扩展Markdig(就像微软所做的那样),但是让我们保持简单,只添加对 Markdown 的支持,而不添加所有花哨的扩展:
-
在
MyBlogServerSide项目下,右键单击解决方案资源管理器中的依赖关系节点,并选择管理 NuGet 包。 -
Search for
Markdigand click Install as shown in Figure 6.3:![Figure 6.3 – Add NuGet dialog]()
图 6.3–添加数字获取对话框
-
右键单击
components文件夹中的,选择添加 | 类,然后命名组件InputTextAreaOnInput.cs。 -
Open the new file and add the following code:
using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { public class InputTextAreaOnInput : InputBase<string?> { protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "textarea"); builder.AddMultipleAttributes(1, AdditionalAttributes); builder.AddAttribute(2, "class", CssClass); builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue)); builder.AddAttribute(4, "oninput", EventCallback.Factory.CreateBinder <string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); builder.CloseElement(); } protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage) { result = value; validationErrorMessage = null; return true; } } }前面的代码取自微软的 GitHub 资源库;这就是他们如何实现
InputTextArea组件。在他们的构建系统中,他们不能处理
.razor文件,所以这就是他们以这种方式实现代码的原因。这个文件只有一处改动,那就是oninput,用来表示OnChange。对于大多数情况来说,
OnChange就可以了,这意味着当我离开文本框时,值将被更新(并触发验证)。但是在我们的例子中,我们希望实时更新 HTML 的预览,这就是为什么我们必须实现自己的预览。一种选择可能是不使用
InputTextArea组件,而是使用TextArea标签,但是这样我们就会丢失验证高亮。如果我们需要定制输入控件上的行为,这就是我们要走的路。如果你打算对实现进行大量的修改,我建议使用
.razor文件而不是.cs文件。 -
右键点击
Pages/Admin文件夹中的,选择添加 | 剃须刀组件,命名组件BlogPostEdit.razor。 -
At the top of the
BlogPostEdit.razorfile, replace<h3>BlogPostEdit</h3>with the following code:@page "/admin/blogposts/new" @page "/admin/blogposts/{Id:int}" @inject IMyBlogApi api @inject NavigationManager manager @using MyBlogServerSide.Components @using Markdig;我们添加了两个不同的
page指令,因为我们希望能够创建一个新的博文,并提供一个 ID 来编辑一个已经存在的博文。如果我们不提供标识,Id参数将是null(或默认值)。我们注入了我们的 API 和
NavigationManager以及添加using语句。 -
Now we need to add the form; add the following code:
<EditForm Model="Post" OnValidSubmit="SavePost"> <DataAnnotationsValidator /> <CustomCssClassProvider ProviderType="BootstrapFieldCssClassProvider" /> <InputText @bind-Value="Post.Title"></InputText> <InputDate @bind-Value="Post.PublishDate"> </InputDate> <InputSelect @bind-Value="selectedCategory"> <option value="0" disabled>None selected </option> @foreach (var category in Categories) { <option value="@category.Id">@category.Name </option> } </InputSelect> <ul> @foreach (var tag in Tags) { <li> @tag.Name @if (Post.Tags.Any(t => t.Id == tag.Id)) { <button type="button" @onclick="@(() => { Post.Tags.Remove(Post.Tags.Single (t=>t.Id==tag.Id)); })">Remove </button> } else { <button type="button" @onclick="@(() => { Post.Tags.Add(tag); })">Add </button> } </li> } </ul> <InputTextAreaOnInput @bind-Value="Post.Text" @onkeyup="UpdateHTML"> </InputTextAreaOnInput> <button type="submit" class="btn btn-success"> Save</button> </EditForm>我们添加
EditForm,当提交表单时(如果有效),我们执行SavePost方法。我们添加DataAnnotationValidator,它将根据类中的数据注释来验证我们的模型。我们添加
customCssClassProvider以便获得正确的 Bootstrap 类名。然后,我们为标题、发布日期、类别、标签以及最后但同样重要的文本(博客文章的内容)添加框。最后,我们使用我们在步骤 4 中创建的组件(每次按键都会更新的组件)添加文本。
我们还连接了
@onkeyup事件,这样我们就可以更新每次按键的预览。 -
我们也需要添加我们的
SavePost方法。在code部分添加以下代码:public async Task SavePost() { if (selectedCategory != 0 && Categories != null) { var category = Categories.FirstOrDefault(c => c.Id == selectedCategory); if (category != null) { Post.Category = category; } } await api.SaveBlogPostAsync(Post); manager.NavigateTo("/admin/blogposts"); } -
Now it's time to show the preview. Add the following code just below
EditForm:@((MarkupString)markDownAsHTML)我们使用
MarkupString来确保 Blazor 在不转义字符的情况下输出 HTML 代码。你可能还记得第 4 章**了解基本 Blazor 组件中的内容。
** We also need some variables. Add the following code in the code section:
```cs
[Parameter]
public int? Id { get; set; }
BlogPost Post { get; set; } = new BlogPost();
List<Category>? Categories { get; set; }
List<Tag>? Tags { get; set; }
int selectedCategory = 0;
string? markDownAsHTML { get; set; }
```
我们为博客帖子的 ID 添加了参数(如果我们想编辑一个),一个变量来保存我们正在编辑的帖子,一个保存所有类别,一个保存所有标签。我们还添加了一个保存当前选择的类别的变量和一个保存转换为 HTML 的标记的变量。
* Now it is time to set up `Markdig`. Add the following code somewhere in the `code` section:
```cs
MarkdownPipeline pipeline;
protected override Task OnInitializedAsync()
{
pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley()
.Build();
return base.OnInitializedAsync();
}
```
要配置`Markdig`,我们需要创建一个管道。正如我在本章前面提到的,这是微软用于其文档站点的引擎。它有许多扩展可用,包括源代码突出显示和表情符号。
为了让它更有趣一点,我们还在管道中添加了表情符号。
* 我们还需要添加代码来加载数据(博客文章、类别和标签)。在`code`部分增加以下方法:
```cs
protected void UpdateHTML()
{
markDownAsHTML = Markdig.Markdown.ToHtml(Post.Text, pipeline);
}
bool hasTag(MyBlog.Data.Models.Tag tag)
{
return Post.Tags.Contains(tag);
}
protected override async Task OnParametersSetAsync()
{
if (Id != null)
{
Post = await api.GetBlogPostAsync(Id.Value);
if (Post.Category != null)
{
selectedCategory = Post.Category.Id;
}
UpdateHTML();
}
Categories = await api.GetCategoriesAsync();
Tags = await api.GetTagsAsync();
base.OnParametersSet();
}
```
* Now run the site, navigate to `/admin/blogposts`, click on a blog post to edit it, and test the new Markdown support. *Figure 6.4* shows the edit page with Markdown support:

图 6.4–支持降价的编辑页面
我们还有一件事要做:我们需要确保博文页面显示的是转换后的 HTML 版本的 Markdown。
* 打开`/Pages/Post.razor`并在文件顶部添加以下`using`语句:
```cs
@using Markdig;
```
* 将以下代码添加到`code`部分:
```cs
MarkdownPipeline pipeline;
protected override Task OnInitializedAsync()
{
pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley()
.Build();
return base.OnInitializedAsync();
}
```
* Replace the following row:
```cs
@((MarkupString)BlogPost.Text)
```
替换为:
```cs
@((MarkupString)Markdig.Markdown.ToHtml(BlogPost.Text, pipeline))
```*
*干得好!现在我们有了一个管理界面,可以开始写博客了。
总结
在本章中,我们学习了如何创建表单。我们进行 API 调用来获取和保存数据。
我们构建了自定义输入控件,并利用了中的一些新功能.NET 5 在我们的控件上获得 Bootstrap 样式。大多数业务应用使用表单,通过使用数据注释,我们可以添加接近数据的逻辑(甚至在创建数据库时使用注释,就像我们在 第 3 章介绍实体框架核心中所做的那样)。
Blazor 在验证和输入控制方面提供的功能将帮助我们构建令人惊叹的应用,并给我们的用户带来出色的体验。您可能会注意到,现在管理页面已完全打开,因此下一步将是通过登录来保护我们的博客,但我们将在 第 8 章身份验证和授权中回到这一点。
在下一章中,我们将创建一个应用编程接口,以便在我们的 Blazor 网络组装项目中获取数据。*
七、创建应用编程接口
Blazor WebAssembly 需要能够检索数据,也能够更改我们的数据。为了做到这一点,我们需要一个可以访问数据库的应用编程接口。在本章中,我们将创建一个网络应用编程接口。
当我们使用 Blazor Server 时,API 将与页面一起被保护(如果我们添加一个Authorize属性),所以我们免费获得它。但是有了 WebAssembly,一切都将在浏览器中执行,所以我们需要一些 WebAssembly 可以与之通信的东西来更新服务器上的数据库。
为此,我们将涵盖需要涵盖的三个主题。在本章中,我们将介绍前两个:
- 创建服务
- 创建客户端
第三个话题是调用 API ,不过这一部分我们在本章就不赘述了;相反,我们将在 第 9 章 【共享代码和资源】中回到它。
技术要求
确保您已经阅读了前面的章节,或者使用Ch6文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 07找到本章最终结果的源代码。
创建服务
有很多方法可以创建服务,比如通过 REST 或者 gRPC。在这本书里,我们将涵盖 REST。
对于那些之前没有使用过 REST 的人来说, REST 代表表征状态转移。简单来说,就是机器使用 HTTP 与其他机器对话的一种方式。
使用 REST,我们对不同的操作使用不同的 HTTP 动词。可能是这样的:

这就是我们要为标签、类别和博文实现的。
由于 API 会考虑是否应该创建 p ost ,所以我们会稍微欺骗一下,只实现 Put (替换),因为我们不知道是创建还是更新数据。
API 只会是 Blazor WebAssembly 使用的,所以我们会在MyBlogWebAssembly.Server项目中实现 API。
添加数据库访问
执行以下步骤以提供数据库访问:
-
在
MyBlogWebAssembly.Server项目中,打开Startup.cs。 -
In the
Configure servicesmethod, add the following lines (at the top of the method):services.AddDbContextFactory<MyBlogDbContext>(opt => opt.UseSqlite($"Data Source=../../MyBlog.db")); services.AddScoped<IMyBlogApi, MyBlogApiServerSide>();这与
MyBlogServerSide项目的数据库配置相同。我们甚至指向同一个数据库,但是由于对于 Blazor WebAssembly 项目来说,文件夹结构更深一层,所以我们使用
..\..\MyBlog.db到达现有的数据库。 -
通过右键单击
MyBlogWebAssembly.Server项目下的依赖项并选择添加项目引用,添加对MyBlog.Data项目的引用。 -
勾选
MyBlog.Data,点击确定。 -
添加以下名称空间:
using MyBlog.Data; using MyBlog.Data.Interfaces; using Microsoft.EntityFrameworkCore;
现在我们增加了对我们在MyBlog.Data项目中的类的访问。
我们对它进行了配置,这样如果我们请求一个IMyBlogApi的实例,我们将得到一个MyBlogApiServerSide类的实例。这是因为我们在服务器端,所以 API 可以直接访问数据库。
现在,让我们创建应用编程接口。在Controllers文件夹中,我们已经有了一个获取天气预报数据的 API。
添加 API 控制器
执行以下步骤创建应用编程接口:
-
在
MyBlogWebAssembly.Server项目中,右键点击Controllers文件夹,选择添加 | 类。命名文件MyBlogApiController.cs。 -
在文件顶部增加
using语句:using Microsoft.AspNetCore.Mvc; using MyBlog.Data.Interfaces; using MyBlog.Data.Models; using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; -
继承自
ControllerBase并添加属性。班级应该是这样的:[ApiController] [Route("[controller]")] public class MyBlogApiController:ControllerBase { } -
Now we need to access the data, and we will do that through the server-side API. Add the following code inside the class we just created:
internal readonly IMyBlogApi api; public MyBlogApiController(IMyBlogApi api) { this.api = api; }现在我们可以通过
api变量访问数据。 -
Next, we will add the code to get blog posts. Add the following code under the code we just added:
[HttpGet] [Route("BlogPosts")] public async Task<List<BlogPost>> GetBlogPostsAsync(int numberofposts, int startindex) { return await api.GetBlogPostsAsync(numberofposts, startindex); }我们创建了一个直接从数据库返回数据的方法(与 Blazor Server 项目使用的 API 相同)。
转到以下网址:
https://localhost:5001/MyBlogApi/BlogPosts?numberofposts=10&startindex=0(端口号可能是其他内容)。确保启动MyBlogWebAssembly.Server项目。我们将通过我们的博客文章列表获得一些 JSON。有几件事情值得注意。方法叫做
GetBlogPostsAsync。我们选择和 API 同名,但是 URL 和方法名不一样;它由Route属性指定。我们使用与IMyBlogApi相同的方法名称;当所有东西的名称都相同时,更容易遵循代码。我们还指定了
HttpGet属性,这将确保该方法仅在我们使用 Get 动词时运行。我们有了一个良好的开端!现在我们也需要实现 API 的其余部分。
-
Let's add the function to get the blog post count:
[HttpGet] [Route("BlogPostCount")] public async Task<int> GetBlogPostCountAsync() { return await api.GetBlogPostCountAsync(); }我们使用 Get 动词,但是用了另一条路线。
-
We also need to be able to get one blog post. Add the following method:
[HttpGet] [Route("BlogPosts/{id}")] public async Task<BlogPost> GetBlogPostAsync(int id) { return await api.GetBlogPostAsync(id); }在这种情况下,我们使用 Get 动词,但是带有另一个包含我们想要获取的帖子的 id 的 URL。
接下来,我们需要一个受保护的 API,通常是更新或删除东西的 API。
-
Let's add an API that saves a blog post. Add the following code under the code we just added:
[Authorize] [HttpPut] [Route("BlogPosts")] public async Task<BlogPost> SaveBlogPostAsync([FromBody] BlogPost item) { return await api.SaveBlogPostAsync(item); }正如我在本章前面提到的中,我们将只添加一个用于创建和更新博客帖子的应用编程接口,我们将使用 Put 动词(替换)来实现这一点。我们在方法中添加了
Authorize属性,这将确保用户需要通过身份验证才能调用方法。 -
Next up, we add a method for deleting blog posts. To do this, add the following code:
[Authorize] [HttpDelete] [Route("BlogPosts")] public async Task DeleteBlogPostAsync([FromBody] BlogPost item) { await api.DeleteBlogPostAsync(item); }在这种情况下,我们使用删除动词,就像保存一样,我们也添加了
Authorize属性。 -
Next, we need to do this for
CategoriesandTagsas well. Let's start withCategories. Add the following code to theMyBlogApiControllerclass:
```cs
[HttpGet]
[Route("Categories")]
public async Task<List<Category>> GetCategoriesAsync()
{
return await api.GetCategoriesAsync();
}
[HttpGet]
[Route("Categories/{id}")]
public async Task<Category> GetCategoryAsync(int id)
{
return await api.GetCategoryAsync(id);
}
[Authorize]
[HttpPut]
[Route("Categories")]
public async Task<Category> SaveCategoryAsync([FromBody] Category item)
{
return await api.SaveCategoryAsync(item);
}
[Authorize]
[HttpDelete]
[Route("Categories")]
public async Task DeleteCategoryAsync([FromBody] Category item)
{
await api.DeleteCategoryAsync(item);
}
```
这些都是处理**类**需要的方法。
- 接下来,让我们对标签做同样的事情。在我们刚刚添加的代码下添加以下代码:
```cs
[HttpGet]
[Route("Tags")]
public async Task<List<Tag>> GetTagsAsync()
{
return await api.GetTagsAsync();
}
[HttpGet]
[Route("Tags/{id}")]
public async Task<Tag> GetTagAsync(int id)
{
return await api.GetTagAsync(id);
}
[Authorize]
[HttpPut]
[Route("Tags")]
public async Task<Tag> SaveTagAsync([FromBody] Tag item)
{
return await api.SaveTagAsync(item);
}
[Authorize]
[HttpDelete]
[Route("Tags")]
public async Task DeleteTagAsync([FromBody] Tag item)
{
await api.DeleteTagAsync(item);
}
```
太好了。我们有一个 API!现在是时候编写将访问该 API 的客户端了。
创建客户端
要访问应用编程接口,我们需要创建一个客户端。有很多方法可以做到这一点,但是我们将以最简单的方式,通过自己编写代码来做到这一点。
客户端将实现相同的IMyBlogApi界面。因此,无论我们使用哪种实现,我们都有完全相同的代码,使用MyBlogApiServerSide或MyBlogApiClientSide直接访问数据库,我们接下来将创建:
-
右键单击
MyBlog.Data下的依赖关系节点,选择管理 NuGet 包。 -
搜索
Microsoft.AspNetCore.Components.WebAssembly.Authentication点击安装。 -
另外,搜索
Newtonsoft.Json和Microsoft.Extensions.Http,点击安装。 -
我们需要一些助手的方法,所以右击
MyBlog.Data添加一个文件夹,然后添加 | 文件夹,命名文件夹Extensions。 -
右键单击新的文件夹,选择添加 | 类。命名类
HttpClientExtensions.cs。 -
添加以下名称空间:
using Newtonsoft.Json; using System.Net.Http; using System.Threading; -
Replace the class with the following code:
public static class HttpClientExtensions { public static Task<HttpResponseMessage> DeleteAsJsonAsync<T>(this HttpClient httpClient, string requestUri, T data) => httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Delete, requestUri) { Content = Serialize(data) }); public static Task<HttpResponseMessage> DeleteAsJsonAsync<T>(this HttpClient httpClient, string requestUri, T data, CancellationToken cancellationToken) => httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Delete, requestUri) { Content = Serialize(data) }, cancellationToken); public static Task<HttpResponseMessage> DeleteAsJsonAsync<T>(this HttpClient httpClient, Uri requestUri, T data) => httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Delete, requestUri) { Content = Serialize(data) }); public static Task<HttpResponseMessage> DeleteAsJsonAsync<T>(this HttpClient httpClient, Uri requestUri, T data, CancellationToken cancellationToken) => httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Delete, equestUri) { Content = Serialize(data) }, cancellationToken); private static HttpContent Serialize(object data) => new StringContent (JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); }这些是一些扩展方法,将帮助我们调用 API。
-
右键点击
MyBlog.Data项目,选择添加 | 类。命名班级MyBlogApiClientSide.cs。 -
打开新创建的文件。
-
将
IMyBlogApi添加到类中,像这样公开:
```cs
public class MyBlogApiClientSide:IMyBlogApi
{}
```
- Some of the API calls are going to be public (do not require authentication), but
HttpClientwill be configured to always require a token (we will do that later in the chapter).
因此,我们将需要一个经过身份验证的`HttpClient`和一个未经身份验证的`HttpClient`,这取决于我们调用的是什么 API。
- 为了能够调用 API,我们需要注入
HttpClient。将以下代码添加到类中:
```cs
private readonly IHttpClientFactory factory;
public MyBlogApiClientSide(IHttpClientFactory factory)
{
this.factory = factory;
}
```
- 我们还需要添加以下名称空间:
```cs
using MyBlog.Data.Interfaces;
using System.Net.Http;
using MyBlog.Data.Models;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using MyBlog.Data.Extensions;
using Newtonsoft.Json;
```
- Now it's time to implement calls to the API. Let's begin with the Get calls for blog posts. Add the following code:
```cs
public async Task<BlogPost> GetBlogPostAsync(int id)
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<BlogPost> ($"MyBlogAPI/BlogPosts/{id}");
}
public async Task<int> GetBlogPostCountAsync()
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<int> ("MyBlogAPI/BlogPostCount");
}
public async Task<List<BlogPost>> GetBlogPostsAsync(int numberofposts, int startindex)
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<List<BlogPost>> ($"MyBlogAPI/BlogPosts?numberofposts= {numberofposts}&startindex={startindex}");
}
```
我们使用我们注入的`HttpClient`然后调用`GetFromJsonAsync`,它会自动下载 JSON 并将其转换为我们提供给泛型方法的类。
现在有点棘手了:我们需要处理认证幸运的是,这是内置在`HttpClient`中的,所以我们只需要处理`AccessTokenNotAvailable Exception`。如果令牌丢失,它会自动尝试续订,但是如果出现问题(例如,用户没有登录),我们可以重定向到登录页面。
我们将在 [*第 8 章*](08.html#_idTextAnchor122)*认证和授权*中回到令牌和认证是如何工作的。
- Next, we add the API calls that need authentication, such as saving or deleting a blog post.
在我们刚刚添加的代码下添加以下代码:
```cs
public async Task<BlogPost> SaveBlogPostAsync(BlogPost item)
{
try
{
var httpclient = factory.CreateClient("Authenticated");
var response= await httpclient.PutAsJsonAsync<BlogPost>
("MyBlogAPI/BlogPosts",item);
var json = await response.Content.ReadAsStringAsync();
return
JsonConvert.DeserializeObject<BlogPost>(json);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
return null;
}
public async Task DeleteBlogPostAsync(BlogPost item)
{
try
{
var httpclient = factory.CreateClient("Authenticated");
await httpclient.DeleteAsJsonAsync<BlogPost> ("MyBlogAPI/BlogPosts", item);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
```
如果呼叫抛出`AccessTokenNotAvailableException`,则表示`HttpClient`无法获取或自动续订令牌,用户需要登录。
这种状态可能永远不会发生,因为我们将确保当用户导航到该页面时,他们需要登录,但安全总比抱歉好。
我们还使用了一个名为`Authenticated`的`HttpClient`,我们需要对其进行配置,但是我们将在 [*第 8 章*](08.html#_idTextAnchor122)*认证和授权*中回到这个问题。
- 现在我们需要为类别做同样的事情。将以下代码添加到
MyBlogApiClientSide类中:
```cs
public async Task<List<Category>> GetCategoriesAsync()
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<List<Category>> ($"MyBlogAPI/Categories");
}
public async Task<Category> GetCategoryAsync(int id)
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<Category> ($"MyBlogAPI/Categories/{id}");
}
public async Task DeleteCategoryAsync(Category item)
{
try
{
var httpclient = factory.CreateClient("Authenticated");
await httpclient.DeleteAsJsonAsync<Category> ("MyBlogAPI/Categories", item);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
public async Task<Category> SaveCategoryAsync(Category item)
{
try
{
var httpclient = factory.CreateClient("Authenticated");
var response = await httpclient.PutAsJsonAsync<Category> ("MyBlogAPI/Categories", item);
var json = await response.Content.ReadAsStringAsync();
return
JsonConvert.DeserializeObject<Category>(json);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
return null;
}
```
- And next up, we will do the same for Tags. Add the following code just under the code we just added:
```cs
public async Task<Tag> GetTagAsync(int id)
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<Tag> ($"MyBlogAPI/Tags/{id}");
}
public async Task<List<Tag>> GetTagsAsync()
{
var httpclient = factory.CreateClient("Public");
return await httpclient.GetFromJsonAsync<List<Tag>> ($"MyBlogAPI/Tags");
}
public async Task DeleteTagAsync(Tag item)
{
try
{
var httpclient = factory.CreateClient("Authenticated");
await httpclient.DeleteAsJsonAsync<Tag> ("MyBlogAPI/Tags", item);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
public async Task<Tag> SaveTagAsync(Tag item)
{
try
{
var httpclient = factory.CreateClient("Authenticated");
var response = await httpclient.PutAsJsonAsync<Tag> ("MyBlogAPI/Tags", item);
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Tag>(json);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
return null;
}
```
干得好!我们的 API 客户端现在完成了!
总的来说,这两步都完成了,只剩下一步了;正如本章前面提到的一样,我们不会涉及本章的最后一部分。相反,我们将在 第 9 章共享代码和资源中回到它。
总结
在本章中,我们学习了如何创建应用编程接口和应用编程接口客户端,这是大多数应用的重要组成部分。这样,我们可以从我们的数据库中获取博客文章,并在我们的 Blazor WebAssembly 应用中显示它们。
在下一章 第八章认证和授权中,我们将为我们的网站添加登录功能。
在之后的章节中, 第 9 章共享代码和资源,我们最终会让两个项目运行在同一个代码上,这也是我们第一次尝试我们的 API 的地方。
八、认证授权
在这一章中,我们将学习如何向我们的博客添加身份验证和授权,因为我们不希望只有任何人能够创建或编辑博客文章。
完全涵盖身份验证和授权本身需要一整本书,因此我们在这里将保持简单。本章的目标是在 ASP.NET 内置的现有功能的基础上,让内置的身份验证和授权功能发挥作用。这意味着这里没有太多的 Blazor 魔法;我们可以利用已经存在的许多资源。
今天几乎每个系统都有某种登录方式,无论是管理界面(像我们的)还是会员登录门户。有许多不同的登录提供商,如谷歌、推特和微软。我们可以使用所有这些提供者,因为我们将仅仅建立在已经存在的架构上。
我们将保持简单,并将用户添加到数据库中。
我们将在本章中讨论以下主题:
- 实现身份验证
- 添加授权
技术要求
确保您已经阅读了前面的章节,或者使用Chapter07文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 08找到本章最终结果的源代码。
实施认证
认证方面有很多内置功能。实现身份验证最简单的方法是在创建项目时选择一个身份验证选项,但是我们在这里学习如何正确工作,因此我们将自己实现身份验证。
我们需要为 Blazor Server 项目和 Blazor WebAssembly 项目分别实现身份验证,因为它们的工作方式有些不同。
但是我们仍然可以在这两个项目之间共享一些东西——首先,让我们将必要的表添加到数据库中。
向数据库添加表
为了能够添加认证,我们需要将必要的表添加到我们的数据库中。这是我们可以使用实体框架完成的事情:
-
在
MyBlog.Data项目中,我们需要添加几个 NuGet 包;右键单击依赖项,选择管理号码包。 -
搜索
Microsoft.AspNetCore.Identity.EntityFrameworkCore点击安装。 -
搜索
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore点击安装。 -
We also need to add
IdentityServer. Search forMicrosoft.AspNetCore.ApiAuthorization.IdentityServerand click Install.如果您只打算使用 Blazor Server,则不需要这一步,但由于我们希望我们的解决方案同时在 Blazor Server 和 Blazor WebAssembly 上工作,因此我们将确保现在添加此
IdentityServer。 -
Open the
MyBlogDbContext.csfile. Change the code so thatMyBlogDbContextinherits fromApiAuthorizationDbContext <AppUser>and add a new constructor and overriddenOnModelCreatingas follows:public class MyBlogDbContext : ApiAuthorizationDbContext<AppUser> { public MyBlogDbContext(DbContextOptions options) : base(options, new OperationalStoreOptionsMigrations()) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); }通过添加这些代码,我们也包括了继承类中的表。
-
In the
MyBlogDbContext.csfile, we have a class calledMyBlogDbContextFactory. Change the path to the database to../MyBlog.dbas follows:optionsBuilder.UseSqlite("Data Source = ../MyBlog.db");这样,当我们更新数据库时,我们将为我们所有的项目更新它(我们所有的项目都使用相同的数据库文件)。
-
Also, add this class (use the same file since the projects are tightly coupled):
public class OperationalStoreOptionsMigrations : IOptions<OperationalStoreOptions> { public OperationalStoreOptions Value => new OperationalStoreOptions() { DeviceFlowCodes = new TableConfiguration("DeviceCodes"), EnableTokenCleanup = false, PersistedGrants = new TableConfiguration("PersistedGrants"), TokenCleanupBatchSize = 100, TokenCleanupInterval = 3600, }; }我们使用这个类来配置
IdentityServer,我们需要这个类,因为我们对身份部分(用户名、密码和令牌)和我们的数据(博客文章、标签和类别)使用相同的数据上下文。为了能够创建
DbContextFactory,我们需要一个只有一个参数的构造函数。我们本可以创建多个数据库上下文,一个用于我们的数据,一个用于身份信息,但是随着我们的前进,我们所做的将证明是一个更容易的解决方案。
增加以下
using语句:using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using IdentityServer4.EntityFramework.Options; using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; using Microsoft.Extensions.Options; -
右键单击
models文件夹中的,选择添加 | 类。名称班级AppUser.cs。 -
Open the
AppUserclass and replace the content with the following:using Microsoft.AspNetCore.Identity; namespace MyBlog.Data.Models { public class AppUser : IdentityUser {} }现在我们需要创建一个迁移,就像我们之前在 第三章引入实体框架核心一样;我们将使用 PowerShell 来实现这一点。
-
Open PowerShell and navigate to the folder that you have the
MyBlog.Dataproject in.
这也可以在 Visual Studio 的开发人员 PowerShell 中完成。
- Execute the following commands:
```cs
dotnet ef migrations add Identity
dotnet ef database update
```
提醒一下,我们正在运行`dotnet`工具来创建名为`Identity`的迁移。
我们还更新数据库,以便它获得所有最新的迁移,我们准备开始使用新的数据上下文。
接下来,我们需要配置 Blazor 服务器项目。
配置 Blazor 服务器项目
我们需要告诉 Blazor Server 项目,我们希望它使用身份验证。我们通过在Startup.cs中添加配置来做到这一点:
-
在
MyBlogServerSide项目中,右键单击依赖关系节点,选择管理 NuGet 包。 -
Search for
Microsoft.AspNetCore.Identity.UIand click Install.这个包包含一个用户界面和扩展,当涉及到用户登录时将帮助我们。
ASP.NET 支持多种不同的身份验证方式,因此利用其身份验证基础架构中已经存在的东西非常有意义。
-
右键点击
MyBlogServerSide项目,选择添加文件夹,命名文件夹Authentication。 -
右键点击文件夹,选择添加 | 类,命名类
RevalidatingIdentityAuthenticationStateProvider.cs。 -
We don't need to talk about the content of this class since this is normally provided in a Blazor template. Simply copy the content from the GitHub repository found here: https://github.com/PacktPublishing/Web-Development-with-Blazor/blob/master/Chapter08/MyBlog/MyBlogServerSide/Authentication/RevalidatingIdentityAuthenticationStateProvider.cs.
当我们在创建项目时选择添加身份验证时,这是微软将为我们提供的文件之一。
它将检查用户凭据是否仍然有效(默认为 30 分钟后)。
-
打开
Startup.cs并添加以下名称空间:using MyBlog.Data.Models; using Microsoft.AspNetCore.Components.Authorization; using MyBlogServerSide.Authentication; -
To not have to repeat ourselves, let's add the connection string as a setting instead.
打开
appsetting.json并在第一个大括号后添加以下内容:"ConnectionStrings": { "MyBlogDB": "Data Source=../MyBlog.db" }, -
Add this code at the bottom of the
ConfigureServicesmethod:services.AddDbContext<MyBlogDbContext>(opt => opt.UseSqlite(Configuration.GetConnectionString("MyBlogDB"))); services.AddDefaultIdentity<AppUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<MyBlogDbContext>(); services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider <AppUser>>();我们配置了内置的身份提供程序,这样 Blazor 就知道在哪里可以找到用户和密码。
我们还需要为
DbContext增加一个配置。我们将在应用的其余部分使用DbContextFactory,但是Identity功能需要DbContext,所以我们为Identity功能添加了一个副本来工作。确保在我们刚刚添加的代码上方几行
DBContextFactory处更改连接字符串。 -
在
app.UseRouting()正下方的Configure方法中,添加以下代码:app.UseAuthentication(); app.UseAuthorization(); -
Open the
App.Razorfile and replace the content with the following:
```cs
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly"
PreferExactMatches="@true">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>Not authorized</p>
</NotAuthorized>
<Authorizing>
<p>Checking</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
```
我们添加了`CascadingAuthenticationState`,这将确保所有组件都可以访问`AuthenticationState`(无论我们是否登录)。
我们还增加了`AuthorizeRouteView`,会检查页面是否通过认证。
如果用户没有通过身份验证,我们可以使用`NotAuthorized`模板显示另一条消息,并在检查授权时显示另一条消息。
- 右键点击
components文件夹,选择添加 | 剃须刀组件。命名组件LoginDisplay.razor。 - Open the new component and replace the content with the following:
```cs
<AuthorizeView>
<Authorized>
<a href="Identity/Account/Manage">Hello, @context.User.Identity.Name!</a>
<form method="post" action="/LogOut">
<button type="submit" class="nav-link btn btn-link">Log out</button>
</form>
</Authorized>
<NotAuthorized>
<a href= "Identity/Account/Register"> Register </a>
<a href="Identity/Account/Login">Log in</a>
</NotAuthorized>
</AuthorizeView>
```
在这个文件中,我们使用了内置的`AuthorizeView`组件,这将使得根据用户是否登录来指定不同的视图成为可能。
如果他们已登录,我们希望显示注销链接,如果他们未登录,我们希望显示登录或注册链接。
- 打开
_Imports.razor并在文件中任意位置添加以下using语句:
```cs
@using MyBlogServerSide.Components
```
- 打开
Shared/MainLayout.razor并在关于链接后的页面中添加组件:
```cs
<LoginDisplay />
```
- 身份 UI 需要一个名为
_LoginPartial.cshtml的文件才能工作。右键点击Pages文件夹,选择添加 | 文件夹;命名文件夹Shared。 - 右键点击
Pages/Shared文件夹,点击添加 | 新项目。 - 点击剃刀页面–清空并命名文件
_LoginPartial.cshtml。 - Replace the content of the file with the following:
```cs
@using Microsoft.AspNetCore.Identity;
@using MyBlog.Data.Models;
@inject SignInManager<AppUser> SignInManager
@inject UserManager<AppUser> UserManager
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage"> Hello @User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/" method="post"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
</li>
}
</ul>
```
我们有这样一个文件的原因是登录页面来自`Microsoft.AspNetCore.Identity.UI`包,它为我们提供了使用脸书、谷歌、微软、推特和其他账户登录的功能。
我们免费获得所有这些功能。我们也可以通过搭建视图来定制登录页面,但是我们不会在本书中讨论这个问题。
更多关于脚手架的信息可以在这里找到:[https://docs . Microsoft . com/en-us/aspnet/core/security/authentication/scaffold-identity?视图=aspnetcore-5.0 &选项卡= visual studio](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-5.0&tabs=visual-studio)。
- 右键点击
Pages文件夹,选择添加 | 新项目。 - 点击剃刀页面–清空并命名文件
Logout.cshtml。 - 将文件的内容替换为:
```cs
@page
@using Microsoft.AspNetCore.Identity
@using MyBlog.Data.Models
@attribute [IgnoreAntiforgeryToken]
@inject SignInManager<AppUser> SignInManager
@functions {
public async Task<IActionResult> OnPost()
{
if (SignInManager.IsSignedIn(User))
{
await SignInManager.SignOutAsync();
}
return Redirect("~/");
}
}
```
- Now we need something to secure. We need to edit the following four files and add the
@attribute [Authorize]attribute to each file:
`Pages/Admin/BlogPostEdit.razor`
`Pages/Admin/BlogPostList.razor`
`Pages/Admin/CategoryList.razor`
`Pages/Admin/TagList.razor`
- 将启动项目更改为
MyBlogServerSide,按 F5 运行项目。 - If you now navigate to
https://localhost:5001/admin/Tags(the port number may differ), you will notice that you get a Not authorized message, as shown in Figure 8.1:

图 8.1–未授权视图
- 单击注册并使用您的凭据登录,您现在可以访问
TagList组件。
恭喜,您现在有了一个在服务器端运行登录功能的站点。现在我们需要为 Blazor WebAssembly 实现同样的事情。
配置 Blazor WebAssembly 项目
WebAssembly项目具有一些相同的功能;它稍微复杂一点,因为它也需要 API 身份验证。
默认情况下(如果我们在创建项目时选择添加身份验证),它将使用IdentityServer对客户端和应用编程接口进行身份验证,这也是我们将要使用的。
IdentityServer是一个开源项目,将帮助我们处理我们网站的认证以及我们的 API。
自从我们在我们的MyBlog.Data项目中实施IdentityServer以来,我们已经准备好了大部分我们需要的东西。
让我们实现剩下的。
首先,我们对MyBlogWebAssembly.Server项目进行一些修改。
正在更新我的博客程序集。计算机网络服务器
执行以下步骤更新MyBlogWebAssembly.Server项目:
-
在
MyBlogWebAssembly.Server项目中,打开Startup.cs。 -
添加以下名称空间:
using MyBlog.Data.Models; using Microsoft.AspNetCore.Authentication; -
打开
appsetting.json并在第一个大括号后添加以下内容:"ConnectionStrings": { "MyBlogDB": "Data Source=../../MyBlog.db" }, -
In the
ConfigureServicesmethod, add the following at the bottom of the method (this needs to be afterAddDbContextFactory):services.AddDbContext<MyBlogDbContext>(opt => opt.UseSqlite(Configuration.GetConnectionString("MyBlogDB"))); services.AddDefaultIdentity<AppUser>(options => options.SignIn.RequireConfirmedAccount = false) .AddEntityFrameworkStores<MyBlogDbContext>(); services.AddIdentityServer() .AddApiAuthorization<AppUser, MyBlogDbContext>(); services.AddAuthentication() .AddIdentityServerJwt();我们配置了数据库,这与我们在本章前面为 Blazor Server 项目所做的几乎相同。
我们还配置了
Identity提供商和IdentityServer。我们还在配置中添加了一个 JWT。 JWT 代表 JSON 网络令牌,这是一个互联网标准,用于通过可选签名/加密创建数据,该签名/加密保存 JSON 并可以保存多个声明。
令牌存储在浏览器的会话存储器中。有两个令牌:一个显示我们已经登录,另一个用于 API 访问(这由框架为我们处理)。
这是我们的应用编程接口客户端(我们在本章开头创建的)将发送给应用编程接口进行身份验证的内容。
这将自动发生在我们身上。
-
In the
Configuremethod, just aboveapp.UseEndpoints, add the following code:app.UseIdentityServer(); app.UseAuthentication(); app.UseAuthorization();这些线需要在
app.UseRouting();和app.UseEndpoints之间,否则你会得到一个警告,事情可能不会如你所料。 -
现在我们需要
_LoginPartial,就像我们为 Blazor 服务器项目所做的那样。右键点击Pages文件夹,选择添加 | 文件夹;命名文件夹Shared。 -
右键点击
Pages/Shared,点击添加 | 剃刀页面;命名页面_LoginPartial.cshtml。 -
将下面的代码添加到我们刚刚创建的文件中:
@using Microsoft.AspNetCore.Identity @using MyBlog.Data.Models @inject SignInManager<AppUser> SignInManager @inject UserManager<AppUser> UserManager @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ var returnUrl = "/"; if (Context.Request.Query.TryGetValue("returnUrl", out var existingUrl)) { returnUrl = existingUrl; } } <ul class="navbar-nav"> </ul> -
Inside of the
<ul>tag, add the following code:@if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage"> Hello @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/" method="post"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Register </a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Login</a> </li> }根据用户的当前状态,我们显示用户是否登录。如果用户登录,我们会显示一个问候和一个注销链接。
-
We also need to create a controller for Open ID Connect.
右键点击`Controllers`文件夹,选择**添加** | **类**;命名班级`OidcConfigurationController.cs`。
- Open the file we just created and replace the content with the following:
```cs
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace MyBlogWebAssembly.Server.Controllers
{
public class OidcConfigurationController : Controller
{
private readonly ILogger<OidcConfigurationController> _logger;
public OidcConfigurationController (IclientRequestParametersProvider clientRequestParametersProvider, ILogger<OidcConfigurationController> logger)
{
ClientRequestParametersProvider = clientRequestParametersProvider;
_logger = logger;
}
public IclientRequestParametersProvider ClientRequestParametersProvider { get; }
[HttpGet("_configuration/{clientId}")]
public IactionResult GetClientRequestParameters([FromRoute] string clientId)
{
var parameters = ClientRequestParametersProvider. GetClientParameters(HttpContext, clientId);
return Ok(parameters);
}
}
}
```
当客户端请求令牌时,该控制器负责将令牌发送给客户端。
- Open
appsettings.jsonand add the following just above the last curly brace:
```cs
,
"IdentityServer": {
"Clients": {
"MyBlogWebAssembly.Client": {
"Profile": "IdentityServerSPA"
}
}
}
```
这是我们客户的名字,需要注明;您必须在 API 中使用相同的客户端名称。
- Open
appsettings.Development.jsonand add the following:
```cs
,
"IdentityServer": {
"Key": {
"Type": "Development"
}
}
```
最后两步是配置`IdentityServer`。我们创建了一个名为`IdentityServerSPA`的客户档案。我们还使用了`Development`的密钥类型,它创建了一个我们可以在开发过程中使用的假证书(但是在部署到生产服务器时,我们需要用真实证书替换它)。
服务器部分现在已经完成;现在我们需要对MyBlogWebAssembly.Client项目进行修改。
正在更新我的博客程序集。客户
现在我们需要告诉客户端项目我们要告诉项目使用身份验证:
-
首先,我们需要添加几个 NuGet 包。右键点击
MyBlogWebAssembly.Client项目,点击管理 NuGet 包。 -
搜索
Microsoft.AspNetCore.Components.WebAssembly.Authentication点击安装。 -
搜索
Microsoft.Extensions.Http点击安装。 -
Open
Program.csand replace the linebuilder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });with the following code block:builder.Services.AddHttpClient("Authenticated", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler<BaseAddressAuthorizationMessage Handler>(); builder.Services.AddHttpClient("Public", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); builder.Services.AddApiAuthorization();我们添加了两个
HttpClient依赖注入,一个用于调用经过身份验证的 API,一个用于调用任何未经身份验证的 API。如果我们在没有令牌(没有登录)的情况下调用一个 API,调用经过认证的 API 会抛出异常;这就是为什么我们需要一个特定的 API 来调用不需要我们登录的 API。
-
增加以下
using语句:using Microsoft.AspNetCore.Components.WebAssembly.Authentication; -
现在我们需要添加几个文件。右键点击
Pages文件夹,选择添加 | 剃须刀组件,命名文件Authentication.razor。 -
Replace the content of the file with the following:
@page "/authentication/{action}" @using Microsoft.AspNetCore.Components.WebAssembly.Authentication <RemoteAuthenticatorView Action="@Action" /> @code{ [Parameter] public string Action { get; set; } }该组件将重定向并登录服务器。
这是许多魔法发生的地方:根据动作,它会将您重定向到服务器和内置的身份验证用户界面。
RemoteAuthenticatorView有很多不同的模板可以用来自定义组件,比如LogInFailed、CompletingLogOut和LoggingIn。我们使用服务器进行身份验证,客户端将获得一个令牌,这样我们就知道我们已经登录,并且可以从我们的 API 中获取数据。
-
右键点击
Shared文件夹,选择添加 | 剃须刀组件,命名文件LoginDisplay.razor。 -
Replace the content of the file with the following:
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @inject NavigationManager Navigation @inject SignOutSessionStateManager SignOutManager <AuthorizeView> <Authorized> <a href="authentication/profile">Hello, @context.User.Identity.Name!</a> <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button> </Authorized> <NotAuthorized> <a href="authentication/register">Register</a> <a href="authentication/login">Log in</a> </NotAuthorized> </AuthorizeView> @code{ private async Task BeginSignOut(MouseEventArgs args) { await SignOutManager.SetSignOutState(); Navigation.NavigateTo("authentication/logout"); } }这个文件和我们为 Blazor 服务器创建的文件有点不同。它使用我们刚刚创建的
authentication组件,并对服务器进行回调。它将注销客户端和服务器。
-
右键点击
Shared文件夹,选择添加 | 剃须刀组件,命名文件RedirectToLogin.razor。 -
Replace the content of the file with the following:
```cs
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
protected override void OnInitialized()
{
Navigation.NavigateTo($"authentication/ login?returnUrl={Uri.EscapeDataString (Navigation.Uri)}");
}
}
```
该组件将重定向到登录页面,再次使用`authentication`组件。
- 打开
_Imports.razor并添加此:
```cs
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
```
- 现在我们需要通过打开
App.Razor激活认证,并用以下代码替换内容:
```cs
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly"
PreferExactMatches="@true">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (!context.User.Identity. IsAuthenticated)
{
<RedirectToLogin />
}
else
{
<p>You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
```
- Now we need to add a reference to the JavaScript file. Open
wwwroot/index.htmland just above the reference toblazor.webassembly.js, add the following:
```cs
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
```
现在一切都准备好了,但是我们还需要一些东西来调用 API。
- 在
@page指令
```cs
@inject IHttpClientFactory factory
```
后打开`Pages/FetchData.razor`并注射`IhttpClientFactory`
- Change the content of the
OnInitializedAsyncmethod to this:
```cs
try
{
var httpclient = factory.CreateClient("Authenticated");
forecasts = await httpclient.GetFromJsonAsync <WeatherForecast[]>("WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
```
这将调用天气预报服务,如果令牌丢失,它将重定向到登录页面。
- 添加以下命名空间:
```cs
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
```
- 现在是运行项目的时候了。确定将
MyBlogWebAssembly.Server设为启动项目,按 F5 。 - 现在导航到天气预报服务,您将被重定向到登录/注册页面,在那里您可以创建帐户或登录。
太好了,现在登录我们网站的一切都准备好了。有时候,我们想授予不同的用户不同的权限,这就是下一节的内容。
添加授权
至此,我们知道用户是否通过认证,但用户是否可以访问特定的功能?这就是授权的意义所在。幸运的是,内置函数也支持这一点,尽管我们必须为它编写一些代码。
服务器端拥有向用户添加角色所需的所有表。但是,没有可用的用户界面。对于我们的应用,我们将在数据库中手动添加一个角色,但是首先,我们需要配置角色。
从服务器添加角色
执行以下步骤从服务器添加角色:
-
在
MyBlogWebAssembly.Server项目中,打开Startup.cs文件。 -
In the
ConfigureServicesmethod, add options to.AddApiAuthorizationand remove the default claim mapping as follows:.AddApiAuthorization<AppUser, MyBlogDbContext>(options => { options.IdentityResources["openid"].UserClaims. Add("name"); options.ApiResources.Single().UserClaims.Add("name"); options.IdentityResources["openid"].UserClaims. Add("role"); options.ApiResources.Single(). UserClaims.Add("role"); }); JwtSecurityTokenHandler.DefaultInboundClaimFilter. Remove("role");这将包括令牌中的角色,以便我们可以在客户端使用令牌。
-
将角色添加到
Services.AddDefaultIdentity中,因此它现在看起来像这样:services.AddDefaultIdentity<AppUser>(options => options.SignIn.RequireConfirmedAccount = false) .AddRoles<IdentityRole>() .AddEntityFrameworkStores<MyBlogDbContext>(); -
添加名称空间:
using Microsoft.AspNetCore.Identity; using System.IdentityModel.Tokens.Jwt;
服务器现在会将角色发送给客户端,但客户端不会监听;因此,接下来,我们需要对客户端进行更改。
向客户端添加角色
为了让客户端提取角色,我们需要从访问令牌中解析它们。别担心,事情没有听起来那么复杂:
-
右键点击
MyBlogWebAssembly.Client项目,然后点击添加 | 新建文件夹;命名文件夹Authentication。 -
右键点击
Authentication文件夹,选择添加 | 类,然后命名类RoleAccountClaimsPrincipalFactory.cs。 -
Replace the content of the file with the code from GitHub here: https://github.com/PacktPublishing/Web-Development-with-Blazor/blob/master/Chapter08/MyBlog/MyBlogWebAssembly/Client/Authentication/RoleAccountClaimsPrincipalFactory.cs.
我们在这里所做的是确保我们得到了可以找到角色的 JSON 节点。根据返回的节点数量,可以有一个字符串或字符串数组。我们检查它是否是一个数组,如果是,我们将每个项目添加给用户;如果它是一个字符串,我们只向用户添加该单个项目。
-
现在我们需要将它添加到依赖注入管道中。打开
program.cs,将builder.Services.AddApiAuthorization();替换为:builder.Services.AddApiAuthorization() .AddAccountClaimsPrincipalFactory<RoleAccountClaims PrincipalFactory>(); -
添加以下命名空间:
using MyBlogWebAssembly.Client.Authentication;
现在使用角色的一切都准备好了,接下来我们需要向数据库中添加一个角色。
向数据库添加角色
要将数据添加到我们的数据库中,我们可以使用一个名为 SQLite 的数据库浏览器的工具:
-
从【https://sqlitebrowser.org/】下载 SQLite 的数据库浏览器(如果你有其他更喜欢使用的应用,请随意使用)。
-
在数据库浏览器中打开
MyBlog.db;那里应该有 15 张桌子。 -
点击浏览数据选项卡,选择
AspNetRoles表格。 -
Now create a role – let's call it
Administrator. Click on the Insert new row into the current table button (a document with a small +) and use the following values:Id : 留空
名称:T0
归一化名称 :
administrator同意盖章 : 留空
-
将表格更改为
AspNetUsers并复制您的用户标识(一个 GUID)。 -
将表格改为
AspNetUserRoles,点击在当前表格中插入新行按钮(一个带有小 + 的文档),然后粘贴用户的 ID 和角色的 ID。
太好了。现在我们的用户是管理员。让我们快速测试一下:
-
在
MyBlogWebAssembly.Client项目中,打开Pages/Index.razor并在组件底部添加以下内容:<AuthorizeView Roles="Administrator"> <Authorized> You are an admin! </Authorized> <NotAuthorized> Not logged in or not administrator </NotAuthorized> </AuthorizeView> -
打开
_Imports.razor并将其添加为名称空间:@using MyBlogWebAssembly.Client.Pages -
打开
Shared/MainLayout.razor并在关于链接后的页面中添加组件:<LoginDisplay /> -
将
MyblogWebAssembly.Server设为启动项目。 -
现在运行项目( Ctrl + F5 )会看到消息未登录或者不是管理员,登录后会变成你是管理员!。
太棒了。我们有认证和授权工作!
总结
在本章中,我们学习了如何向现有站点添加身份验证。在创建项目时添加身份验证更容易,但是现在我们对幕后发生的事情有了更好的理解。
在下一章中,我们将在我们的 Blazor Server 项目和我们的 Blazor WebAssembly 项目之间共享组件,使两个项目看起来相同(并且看起来很棒),并首次调用我们的 web API。
九、分享代码和资源
在这一章中,是时候把项目放在一起了。可以在 Blazor 服务器和 Blazor WebAssembly 之间共享代码。这也是我们创建可重用组件并在社区或工作场所共享它们的方式。
使用这种方法,选择服务器或网络程序集不再重要。这样,您可以在移植现有站点时使用 Blazor Server,完成后,只需将共享库移动到新的宿主模型中。
我们还将添加静态内容,如 CSS。
在本章中,我们将涵盖以下主题:
- 清理项目
- 设置应用编程接口
- 移动组件
- 添加静态文件
- CSS 隔离
技术要求
请确保您已经阅读了前面的章节,或者使用Chapter08文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 09找到本章最终结果的源代码。
注意
如果您使用 GitHub 中的代码进入本章,请确保使用电子邮件注册用户,并按照说明添加用户和向数据库添加管理员角色。可以在 第八章 【认证授权】中找到说明。
清理项目
在整本书中,我们生成了一堆文件,如果我们在任何时候使用存储库,我们可能会有更多的文件。所以,我们需要做的第一件事就是稍微清理一下项目。
在MyBlogServerSide项目中,删除以下文件(如果以下列表中没有特定的文件,不用担心,直接进入下一个即可):
Pages/Alert–文件夹Pages/Events–文件夹Form-文件夹Pages/ComponentWithCascadingParameter.razorPages/ComponentWithCascadingValue.razorPages/CounterWithoutRazor.csPages/CounterWithParameter.razorPages/DBTest.razorPages/FetchDataWithCodeBehind.razorPages/FetchDataWithInherits.razorPages/Parameters.razorPages/ParentCounter.razorPages/SetFocus.razor
太好了。我们现在有一个稍微干净一点的项目。下一步是建立 Blazor WebAssembly 项目,并使其使用我们的新 API。
设置 API
只有 Blazor WebAssembly 需要访问 Web API,因为它没有直接的数据库访问权限。最常见的架构可能也是对 Blazor 服务器使用网络应用编程接口。
让我们将我们的项目连接到我们的应用编程接口,这里是依赖注入的亮点。
在我们的 Blazor Server 项目中,我们的services.AddScoped<IMyBlogApi, MyBlogApiServerSide>();配置告诉我们的应用,当我们请求IMyBlogApi的实例时,Blazor 应该返回一个MyBlogApiServerSide类的实例,这是一个可以直接访问数据库的 API 版本。
我们的共享组件只知道接口,应该返回的实例是按项目配置的。
在 Blazor WebAssembly 项目中,我们将返回我们在 第 7 章中创建的 Web API 客户端的一个实例,创建一个 API 。
然而,Blazor WebAssembly 项目引用具有直接数据库访问权限的库(如MyBlog.Data所具有的)是没有意义的。如果我们尝试,我们会收到一条错误消息。
因此,我们需要将可以共享的文件移到另一个库中。请执行以下步骤:
-
右键单击
MyBlog解决方案,选择添加 | 新项目。 -
搜索
Class Library (.NET Core),然后点击下一步。 -
命名项目
MyBlog.Data.Shared并保持位置不变,然后点击创建。 -
选择目标框架.NET 5.0(当前)然后点击创建。
-
Right-click on the Dependencies node under the
MyBlogWebAssembly.Clientproject.点击添加项目参考,查看我的博客。数据.共享和我的博客。共享复选框,然后点击确定。
-
Move the following files from the
MyBlog.Dataproject toMyBlog.Data.Shared:Extension–文件夹Interfaces–文件夹Models/BlogPost.csModels/Category.csModels/Tag.csMyBlogApiClientSide.cs不管宿主模型如何,这些文件都是相同的。
-
我们需要添加一些 NuGet 包来让事情正常进行。右键单击
MyBlog.Data.Shared项目下的依赖关系节点,选择管理 NuGet 包。 -
搜索
Newtonsoft.Json,然后点击安装。 -
搜索
Microsoft.AspNetCore.Components.WebAssembly.Authentication,然后点击安装。 -
搜索
Microsoft.Extensions.Http,然后点击安装。 -
现在我们需要参考新项目。右键单击
MyBlog.Data项目下的依赖关系节点,然后单击添加项目引用。 -
查看我的博客。数据.共享然后点击确定。
-
右键单击
MyBlog.Shared项目下的依赖关系节点,然后单击添加项目引用。 -
查看我的博客。数据.共享然后点击确定。
现在我们已经将可共享的类移到了一个新的库中。下一步是移动我们想要在项目之间共享的文件。
移动部件
我们将移动我们可以在 Blazor 服务器和 Blazor WebAssembly 项目之间共享的组件。这是 Blazor 的惊人力量之一;这两个项目唯一不同的是托管模式。代码可以保持不变(大多数情况下)。
在我们的案例中,我们确保有不同的方式来访问数据,只是为了涵盖这些可能性,但我们将在下一节中回到这一点。
首先,我们需要创建一个新项目并移动一些文件。为此,请执行以下步骤:
-
右键单击
MyBlog解决方案,选择添加 | 新项目。 -
搜索
Razor,应该会找到一个名为剃刀类库的模板。选择该模板,点击下一步。 -
命名项目
MyBlog.Shared,保持位置不变(应该已经在正确的文件夹中),然后点击下一步。 -
选择目标框架.NET 5.0(当前)并确保支持页面和视图未选中。然后,点击创建。
-
Add a reference to the
MyBlog.Data.Sharedproject by right-clicking the Dependencies node under theMyBlog.Sharedproject and selecting Add project reference.查看我的博客。数据.共享复选框,然后点击确定。
-
右键单击我的博客下的依赖项添加一个 NuGet 包。共享节点,选择管理 NuGet 包。
-
搜索
Markdig,然后点击安装。 -
Tick the Include pre-release box (at the time of writing, NuGet is only available as pre-release).
搜索
Microsoft.AspNetCore.Components.Web.Extensions,然后点击安装。解开包含预释放框。
-
搜索
Microsoft.AspNetCore.Components.WebAssembly.Authentication,然后点击安装。 -
现在我们需要添加一些名称空间。打开
_Imports.razor文件,用以下内容替换内容:
```cs
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MyBlog.Shared
@using MyBlog.Shared.Components
@using MyBlog.Data.Models;
@using MyBlog.Data.Interfaces;
@using Microsoft.AspNetCore.Components.Web.Extensions.Head
```
- 右键单击
MyBlog.Shared项目,然后选择添加 | 新文件夹。命名文件夹Components。 - Now, move all the files except
LoginDisplay.razorfrom theComponentsfolder in theMyBlogServerSideproject to theComponentsfolder in theMyBlog.Sharedproject.
**登录**显示因托管平台而异,所以我们不想分享那个。
- 右键单击
MyBlog.Shared项目,然后选择添加 | 新文件夹。命名文件夹Pages。 - 将文件夹从
MyBlogServerSide项目的Pages文件夹移动到MyBlog.Shared项目的Pages文件夹。 - 将
Index.razor和Post.razor文件从MyBlogServerSide项目中的Pages文件夹移动到MyBlog.Shared项目中的Pages文件夹。 - 右键单击
MyBlog.Shared项目,然后选择添加 | 新文件夹。命名文件夹Shared。 - 将
NavMenu.razor文件从MyBlogServerSide项目中的Shared文件夹移动到MyBlog.Shared项目中的Shared文件夹。 - Add a reference to the
MyBlog.Sharedproject by right-clicking the Dependencies node under theMyBlogServerSideproject and selecting Add project reference.
查看**我的博客。共享**复选框,然后点击**确定**。
现在我们已经移动了所有想要在项目之间共享的文件,并配置了MyBlogServerSide项目。接下来,我们将看看清理共享文件。
清理共享文件
此时,所有都应该构建,但是我们已经移动了文件,所以让我们确保移动的文件有匹配的名称空间:
-
In the
MyBlog.Sharedproject, change the namespace toMyBlog.Shared.Componentson the following files:Components/BootstrapFieldCssClassProvider.csComponents/ CustomCssClassProvider.cs -
We also have a couple of files referring to that namespace. Remove
@using MyBlogServerSide.Componentsfrom the following files:Pages/Admin/BlogPostEdit.razorPages/Admin/BlogPostList.razorPages/Admin/CategoryList.razorPages/Admin/TagList.razor -
Since we removed the namespace, we need to add the new one, but we can do that in
_Imports.razor. In theMyBlog.Sharedproject, add the following namespaces to the_Imports.razorfile:@using MyBlog.Shared @using MyBlog.Shared.Components现在我们可能会问自己,我们为什么不从一开始就这么做?好问题!最佳实践是避免在我们的剃须刀组件中包含
using语句,并始终将其包含在_Imports.razor中。但是为了显示这两个选项都工作得很好,我们在组件中有它们,但是接下来是时候清理一下混乱的了。
太棒了!我们有几个新项目,已经清理干净了。现在,是添加 API 的时候了。
添加原料药
我们通过将数据项目分成两部分来确保我们可以访问该应用编程接口。现在是时候将其添加到 Blazor WebAssembly 项目中了。请执行以下步骤:
-
In the
MyBlogWebAssembly.Clientproject, openProgram.csand add the following:builder.Services.AddScoped<IMyBlogApi, MyBlogApiClientSide>();当我们向依赖注入请求
IMyBlogApi时,我们会得到一个MyBlogApiClientSide的实例,它将调用我们在服务器端托管的 API(而不是直接的数据库调用)。 -
在文件顶部添加以下名称空间:
using MyBlog.Data; using MyBlog.Data.Interfaces; -
删除
Pages/Index.razor文件(因为我们将从共享库中获取该文件)。 -
Shared/NavMenu.razor也是如此。也删除那个文件。 -
Open
App.Razorand, in theroutercomponent, add the following as an additional property:AdditionalAssemblies="new[] { typeof(MyBlog.Shared.Pages.Index).Assembly}"这是告诉路由器在当前项目中寻找匹配,也是在
MyBlog.Shared装配中寻找匹配。在这种情况下,我们询问索引页的性质,然后获取程序集。这样,与仅仅将程序集名称作为字符串添加相比,我们获得了更多的控制(编译器会帮助我们)。
-
Add the following namespaces to
_Imports.razor:@using MyBlog.Shared @using MyBlog.Shared.Shared我们添加这些名称空间,以便我们的代码能够找到我们的页面和我们的
NavMenu组件。 -
In the
MyBlogWebAssembly.Serverproject, open theStartup.csfile and replaceservices.AddControllersWithViews();with the following code:services.AddControllersWithViews().AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler. Preserve; options.JsonSerializerOptions.PropertyNamingPolicy = null; });因为我们正在序列化实体框架对象,所以我们可以得到一个循环引用。这样,我们确保它不会序列化所有级别。在书中,我们保持简单。在现实项目中,应用编程接口实体可能没有循环引用,因此这一步可能没有必要。
这是向我们的项目添加 JSON 选项的一个很好的方法。这些设置与我们在 第 7 章创建 API 时在 API 客户端所做的设置相同。
-
通过右键单击项目并选择设置为启动项目并按 Ctrl + F5 将
MyBlogWebAssembly.Server设置为启动项目。 -
您现在应该会看到博客文章被列出,您应该能够通过点击链接导航到一篇博客文章。
太棒了。我们现在有相同的 Blazor 组件在 Blazor 服务器和 Blazor WebAssembly 中运行。
但是布局还是有很多不尽人意的地方,你猜怎么着?
这就是我们接下来要解决的问题。
添加静态文件
Blazor 可以使用静态文件,比如图像、CSS 和 JavaScript。如果我们将文件放在wwwroot文件夹中,它们将自动暴露在互联网上,并可从我们网站的根目录访问。Blazor 的好处是我们可以用一个库做同样的事情,在一个库中分发静态文件非常容易。
在工作中,我们在所有的 Blazor 项目之间共享组件,共享库也可以依赖于其他库。我们需要使用_content文件夹添加静态文件的链接。
看看这个例子:
<link rel="stylesheet" href="_content/MyBlog.Shared/MyBlogStyle.min.css" />
HTML link标签、rel和href是普通的 HTML 标签和属性,但是通过添加以_content开头的 URL,这是在告诉我们,我们想要访问的内容在另一个库中。库的名称(程序集名称)后面跟着MyBlog.Shared,然后是我们想要访问的文件,它存储在我们库中的wwwroot文件夹中。
Blazor 最终只是 HTML,而 HTML 可以使用 CSS 进行样式化。如上所述,默认情况下,Blazor 模板使用 Bootstrap,我们也将继续使用它。
有一个很棒的网站,有易于使用的 Bootstrap 主题可以下载,可以在https://bootswatch.com/找到。
我喜欢黑暗主题,所以这就是我们将使用的主题,但请稍后随意尝试。
CSS 对 LESS 对 SASS
CSS 代表层叠样式表,在这里你可以设置你的站点的输出样式。 LESS 代表精简样式表并扩展 CSS。 SASS 代表语法上很棒的样式表,工作方式与 LESS 相同。
我们可以在我们的项目中使用其中的任何一个。在我看来,LESS 和 SASS 让的写作风格变得简单了一些。Bootstrap 使用的是 SASS,所以让我们也这样做,将 Bootstrap 下载到我们的项目中。
SASS 将文件转换为 CSS,我们可以使用嵌套标签。那么,看看这个 CSS:
section { font-family: 'Comic Sans MS'; }
section h1, section .h1 {color: red; }
section h2, section .h2 { color: green; }
前面的 CSS 可以用 SASS 编写:
section{
font-family:'Comic Sans MS';
h1{color:red;}
h2{color:green;}
}
SASS 需要编写的代码更少,更容易跟踪。除此之外,还有其他好处,比如变量、循环等等。萨斯有两种口味——萨斯和 SCSS。我们将使用 SCSS,这是最新的,它有括号,所以对于 C#开发人员来说应该会更熟悉一些。
因为 SASS 是透明的,所以我们需要能够将 SASS 转换成 CSS 的东西。所以,第一步是安装 Web Essentials 2019。
准备 CSS/SASS
在使用 SASS 的时候,我更喜欢在 SASS 里什么都有,或者至少尽可能多。这意味着我们需要为引导下载 SASS 文件并安装一个扩展。
我们还将为我们的项目引入一个新的主题:
-
在 Visual Studio 中,点击扩展菜单,选择管理扩展。
-
搜索
Web essentials 2019,从搜索结果中选择,然后点击下载。 -
You will be prompted to restart Visual Studio. Please do so (all instances if you have more than one open) to finish the installation.
Web Essentials 2019 是许多不同扩展的集合,在开发 Web 应用时非常有用。
-
接下来,我们需要为引导下载 SASS 文件。打开网络浏览器,导航至https://getbootstrap.com/docs/5.0/getting-started/download/。
-
点击下载源码按钮,解压 ZIP 文件。
-
在
MyBlog.Shared项目中,创建一个名为Bootstrap的文件夹。 -
将
scss文件夹从 ZIP 文件复制到Bootstrap文件夹。 -
接下来,我们需要下载一个新的主题。导航至https://bootswatch.com/darkly/。
-
在名为的顶部菜单中,有一些链接。下载
_bootswatch.scss和_variables.scss。 -
在
MyBlog.Shared项目中,创建三个新文件夹,使新结构看起来像这样:Bootswatch/Dist/Darkly。 -
将
_bootswatch.scss和_variables.scss复制到Darkly文件夹中。 -
既然我们已经安装了 Web Essentials,我们现在可以使用其中一个扩展了。
-
We can now create a new SASS file by doing this:
选择`wwwroot`文件夹,按 *Shift* + *F2* ,将显示一个小对话框,您可以在其中提供文件名。它将使用文件扩展名来加载正确的模板。命名文件`MyBlogStyle.scss`。
- In the new file, add the following:
```cs
@import "../Bootswatch/Dist/Darkly/_variables";
@import "../Bootstrap/scss/bootstrap";
@import "../Bootswatch/Dist/Darkly/_bootswatch";
```
这将导入引导样本变量,以及引导和引导样本文件,当生成时,它将获取所有文件并将它们放在一个文件中。
- We get some styles with Blazor that we can move to our SCSS file. We want to keep the styles for the error message box from
site.css.
在`MyblogStyle.scss`末尾增加以下内容:
```cs
.content {
padding-top: 1.1rem;
}
.navbar-brand
{
margin-left:30px;
}
.bi
{
margin-right:5px;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
```
因此,在这个案例中,我们将 SASS(引导和 Bootswatch)与 Blazor 模板附带的 CSS 混合在一起。
- Right-click on the
MyBlogStyle.scssfile and select Web compiler | Compile file.
您会注意到它将创建四个新文件。首先,我们有`MyBlogStyle.css`和`MyBlogStyle.min.css`,这是生成的 CSS 和 CSS 的缩小版。它们位于`MyBlogStyle.css`节点下。我们还得到`compilerconfig.json`和`compilerconfig.json.defaults`,这是网络编译器的设置。
现在我们有了所有的先决条件和 CSS,我们可以添加到我们的网站。
向我的博客服务器添加 CSS
现在是时候给我们的网站添加新风格了。让我们从MyBlogServerSide开始:
-
打开
Pages/_Host.cshtml。 -
删除这些行:
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> -
添加对我们新样式表的引用(包含引导和引导样本黑色主题):
<link rel="stylesheet" href="_content/MyBlog.Shared/MyBlogStyle.min.css" /> -
Open
App.Razorand, in theroutercomponent, add the following as an additional property:AdditionalAssemblies="new[] { typeof(MyBlog.Shared.Pages.Index).Assembly}"这与我们在本章前面为网络组装项目所做的事情是一样的。
-
在
_Imports.Razor中,添加以下名称空间:@using MyBlog.Shared @using MyBlog.Shared.Shared -
设置
MyBlogServerSide为启动项目,按 Ctrl + F5 运行项目。
太好了。我们的 Blazor 服务器项目现在更新为使用新的样式。
给我的 BlogWebAssembly 添加 CSS。客户
现在让我们对 Blazor WebAssembly 项目做同样的事情:
-
在
MyBlogWebAssembly.Client项目中,打开wwwroot/index.html: -
删除以下行:
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> -
添加 CSS:
<link rel="stylesheet" href="_content/MyBlog.Shared/MyBlogStyle.min.css" /> -
设置
MyBlogwebAssembly.Server为启动项目,按 Ctrl + F5 运行项目。
现在两个项目的布局都一样了。
使管理界面更加可用
现在让我们再清理一下。我们只是从管理功能开始,所以让我们让它更容易访问。左边的菜单不再是必需的,所以让我们更改它,以便只有当您是管理员时,菜单才可见。
我们需要在这两个项目中实施变革:
-
打开
MyBlogWebAssembly.Client/Shared/MainLayout.razor把AutorizeView放在sidebar周围div像这样:<AuthorizeView Roles="Administrator"> <div class="sidebar"> <NavMenu /> </div> </AuthorizeView> -
用
MyBlogServerSide/Shared/MainLayout.razor做同样的事情。 -
设置
MyBlogServerSide为启动项目,按 Ctrl + F5 运行。 -
Verify that the menu is only visible when you are logged in by logging in and out.
现在我们需要让菜单看起来更好。即使点击计数器真的很有趣,但当涉及到我们的博客时,它就没有多大意义了。
由于nav菜单现在是共享的,我们可以把它放在一个地方,对于 Blazor Server 和 Blazor WebAssembly,它都会改变。
让菜单更有用
我们应该将链接替换为我们管理页面的链接,您可能已经注意到链接中的图标已经消失了(因为我们删除了旧的 CSS),但是请不要担心,因为 Bootstrap 有一些我们可以使用的图标:
-
打开网络浏览器,导航至https://github.com/twbs/icons/releases/latest/。
-
在页面底部的资产标题下,有一个到
bootstrap-icons-{versionnumber}.zip的链接。下载那个文件。 -
Once it's downloaded, extract the ZIP and copy
bootstrap-Icons-{versionnumber}to thewwwrootfolder in ourMyBlog.Sharedproject.在
MyBlogStyle.scss中增加以下一行:@import "./bootstrap-icons-1.4.1/bootstrap-icons";在这种情况下,是
1.4.1版本,您可以根据版本号进行更改。由于 SASS 是完全 CSS 兼容的,我们可以这样导入 CSS,并且由于有浏览器需要访问的文件(字体文件),我们将其放在
wwwroot文件夹中。 -
We also need to make a small change at the top of the
wwwroot\bootstrap-icons-1.4.1\font\bootstrap-icons.cssfile.考虑到文件在库中,文件的路径必须是正确的。
将文件顶部(唯一更改的是路径)
./fonts更改为/_content/MyBlog.Shared/bootstrap-icons-1.4.1/fonts:@font-face { font-family: "bootstrap-icons"; src: url("/_content/MyBlog.Shared/bootstrap-icons-1.4.1/fonts/bootstrap-icons.woff2?231ce25e89ab5804f9a6c427b8d325c9") format("woff2"), url("/_content/MyBlog.Shared/bootstrap-icons-1.4.1/fonts/bootstrap-icons.woff?231ce25e89ab5804f9a6c427b8d325c9") format("woff"); } -
点击
MyBlog.Shared/wwwroot/bootstrap-icons-1.4.1/fonts/bootstrap-icons.woff,确保复制到输出目录设置为如果更新复制。 -
点击
MyBlog.Shared/wwwroot/bootstrap-icons-1.4.1 /fonts/bootstrap-icons.woff2,确保复制到输出目录设置为如果更新复制。 -
In the
MyBlog.Sharedproject, open theShared/Navmenu.razorfile.编辑代码,使其看起来像这样(保持代码块不变):
<div class="top-row pl-4 navbar navbar-dark"> <a class="navbar-brand" href="">MyBlog Admin</a> <button class="navbar-toggler" @onclick="ToggleNavMenu"> <span class="navbar-toggler-icon"></span> </button> </div> <div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> <ul class="nav flex-column"> <li class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="bi bi-house-door" aria-hidden="true"></span> Home </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="Admin/Blogposts"> <span class="bi bi-signpost-2" aria-hidden="true"></span> Blog posts </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="Admin/Tags"> <span class="bi bi-tags" aria-hidden="true"></span> Tags </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="Admin/Categories"> <span class="bi bi-collection" aria-hidden="true"></span> Categories </NavLink> </li> </ul> </div>
我们更改了文件中的链接和图标。
让博客看起来像博客
管理界面已经完成(至少目前如此),我们应该关注我们博客的首页。首页应该有博客文章的标题和一些描述。对于我的博客,我把第一段作为一个引子,所以这也是我们可以在这里做的事情:
-
在
MyBlog.Shared项目中,打开Pages/index.razor文件。 -
我们不再需要虚假的博文,所以让我们移除页面顶部的按钮,以及
<br/>标签。 -
移除
AddSomePosts方法。现在,当我们有一个管理员,我们可以创建我们的职位。 -
To be able to get the first paragraph, we need to add the following method:
public string GetFirstParagraph(string html) { var m = System.Text.RegularExpressions. Regex.Matches(html, @"<p>(.*?)</p>",System.Text. RegularExpressions.RegexOptions.Singleline); if (m.Count>0) { return m[0].Groups[1].Value; } else { return ""; } }它使用正则表达式来查找第一个
<p>标签和第一个</p>标签之间的内容。如果没有找到段落,它将返回一个空字符串。我们可以取前 100 个字母左右,但这可能会把单词减少一半,或者让其余的帖子看起来很奇怪,因为我们缺少一个紧密的标签。
在这种情况下,我们保持这部分简单。这应该很容易,所以请确保我们的博客文章中有一个段落。
-
Inside the
Virtualizecomponent, change the content (RenderFragment) to the following:<article> <h2>@p.Title</h2> @((MarkupString)GetFirstParagraph(Markdig.Markdown. ToHtml(p.Text, pipeline))) <br /> <a href="/Post/@p.Id">Read more</a> </article>同时移除
<ul>标签。为了让这个代码生效,我们还需要添加
Markdig的代码。 -
在文件顶部为
Markdig添加一条using语句:@using Markdig; -
添加一个
OnInitializedAsync方法来处理Markdig管道的实例化(这与我们在post.razor文件中的代码相同):MarkdownPipeline pipeline; protected override Task OnInitializedAsync() { pipeline = new MarkdownPipelineBuilder() .UseEmojiAndSmiley() .Build(); return base.OnInitializedAsync(); } -
现在,使用 Ctrl + F5 运行项目,看看我们的新首页。
分享问题
分享代码的时候,有一些事情需要我们去思考。Blazor Server 的情况很好(由于数据库的低延迟),但是当我们使用一个应用编程接口时,一些问题会不时出现:
-
In the
MyBlog.Sharedproject, openPages/Admin/BlogPostEdit.razor.当我们在 Blazor 服务器上运行这个文件时,这个文件中有两个错误。我们正在循环浏览类别和标签列表;两者都可以为空。
我们应该在循环之前检查对象是否可能为空。我保留这个 bug 是为了表明在 Blazor 服务器上运行良好的东西可能在 Blazor WebAssembly 上不运行。
但是,我们应该始终执行空检查。
-
在
category循环周围添加一个空检查:@if (Categories != null) { @foreach (var category in Categories) { <option value="@category.Id">@category.Name </option> } } -
在
tag循环中添加一个空检查:@if (Tags != null) { @foreach (var tag in Tags) { <li> @tag.Name @if (Post.Tags.Any(t => t.Id == tag.Id)) { <button type="button" @onclick="@(() => { Post.Tags.Remove (Post.Tags.Single(t => t.Id == tag.Id)); })">Remove </button> } else { <button type="button" @onclick="@(() => { Post.Tags.Add(tag); })"> Add</button> } </li> } }
记住总是检查空值是很好的,但是有时这些东西会从我们身边溜走。当共享代码时,再看一遍代码总是好的。
CSS 隔离
英寸 NET 5,微软增加了一个叫做的隔离 CSS。这是许多其他框架也有的东西。想法是专门为一个组件编写 CSS。当然,好处是我们创建的 CSS 不会影响任何其他组件。
Blazor 的模板对Shared/MainLayout.razor和NavMenu.Razor使用隔离 CSS。如果您在MyBlogWebAssebly.Client项目中展开Shared/MainLayout.razor,您将看到一个名为MainLayout.razor.css的文件。
你也可以在这里通过添加一个名为MainLayout.razor.scss的文件来使用 SASS。重要的是我们添加的文件应该生成一个名为MainLayout.razor.css的文件,以便编译器拾取。
这是一个命名约定,将确保重写 CSS 和 HTML 输出。
CSS 有以下命名约定:
.main {
flex: 1;
}
它将被改写如下:
.main[b-bfl5h5967n] {
flex: 1;
}
这意味着元素需要有一个名为b-bfl5h5967n的属性(在这种情况下),以便应用样式。
在MainLayout组件中具有CSS标签的div标签将如下输出:
<div class="main" b-bfl5h5967n>
为了实现所有这些,我们还需要一个到 CSS(由模板提供)的链接,如下所示:
<link href="{Assemblyname}.styles.css" rel="stylesheet">
现在对组件库很有用。我们的共享库中有一个隔离了 CSS 的组件(NavMenu),并且NavMenu组件的 CSS 包含在{Assemblyname}.styles.css文件中。
为了包含我们共享的 CSS,我们不需要做任何额外的事情。如果您正在创建一个库供任何人使用,我会考虑使用隔离的 CSS 方法,如果您的组件需要某种 CSS 才能正常工作。
这样,我们的用户就不必添加对我们的 CSS 的引用,也没有我们的 CSS 破坏用户应用中某些东西的风险(因为它是孤立的)。我更喜欢写适合整个网站的 CSS,而不是只适合一个组件。我认为以这种方式跟踪更容易。
社区中的许多人使用相同的论点作为使用孤立 CSS 的理由(更容易跟踪)。我很喜欢它住得离组件更近的事实。我们现在有一个工作的管理界面和一个好看的网站(是的,我知道这并不完美,但现在我们知道如何处理风格)。
总结
在本章中,我们将组件移动到了一个共享库中,并将该库用于我们的 Blazor Server 和 Blazor WebAssembly 项目。
像这样使用共享库是创建共享库(供其他人使用)的方法,也是构建我们内部项目的好方法(这样很容易从 Blazor Server 更改为 Blazor WebAssembly,或者反过来)。如果您已经有了一个站点,您可以在一个共享库中构建您的 Blazor 组件,就像我们在本章中所做的那样。
通过使用组件作为您现有站点的一部分(使用 Blazor 服务器),您可以一点一点地开始使用 Blazor,直到您转换了整个东西。完成后,您可以决定是继续使用 Blazor Server(正如我提到的,我们在工作中使用 Blazor Server)还是转移到 Blazor WebAssembly。
我们还学习了如何根据平台使用依赖注入来使用不同的方式访问数据。最后但同样重要的是,我们讨论了如何在我们的站点中使用 SASS 和 CSS,包括常规 CSS 和隔离 CSS。
在下一章中,我们将了解作为 Blazor 开发人员,我们试图避免的一件事(至少我是这样的)——JavaScript。
十、JavaScript 互操作
在这一章中,我们将看一下 JavaScript。在某些场景中,我们仍然需要使用 JavaScript,或者我们希望使用依赖于 JavaScript 的现有库。Blazor 使用 JavaScript 更新文档对象模型 ( DOM ),下载文件,访问客户端本地存储等东西。
所以,现在有,将来也会有,我们需要用 JavaScript 交流,或者让 JavaScript 和我们交流的情况。别担心。Blazor 社区是一个令人惊叹的社区,所以很有可能有人已经构建了我们需要的互操作。
在本章中,我们将涵盖以下主题:
- 为什么我们需要 JavaScript?
- .NET 转换为 JavaScript
- JavaScript 到。网
- 实现现有的 JavaScript 库
技术要求
确保您已经阅读了前面的章节,或者使用Chapter09文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 10找到本章最终结果的源代码。
注意
如果您使用 GitHub 中的代码进入本章,请确保使用电子邮件注册用户,并按照说明添加用户和向数据库添加管理员角色。可以在 第八章 【认证授权】中找到说明。
我们为什么需要 JavaScript?
很多人说 Blazor 是 JavaScript 杀手,但事实是 Blazor 需要 JavaScript 才能工作。有些事件只在 JavaScript 中被触发,如果我们想使用这些事件,我们需要进行互操作。
我开玩笑地说,我从来没有像开始用 Blazor 开发时那样写过这么多 JavaScript。冷静点……没那么糟。
我已经编写了几个需要 JavaScript 才能工作的库。它们被称为Blazm.Components和Blazm.Bluetooth。
第一个是网格组件,使用 JavaScript 互操作来触发 C#代码(JavaScript to。. NET)当窗口被调整大小时,如果所有的列都不能容纳在窗口中,则删除这些列。
当这种情况被触发时,C#代码调用 JavaScript 根据客户端宽度获取列的大小,这只有网络浏览器知道,并且根据这个答案,如果需要,它会删除列。
第二个,Blazm.Bluetooth使得使用网络蓝牙与蓝牙设备交互成为可能,网络蓝牙是一种网络标准,可以通过 JavaScript 访问。
它使用双向通信;蓝牙事件可以触发 C#代码,C#代码可以迭代设备并向设备发送数据。它们都是开源的,所以如果你有兴趣看一看现实世界的项目,你可以在我的 GitHub 上查看它们:https://github.com/EngstromJimmy。
我认为在大多数情况下,我们不需要自己编写 JavaScript。Blazor 社区真的很大,所以很有可能有人已经写出了我们需要的东西。但是,我们也不需要害怕使用 JavaScript,接下来我们将看看向我们的 Blazor 项目添加 JavaScript 调用的不同方法。
.NET 转换为 JavaScript
从调用 JavaScript .NET 很简单。有两种方法可以做到这一点:
- 全局 JavaScript
- JavaScript 隔离
我们将通过这两种方式来看看有什么不同。
全局 JavaScript(老办法)
一种方法是使我们想要调用的 JavaScript 方法可以通过 JavaScript 窗口全局访问,这是一种不好的做法,因为它可以被所有脚本访问,并且可能会替换其他脚本中的功能(如果我们不小心使用了相同的名称)。
例如,我们能做的是使用作用域,在全局空间中创建一个对象,并将我们的变量和方法放在该对象上,这样我们至少可以降低一点风险。
使用范围可能如下所示:
window.myscope = {};
window.myscope.methodName = () => { ... }
我们创建一个名为myscope的对象。然后我们在那个对象上声明一个名为methodName的方法。在本例中,方法中没有代码;这只是为了展示如何做到这一点。
然后,要从 C#调用该方法,我们将使用JSRuntime这样调用它:
@inject IJSRuntime jsRuntime
await jsRuntime.InvokeVoidAsync(“myscope.methodName “);
我们可以使用两种不同的方法来调用 JavaScript:
InvokeVoidAsync,调用 JavaScript,但不期望返回值InvokeAsync<T>,调用 JavaScript,需要类型为T的返回值
如果我们愿意,我们也可以向我们的 JavaScript 方法发送参数。我们还需要参考 JavaScript,JavaScript 必须存储在wwwroot文件夹中。
另一种方法是 JavaScript Isolation,它使用这里描述的方法,但是使用模块。
JavaScript 隔离
英寸 NET 5 中,我们获得了一种使用 JavaScript Isolation 添加 JavaScript 的新方法,这是一种更好的调用 JavaScript 的方法。它不使用全局方法,也不要求我们引用 JavaScript 文件。
这对组件供应商和最终用户来说都是很棒的,因为当我们需要的时候会加载 JavaScript。它只会被加载一次(Blazor 为我们处理),我们不需要添加对 JavaScript 文件的引用,这使得启动和使用库变得更加容易。
所以,让我们实现它。
孤立的 JavaScripts 也需要存储在wwwroot文件夹中。我们不能让 JavaScript 文件很好地隐藏在组件节点下,至少不能使用内置功能。
在与 Mads Kristensen(Visual Studio 的程序经理和 Web Essentials Extension 的作者)讨论了这个问题后,他建议也许我们可以使用 Visual Studio 中的另一个功能来使它工作。
就这么办吧!
在我们的项目中,我们可以删除类别和组件。让我们实现一个简单的 JavaScript 调用来显示一个提示,以确保用户想要删除类别或标签:
-
在
MyBlog.Shared项目中,选择Components/ItemList.razor文件,按 Shift + F2 新建一个文件,并将文件命名为ItemList.razor.js。 -
Open the new file and add the following code:
export function showConfirm(message) { return confirm(message); }JavaScript 隔离使用标准的 es 模块,可以按需加载。它公开的方法只能通过该对象访问,而不能全局访问,就像旧的方式一样。
** 打开ItemList.razor,在文件顶部注入IJSRuntime:
```cs
@inject IJSRuntime jsRuntime
```
* In the `code` section, let’s add a method that will call JavaScript:
```cs
IJSObjectReference jsmodule;
private async Task<bool> ShouldDelete()
{
jsmodule = await jsRuntime. InvokeAsync<IJSObjectReference>(“import”, “/_content/ MyBlog.Shared/ItemList.razor.js”);
return await jsmodule.InvokeAsync<bool> (“showConfirm”, “Are you sure?”);
}
```
`IJSObjectReference`是对我们将进一步导入的特定脚本的引用。它可以访问我们的 JavaScript 中导出的方法,没有别的。
我们运行`Import`命令,并将文件名作为参数发送。这将运行 JavaScript 命令`let mymodule = import(“/_content/MyBlog.Shared/ItemList.razor.js”)`,并返回模块。
现在我们可以使用该模块访问我们的`showConfirm`方法并发送参数`“Are you sure?”`。
* Change the **Delete** button we have in the component to the following:
```cs
<td><button class=”btn btn-danger” @onclick=”@(async ()=>{ if (await ShouldDelete()) { await DeleteEvent.InvokeAsync(item); } })”>Delete</button></td>
```
不仅仅是调用我们的`Delete`事件回调,我们首先调用我们的新方法。让 JavaScript 确认你真的要删除,如果是,那么运行`Delete`事件回调。
但是我们还需要做一件事,我们有两个选择。我们可以将`ItemList.razor.js`移动到`wwwroot`文件夹(并保持在那里)。或者我们可以让 Visual Studio 来做,让文件靠近组件。
我更喜欢第二种选择。
在 Blazor 项目中,我们可以右键单击项目文件上的,选择**管理客户端库**,这将创建`libman.json`,但是由于这是一个库,我们需要手动创建它。
* 点击`MyBlog.Shared`,按 *Shift* + *F2* ,命名文件`libman.json`。* Replace the content in the file with the following code:
```cs
{
“version”: “1.0”,
“defaultProvider”: “filesystem”,
“libraries”: [
{
“library”: “Components”,
“files”: [
“*.js”
],
“destination”: “wwwroot/”
}
]
}
```
`libman.json`文件是库管理器的配置文件,它会将所有扩展名为`.js`的文件复制到`wwwroot`。
* 为了运行脚本,我们通过右键单击`MyBlog.Shared`项目并选择**编辑项目文件**来构建项目。* Add the following code somewhere in the file:
```cs
<ItemGroup>
<PackageReference
Include=”Microsoft.Web.LibraryManager.Build” Version=”2.1.76” />
</ItemGroup>
```
这个方法有一些*陷阱*。只有当我们对需要编译的文件进行更改时,脚本才会运行。因此,我们可能会发现自己处于这样一种境地:我们做出了改变,但改变似乎没有发生。
确保脚本运行正常。你只需要保存`libman.json`文件就可以运行了。*
*这种变通方法非常好,因为我们可以将 JavaScript 文件、Razor、CSS 和代码放在同一个地方。这种方法并非完全没有麻烦。在某些情况下,我们需要从wwwroot文件夹手动删除 JavaScript 文件。
另一种方法是将文件放在wwwroot文件夹中开始。
JavaScript 到。网
反过来呢?我认为打电话。来自 JavaScript 的. NET 代码并不是一个非常常见的场景,如果我们发现自己处于这种场景中,我们可能会想一想我们在做什么。
我认为作为 Blazor 开发者,应该尽量避免使用 JavaScript。当然,有时候 JavaScript 是唯一的选择,正如我前面提到的,Blazm 双向使用通信。
从 JavaScript 回调到有三种方法.NET 代码:
- 静态的.NET 方法调用
- 实例方法调用
- 组件实例方法调用
让我们仔细看看它们。
静止.NET 方法调用
要从 JavaScript 中调用一个. NET 函数,我们可以使函数成为静态的,我们还需要在方法中添加JSInvokable属性。
我们可以在 Razor 组件的code部分,或者在一个类中添加这样的函数:
[JSInvokable]
public static Task<int[]> ReturnArrayAsync()
{
return Task.FromResult(new int[] { 1, 2, 3 });
}
在 JavaScript 文件中,我们可以使用以下代码调用该函数:
DotNet.invokeMethodAsync(‘BlazorWebAssemblySample’, ‘ReturnArrayAsync’)
.then(data => {
data.push(4);
console.log(data);
});
DotNet对象来自Blazor.js或blazor.server.js文件。
BlazorWebAssemblySample是组件的名称,ReturnArrayAsync是静态的名称.NET 函数。
如果我们不希望函数名与方法名相同,也可以在JSInvokeable属性中指定函数的名,如下所示:
[JSInvokable(“DifferentMethodName”)]
在这个示例中,JavaScript 调用回.NET 代码,返回一个int数组。
它作为我们正在等待的 JavaScript 文件中的承诺返回,然后(使用then运算符)我们继续执行,向数组中添加一个4,然后在控制台中输出值。
实例方法调用
这个方法有点棘手;我们需要传递的一个实例.NET 对象以便能够调用它(这是Blazm.Bluetooth正在使用的方法)。
首先,我们需要一个处理方法调用的类:
using Microsoft.JSInterop;
public class HelloHelper
{
public HelloHelper(string name)
{
Name = name;
}
public string Name { get; set; }
[JSInvokable]
public string SayHello() => $”Hello, {Name}!”;
}
这是一个在构造函数中接受一个字符串(一个名称)的类,以及一个名为SayHello的方法,该方法返回一个包含“Hello,”和我们在创建实例时提供的名称的字符串。
因此,我们需要做的是创建该类的一个实例,提供一个名称,并创建DotNetObjectReference<T>,这将使 JavaScript 能够访问该实例。
但是首先,我们需要可以调用.NET 函数:
export function sayHello (hellohelperref) {
return hellohelperref.invokeMethodAsync(‘SayHello’) .then(r => console.log(r));
}
在这种情况下,我们使用导出语法,并导出一个名为sayHello的函数,该函数以名为dotnetHelper的DotNetObjectReference为例。
在这种情况下,我们调用SayHello方法,这是上的SayHello方法.NET 对象。在这种情况下,它将引用HelloHelper类的一个实例。
我们还需要调用 JavaScript 方法,我们可以从一个类或者,在这种情况下,从一个组件:
@page “/interop”
@inject IJSRuntime jsRuntime
@implements IDisposable
<button type=”button” class=”btn btn-primary” @onclick=”async ()=> { await TriggerNetInstanceMethod(); }”> Trigger .NET instance method HelloHelper.SayHello </button>
@code {
private DotNetObjectReference<HelloHelper> objRef;
IJSObjectReference jsmodule;
public async ValueTask<string>
TriggerNetInstanceMethod()
{
objRef = DotNetObjectReference.Create(new HelloHelper(“Bruce Wayne”));
jsmodule = await jsRuntime. InvokeAsync<IJSObjectReference>(“import”, “/_content/ MyBlog.Shared/Interop.razor.js”);
return await jsmodule.InvokeAsync<string>(“sayHello”, objRef);
}
public void Dispose()
{
objRef?.Dispose();
}
}
让我们复习一下这门课。我们注入IJSRuntime是因为我们需要一个来调用 JavaScript 函数。为了避免任何内存泄漏,我们还必须确保实现IDiposable,并且在文件的底部,我们确保处理掉DotNetObjectReference实例。
我们创建一个DotNetObjectReference<HelloHelper>类型的私有变量,它将包含我们对HelloHelper实例的引用。我们创建IJSObjectReference以便加载我们的 JavaScript 函数。
然后,我们创建一个引用HelloHelper类的新实例的DotNetObjectReference.Create(new HelloHelper(“Bruce Wayne”))实例,并为其提供名称“Bruce Wayne”。
现在我们有了objref,我们将把它发送给 JavaScript 方法,但是首先,我们加载 JavaScript 模块,然后我们调用JavaScriptMethod并将引用传递给我们的HelloHelper实例。现在,JavaScript sayHello方法将运行hellohelperref.invokeMethodAsync(‘SayHello’),该方法将调用SayHelloHelper,并用“Hello, Bruce Wayne”返回一个字符串。
还有两种方式我们可以用来调用.NET 函数。我们可以在组件实例上调用一个方法,在那里我们可以触发一个操作。但是,这不是推荐 Blazor 服务器使用的方法。我们也可以通过使用helper类来调用组件实例上的方法。
打电话以来.NET 是非常罕见的,我们就不讨论这两个例子了。相反,我们将深入研究在实现现有的 JavaScript 库时需要考虑的事情。
实现现有的 JavaScript 库
在我看来,最好的方法是避免移植 JavaScript 库。Blazor 需要保持 DOM 和渲染树同步,让 JavaScript 操作 DOM 可能会危及这一点。
大多数组件供应商,如 Telerik、Synfusion、Radzen,当然还有 Blazm,都有本机组件,这意味着它们不仅仅是包装一个 JavaScript 库,而是专门为 C#中的 Blazor 编写的。尽管组件在某种程度上使用了 JavaScript,但目标是将它保持在最低限度。
所以,如果你是一个库维护者,我的建议是编写一个原生的 Blazor 版本的库,将 JavaScript 保持在最低限度,最重要的是,不要让 Blazor 开发人员必须编写 JavaScript 才能使用你的组件。
一些组件将不能使用 JavaScript 实现,因为它们需要操作 DOM。
Blazor 在同步 DOM 和渲染树方面相当聪明,但是尽量避免操纵 DOM。如果我们需要使用 JavaScript 做一些事情,请确保在操作区域之外放置一个标签。Blazor 将跟踪该标签,不会考虑标签中的内容。
因为我们很早就在我的工作场所开始使用 Blazor,所以许多供应商还没有完全使用 Blazor 组件。我们需要一个快速的图形组件。在我们之前的网站上(在 Blazor 之前),我们使用了一个名为的组件。
Highcharts 不是一个免费的组件,但它可以免费用于非商业项目。当构建我们的包装时,我们有几件事想要确定。我们希望该组件以与现有组件相似的方式工作,并且希望它尽可能简单易用。
让我们回顾一下我们所做的。
首先,我们添加了对高级图表 JavaScript 的引用:
<script src=”https://code.highcharts.com/highcharts.js”></script>
然后我们添加了一个 JavaScript 文件,如下所示:
export function loadHighcharts(id, json) {
var obj = looseJsonParse(json);
Highcharts.chart(id, obj);
};
export function looseJsonParse(obj) {
return Function(‘”use strict”;return (‘ + obj + ‘)’)();
}
loadHighchart方法取div标签的id,应该转换成图表和 JSON 进行配置。
还有一种方法把 JSON 转换成 JSON 对象,这样就可以传入chart方法。
高图表剃刀组件如下所示:
@inject Microsoft.JSInterop.IJSRuntime jsruntime
<div>
<div id=”@id.ToString()”></div>
</div>
@code
{
[Parameter] public string Json { get; set; }
private string id { get; set; } = “Highchart” + Guid.NewGuid().ToString();
protected override void OnParametersSet()
{
StateHasChanged();
base.OnParametersSet();
}
IJSObjectReference jsmodule;
protected async override Task OnAfterRenderAsync(bool firstRender)
{
if (!string.IsNullOrEmpty(Json))
{
jsmodule = await jsruntime. InvokeAsync<IJSObjectReference>(“import”, “/_ content/MyBlog.Shared/HighChart.razor.js”);
await jsmodule.InvokeAsync<string> (“loadHighcharts”, new object[] { id, Json });
}
await base.OnAfterRenderAsync(firstRender);
}
}
这里需要注意的重要一点是我们有两个嵌套的div标签,一个在外部,我们希望 Blazor 跟踪,另一个在内部,Highchart 将向其中添加东西。
有一个 JSON 参数,我们传入 JSON 进行配置,然后调用我们的 JavaScript 函数。我们在OnAfterRenderAsync方法中运行我们的 JavaScript 互操作,因为否则的话,它会抛出一个异常,正如你可能从 第 4 章了解基本 Blazor 组件中回忆的那样。
现在,唯一剩下的事情就是使用组件,看起来是这样的:
@page “/HighChartTest”
<HighChart Json=”@chartjson”>
</HighChart>
@code {
string chartjson = @” {
chart: { type: ‘pie’ },
series: [{
data: [{
name: ‘Does not look like Pacman’,
color:’black’,
y: 20,
}, {
name: ‘Looks like Pacman’,
color:’yellow’,
y: 80
}]
}]
}”;
}
这个测试代码将显示一个饼图,看起来像图 10.1 :

图 10.1–图表示例
我们现在已经了解了如何获得一个 JavaScript 库来与 Blazor 一起工作,所以如果我们需要什么,这是一个选项。
如上所述,组件供应商正在投资 Blazor,因此他们有可能拥有我们需要的东西,因此我们可能不需要投入时间来创建自己的组件库。
总结
在本章中,我们学习了如何从调用 JavaScript.NET 以及调用.NET 从 JavaScript。在大多数情况下,我们不需要进行 JavaScript 调用,而且 Blazor 社区或组件供应商很可能已经为我们解决了这个问题。
我们还研究了如果需要,如何移植现有的库。
在下一章中,我们将了解状态管理。*
十一、管理状态
在本章中,我们将了解管理状态。大多数应用以某种形式管理状态。
状态只是以某种方式保存的信息。它可以是存储在数据库中的数据、会话状态,甚至是存储在网址中的东西。
用户状态存储在网络浏览器或服务器的内存中。它包含组件层次结构和最近渲染的用户界面(渲染树)。它还包含组件实例中的值或字段和属性,以及依赖注入中存储在服务实例中的数据。
如果我们进行 JavaScript 调用,我们设置的值也会存储在内存中。Blazor Server 依靠电路(SignalR 连接)保存用户状态,Blazor WebAssembly 依靠浏览器的内存。如果我们重新加载页面,电路和内存将会丢失。管理状态不是处理连接或连接问题,而是即使我们重新加载 web 浏览器,我们如何保存数据。
在页面导航或会话之间保存状态可以改善用户体验,这可能是销售与否的区别。想象一下重新加载页面,购物车里所有的商品都不见了;你很可能不会再去那里购物了。
现在想象一下,一周或一个月后回到一页,所有这些东西仍然在那里。
在本章中,我们将涵盖以下主题:
- 在服务器端存储数据
- 将数据存储在网址中
- 实现浏览器存储
- 使用内存状态容器服务
其中有些事情我们已经谈过了,甚至已经实现了。让我们借此机会回顾一下我们已经讨论过的事情,并介绍一些新技术。
技术要求
请确保您已经阅读了前面的章节,或者使用Chapter10文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 11找到本章最终结果的源代码。
注意
如果您使用 GitHub 中的代码进入本章,请确保使用电子邮件注册用户,并按照说明添加用户和向数据库添加管理员角色。可以在 第八章 【认证授权】中找到说明。
在服务器端存储数据
在服务器端存储数据有很多不同的方式。唯一要记住的是 Blazor WebAssembly 永远需要一个 API。Blazor 服务器不需要应用编程接口,因为我们可以直接访问服务器端资源。
当涉及到 API 或直接访问时,我与许多开发人员进行了讨论,这一切都归结为您打算对应用做什么。如果您正在构建一个 Blazor Server 应用,并且对迁移到 Blazor WebAssembly 没有兴趣,我可能会选择直接访问,就像我们在MyBlog项目中所做的那样。
不过,我不会在组件中直接进行数据库查询。我会把它保存在一个应用编程接口中,而不是一个网络应用编程接口。正如我们已经看到的,在一个 API 中公开那些 API 函数,就像我们在 第 7 章 、创建一个 API 和 第 9 章 、共享代码和资源中所做的那样,并不是很多步骤。如果我们愿意,我们可以从直接服务器访问开始,然后转到应用编程接口。
说到存储数据的方式,我们可以将数据保存在 Blob 存储、键值存储、关系数据库(就像我们以前做的那样)或表存储中。
可能性没有尽头。如果.NET 可以与该技术进行通信,我们将能够使用它。
在网址中存储数据
乍一看,这个选项听起来可能很可怕,但事实并非如此。在这种情况下,如果我们使用分页,数据可以是博客文章标识或页码。通常,您希望保存在网址中的内容是您希望以后能够链接到的内容,例如我们案例中的博客帖子。
要从网址中读取参数,我们使用以下语法:
@page "/post/{BlogPostId:int}"
网址是post后跟帖子的Id。
要找到那个特定的路线,BlogPostId必须是整数,否则就找不到路线。
我们还需要一个同名的public参数:
[Parameter]
public int BlogPostId{ get; set; }
如果我们将数据存储在 URL 中,我们需要确保使用OnParametersSet或OnParametersSetAsync方法,否则如果我们更改参数,数据不会重新加载。如果参数改变,Blazor 不会再运行OnInitializedAsync。
这就是为什么我们的post.razor组件加载OnParametersSet中基于 URL 中参数变化的东西,加载OnInitializedAsync中不受参数影响的东西。
我们可以通过将可选参数指定为可空来使用它们,如下所示:
@page "/post/{BlogPostId:int?}"
路线限制
当我们指定参数应该是什么类型时,这被称为路线约束。我们添加了一个约束,因此只有当参数值可以转换为我们指定的类型时,匹配才会发生。
以下约束可用:
booldatetimedecimalfloatguidintlong
网址元素将被转换成一个CLR对象。因此,在将它们添加到网址时,使用不变的区域性非常重要。
使用查询字符串
到目前为止,我们只讨论了page指令中指定的路由,但是我们也可以从查询字符串中读取数据。
NavigationManager让我们可以访问 URI,所以通过使用这个代码,我们可以访问查询字符串参数:
@inject NavigationManager Navigation
@code{
var query = new Uri(Navigation.Uri).Query;
}
我们不会深入探讨这个问题,但是现在我们知道,如果需要,可以访问查询字符串参数。
不常见的场景
有些场景可能不太常用,但我不想把它们完全排除在外,因为我已经在我的一些实现中使用过它们。我想提一下它们,以防你遇到和我一样的要求。
默认情况下,Blazor 会假设包含一个点的 URL 是一个文件,并且会尝试为用户提供一个文件(如果我们试图匹配一个路由,可能就找不到了)。
通过在Startup.cs中向 Blazor WebAssembly 服务器项目(服务器托管的 WebAssembly 项目)添加以下内容,服务器将把请求重定向到index.html文件:
endpoints.MapFallbackToFile("/example/{param?}", "index.html");
如果网址是example/some . things,它会将的请求重定向到 Blazor WebAssembly 入口点,Blazor 路由会处理它。没有它,服务器只会说文件找不到。
路由(包括网址中的一个点)将起作用,为了做到这一点,我们需要在我们的 Blazor 服务器项目中向Startup.cs添加以下内容:
endpoints.MapFallbackToPage("/example/{param?}", "/_Host");
我们在这里做同样的事情,但是不是重定向到index.html,而是重定向到_Host,这是 Blazor Server 的入口点。另一个不常见的场景是处理将捕获所有内容的路由。
简而言之,我们正在捕获一个具有多个文件夹边界的 URL,但是我们将它们作为一个参数来捕获:
@page "/catch-all/{*pageRoute}"
@code {
[Parameter]
public string PageRoute{ get; set; }
}
前面的代码将捕获"/catch-all/OMG/Racoons/are/awesome",pageRoute参数将包含"OMG/Racoons/are/awesome"。
当我创建自己的博客时,我使用了这两种技术,以便能够保留旧的网址,并使它们工作,即使其他一切(包括网址)都被重写了。
在网址中有数据并不是真正存储数据,我们总是必须确保将数据包含在网址中。如果我们想存储不需要每次都包含在网址中的数据,我们可以使用浏览器存储来代替。
实现浏览器存储
浏览器有一堆不同的方式在网络浏览器中存储数据。根据我们使用的类型,它们会有不同的处理方式。本地存储的范围是用户的浏览器窗口。如果用户重新加载页面,甚至关闭网络浏览器,数据仍然会被保存。
数据也在选项卡间共享。会话存储范围为浏览器选项卡,如果重新加载选项卡,数据将被保存,但如果关闭选项卡,数据将丢失。SessionsStorage在某种程度上使用起来更安全,因为我们避免了由于多个选项卡在存储中操作相同的值而可能出现的错误风险。
为了能够访问浏览器存储,我们需要使用 JavaScript。幸运的是,我们不需要自己编写代码。
英寸 NET 5,微软推出受保护的浏览器存储,在 ASP.NET Core 使用数据保护,在 WebAssembly 中没有。然而,我们可以使用一个名为Blazored.LocalStorage的开源库,它可以被 Blazor Server 和 Blazor WebAssembly 使用。
但是,我们是来学习新事物的,对吗?
因此,让我们实现一个接口,这样我们就可以在我们的应用中使用这两个版本,这取决于我们使用的主机模型。
创建界面
首先,我们需要一个可以读写存储的接口:
-
在我的博客里。共享项目,右键单击项目名称,选择添加 | 新文件夹。命名文件夹
Interfaces。 -
选择新文件夹,按 Shift + F2 创建新类,并命名文件
IBrowserStorage.cs。 -
用以下代码替换文件中的内容:
using System.Threading.Tasks; namespace MyBlog.Shared.Interfaces { public interface IBrowserStorage { Task<T>GetAsync<T>(string key); Task SetAsync(string key,object value); Task DeleteAsync(string key); } }
现在我们有了包含get、set和delete方法的界面。
实现 Blazor 服务器
对于 Blazor 服务器,我们将使用受保护的浏览器存储:
-
右键点击我的博客服务器项目,选择添加 | 新文件夹。命名文件夹
Services。 -
Select the folder and press Shift+ F2. Name the file
MyBlogProtectedBrowserStorage.cs.(我意识到命名有些矫枉过正,但将它们区分开来会更容易,因为我们很快就会创建另一个。)
-
打开新文件,添加以下
using语句:using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using MyBlog.Shared.Interfaces; using System.Threading.Tasks; -
Replace the class with this one:
public class MyBlogProtectedBrowserStorage : IBrowserStorage { ProtectedSessionStorage Storage { get; set; } public MyBlogProtectedBrowserStorage (ProtectedSessionStorage storage) { Storage = storage; } public async Task DeleteAsync(string key) { await Storage.DeleteAsync(key); } public async Task<T?> GetAsync<T>(string key) { var value = await Storage.GetAsync<T>(key); if (value.Success) { return value.Value; } else { return default(T); } } public async Task SetAsync(string key, object value) { await Storage.SetAsync(key,value); } }MyBlogProtectedBrowserStorage类实现了受保护浏览器存储的IBrowserStorage接口。我们注入一个ProtectedSessionStorage实例并实现set、get和delete方法。 -
在
Startup.cs中,添加以下名称空间:using MyBlog.Shared.Interfaces; using MyBlogServerSide.Services; -
在
ConfigureServices方法的下方增加以下内容:services.AddScoped<IBrowserStorage,MyBlogProtectedBrowser Storage>(); -
Protected browser storage will use JavaScript to get the information, and as you may recall from Chapter 10, JavaScript Interop, we can only do those calls from
OnAfterRenderAsyncorOnAfterRender, but there is another way.JavaScript 在
OnAfterRender方法之外的地方不起作用的原因是 Blazor Server 的预渲染特性。 -
Open
Pages/_host.chtmland change therendermode from<component type="typeof(App)" render-mode="ServerPrerendered" />to<component type="typeof(App)" render-mode="Server" />.这将使我们有可能在
OnAfterRender方法之外调用 JavaScript。
我们正在配置 Blazor,以便在注入IBrowserStorage时返回MyBlogProtectedBrowserStorage的一个实例。
这和我们用 API 做是一样的。我们根据平台注入不同的实现。
实现网络组装
对于 Blazor WebAssembly,我们将使用Blazored.SessionStorage:
-
右键单击我的博客程序集下的依赖关系节点。客户端项目,选择管理 Nuget 包。
-
搜索
Blazored.SessionStorage点击安装。 -
右键单击我的博客程序集。客户端项目,选择添加 | 新文件夹。命名文件夹
Services。 -
选择新文件夹,按 Shift + F2 。命名文件
MyBlogBrowserStorage.cs。 -
Open the new file and replace the content with the following code:
using MyBlog.Shared.Interfaces; using System.Threading.Tasks; using Blazored.SessionStorage; namespace MyBlogWebAssembly.Client.Services { public class MyBlogBrowserStorage : IBrowserStorage { ISessionStorageService Storage { get; set; } public MyBlogBrowserStorage (ISessionStorageService storage) { Storage = storage; } public async Task DeleteAsync(string key) { await Storage.RemoveItemAsync(key); } public async Task<T> GetAsync<T>(string key) { return await Storage.GetItemAsync<T>(key); } public async Task SetAsync(string key, object value) { await Storage.SetItemAsync(key,value); } } }ProtectedBrowserStorage和Blazored.SessionStorage的实现非常接近。方法名称不同,但参数相同。 -
在
Program.cs文件中,添加以下名称空间:using Blazored.SessionStorage; using MyBlog.Shared.Interfaces; using MyBlogWebAssembly.Client.Services; -
在
await builder.Build().RunAsync();正上方添加以下代码:builder.Services.AddBlazoredSessionStorage( options => { options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler. Preserve; options.JsonSerializerOptions. PropertyNamingPolicy = null; }); builder.Services.AddScoped<IBrowserStorage, MyBlogBrowserStorage>();
AddBlazoredSessionStorage扩展方法将一切都连接起来,这样我们就可以开始使用浏览器会话存储了。我们还为它提供了一些配置,以便能够序列化我们的数据对象。
然后我们为IBrowserStorage添加我们的配置,就像我们对服务器做的那样,但是在这种情况下,当我们为IBrowserStorage请求依赖注入时,我们返回MyBlogBrowserStorage。
实现共享
我们还需要实现一些调用我们刚刚创建的服务的代码:
-
在我的博客里。共享项目,打开
Pages/Admin/BlogPostEdit.razor。我们将对文件进行一些更改。 -
注入
IBrowserStorage:@inject MyBlog.Shared.Interfaces.IBrowserStorage storage -
In the
OnParameterSetAsyncmethod, we loadpostifIdis notnull.在
if语句中增加else子句:else { var saved = await storage.GetAsync<BlogPost> ("EditCurrentPost"); if (saved != null) { Post = saved; } }当我们加载一个文件并且
Id为空时,这意味着我们正在编辑一个新文件,然后我们可以检查我们是否有一个文件保存在浏览器存储中。该实现在草稿中只能有一个文件,并且只保存新帖子。如果我们编辑一篇已有的文章,它将不会保存那些更改。如果我们不为 Blazor 服务器将
render模式更改为Server,这一部分将会中断。下面是用 prerender 处理受保护浏览器存储的更多信息:https://docs . Microsoft . com/en-us/aspnet/core/blazor/state-management?视图=aspnetcore-5.0 &枢轴=服务器#句柄-预渲染。
-
We need our
UpdateHTMLmethod to become async. Change the method to look like this:protected async Task UpdateHTMLAsync() { if (Post.Text != null) { markDownAsHTML = Markdig.Markdown.ToHtml (Post.Text, pipeline); if (Post.Id == 0) { await storage.SetAsync("EditCurrentPost", Post); } } }如果博文上的
Id为0(零),我们会将帖子存储在浏览器存储中。确保将所有参考资料从UpdateHTML更改为UpdateHTMLAsync。 -
在
Models/BlogPost.cs文件中的MyBlog.Data.Shared项目中,实例化Tags集合,如下所示:public ICollection<Tag> Tags { get; set; } = new Collection<Tag>();
我们完了。现在是时候测试实现了:
-
右键点击 MyBlogServerSide ,选择设置为启动项目,按 Ctrl + F5 运行项目。
-
登录到该网站(这样我们就可以访问管理工具)。
-
点击博文,然后点击新博文。
-
在框中键入任何内容,一旦我们在文本区域键入内容,它就会将帖子保存到存储器中。
-
点击博文(这样我们就可以离开我们的博文了)。
-
点击新博文所有信息依然会在。
-
Press F12 to see the browser developer tools. Click Application | Session storage | https://localhost:5000.
您应该看到一个带有密钥
EditCurrentPost的帖子,该帖子的值应该是一个加密的字符串,如图 11.1 所示:

图 11.1–加密的受保护浏览器存储
接下来让我们测试一下 Blazor WebAssembly:
-
右键单击我的博客程序集。服务器,选择设置为启动项目,按 Ctrl + F5 运行项目。
-
登录到该网站(这样我们就可以访问管理工具)。
-
点击博文,然后点击新博文。
-
在框中键入任何内容,一旦我们在文本区域键入内容,它就会将帖子保存到存储器中。
-
点击博文(这样我们就可以离开我们的博文了)。
-
点击新博文所有信息应该还在。
-
Press F12 to see the browser developer tools. Click Application | Session storage | https://localhost:5000.
你应该看到一个带有
EditCurrentPost键的帖子,那个帖子的值应该是一个 JSON 字符串,如图图 11.2 。如果我们要改变存储中的数据,它也会在应用中改变,所以请记住,这是纯文本,最终用户可以操作数据:

图 11.2–未受保护的浏览器存储
现在,我们已经为 Blazor 服务器实现了受保护的浏览器存储,并为 Blazor WebAssembly 实现了会话存储。
我们只有一条路可以走,所以让我们让它变得最有趣。
使用内存状态容器服务
当涉及到内存中的状态容器时,我们简单地使用依赖注入将服务的实例在内存中保持预定的时间(有作用域的、单例的、瞬态的)。
在 第 4 章了解基本 Blazor 组件中,我们讨论了依赖注入的范围与 Blazor Server 和 Blazor WebAssembly 有何不同。这一部分对我们来说最大的不同是,BlazorWebAssembly 运行在 web 浏览器内部,与服务器或其他用户没有连接。
为了展示内存状态是如何工作的,我们将做一些对博客来说似乎有点过分的事情,但是看起来会有点酷。当我们编辑我们的博客文章时,我们将确保实时更新所有连接到我们博客的网络浏览器(我确实说过矫枉过正)。
我们将不得不根据主机的不同来实现这一点。让我们从 Blazor 服务器开始。
在 Blazor 服务器上实现实时更新
Blazor Server 的实现也可以用于 Blazor WebAssembly,但是由于 WebAssembly 是在我们的浏览器中运行的,它只会通知连接到站点的用户,而这个用户就是你。但是知道 Blazor Server 和 Blazor WebAssembly 中的工作方式是一样的可能会很好:
-
在
MyBlog.Shared项目中,选择Interfaces文件夹,按 Shift + F2 。命名文件IBlogNotificationService.cs。 -
Add the following code:
using MyBlog.Data.Models; using System; using System.Threading.Tasks; namespace MyBlog.Shared.Interfaces { public interface IBlogNotificationService { Action<BlogPost>BlogPostChanged{ get; set; } Task SendNotification(BlogPost post); } }当博客文章更新时,我们可以订阅一个操作,当我们更新文章时,我们可以调用一个方法。
-
In the
MyBlogServerSideproject, select theServicesfolder and press Shift+ F2. Name the fileBlazorServerBlogNotificationService.cs.给类起一个包含
BlazorServer的名字似乎没有必要,但是它确保我们可以很容易地区分这些类。用以下代码替换内容:
using MyBlog.Data.Models; using MyBlog.Shared.Interfaces; using System; using System.Threading.Tasks; namespace MyBlogServerSide.Services { public class BlazorServerBlogNotificationService : IBlogNotificationService { public Action<BlogPost>BlogPostChanged{ get; set; } public Task SendNotification(BlogPost post) { BlogPostChanged?.Invoke(post); return Task.CompletedTask; } } }这里的代码非常简单。如果我们调用
SendNotification,它将检查是否有人在监听BlogPostChanged动作,以及是否触发该动作。 -
In
Startup.csat the end ofConfigureServices, add the dependency injection:services.AddSingleton<IBlogNotificationService, BlazorServerBlogNotificationService>();每当我们请求类型为
IBlogNotificationService的实例时,我们都会得到一个BlazorServerBlogNotificationService的实例。我们添加这个依赖注入作为单例。我怎么强调都不为过。在使用 Blazor Server 时,这对于 ALL 用户来说将是相同的实例,所以我们在使用 Singleton 时一定要小心。
在这种情况下,我们希望服务通知我们博客的所有访问者博客帖子已经更改。
-
在
MyBlog.Shared项目中,打开Post.razor。 -
Add the following code at the top (or close to the top) of the page:
@using MyBlog.Shared.Interfaces @inject IBlogNotificationService notificationService @implements IDisposable我们为
IBlogNotificationService添加依赖注入,我们还需要实现IDisposable来避免任何内存泄漏。在
OnInitializedAsync方法的顶部,添加以下内容:notificationService.BlogPostChanged += PostChanged;我们在事件中添加了一个监听器,这样我们就知道什么时候应该更新信息。
-
We also need the
PostChangedmethod, so add this code:private async void PostChanged(BlogPost post) { if (BlogPost.Id == post.Id) { BlogPost = post; await InvokeAsync(()=>this.StateHasChanged()); } }如果参数与我们当前正在查看的帖子 ID 相同,那么用事件中的帖子替换内容并调用
StateHasChanged。由于这是在另一个线程上发生的,我们需要使用
InvokeAsync调用StateHasChanged,以便它在 UI 线程上运行。这个组件中的最后一件事是通过实现
Dispose方法来停止监听更新。添加以下内容:void IDisposable.Dispose() { notificationService.BlogPostChanged -= PostChanged; }我们只需移除事件侦听器。
-
打开
Pages/Admin/BlogPostEdit.Razor文件。 -
When we make changes to our blog post, we need to send a notification as well. At the top of the file, add the following:
@using MyBlog.Shared.Interfaces @inject IBlogNotificationService notificationService我们添加一个名称空间并注入我们的通知服务。
-
In the
UpdateHTMLAsyncmethod, add the following just under thePost.Text!=nullifstatement:
```cs
await notificationService.SendNotification(Post);
```
每次我们改变一些东西,它现在会发送一个通知说博客文章改变了。我确实意识到,当我们保存一篇帖子时,这样做更有意义,但它会带来一个更酷的演示。
- 右键点击
MyBlogServerSide,选择设置为启动项目,按 Ctrl + F5 运行项目。 - Copy the URL and open another web browser. We should now have two web browser windows open showing us the blog.
在第一个窗口中,打开一篇博文(不管是哪篇),在第二个窗口中,登录并编辑同一篇博文。
- 当我们在第二个窗口中更改博客文章的文本时,更改应该在第一个窗口中实时反映出来。
我一直感到惊讶的是,一个在不使用 Blazor 的情况下实现起来有点棘手的功能只需要 13 个步骤,如果我们不为下一步做准备,它将需要更少的步骤。
接下来,我们将为 Blazor WebAssembly 实现相同的功能,但是 Blazor WebAssembly 在用户的 web 浏览器内部运行。不像 Blazor 服务器那样内置实时通信。
在 Blazor WebAssembly 上实现实时更新
我们已经准备好了很多东西。我们只需要添加一个实时消息系统,由于 SignalR 既容易实现又很棒,让我们使用它。
第一次用 SignalR 的时候,我的第一个想法是,等等,不可能那么容易。我忘了什么东西,或者什么东西不见了。
让我们看看这在今天是否仍然成立:
-
右键单击我的博客程序集。服务器项目,选择添加新文件夹,命名文件夹
Hubs。 -
选择
Hubs文件夹,按 Shift + F2 。命名文件BlogNotificationHub.cs。 -
Replace the code with the following:
using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using MyBlog.Data.Models; namespace MyBlogWebAssembly.Server.Hubs { public class BlogNotificationHub : Hub { public async Task SendNotification(BlogPost post) { await Clients.All.SendAsync ("BlogPostChanged", post); } } }该类继承自
Hub类。有一种方法叫做SendNotification。记住这个名字;我们会回到那个话题。我们称之为
Clients.All.SendAsync,这意味着我们将发送一条名为BlogPostChanged的消息,内容是一篇博文。BlogPostChanged这个名字也很重要,所以也要记住。 -
In the
Startup.csfile at the top of theConfigureServicemethod, add the following:services.AddSignalR().AddJsonProtocol(options => { options.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; options.PayloadSerializerOptions.PropertyNamingPolicy = null; });这添加了 SignalR 并配置了 JSON 序列化来处理实体框架。
-
在方法底部,添加以下内容:
services.AddResponseCompression(opts => { opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/octet-stream" }); }); -
添加以下命名空间:
using MyBlogWebAssembly.Server.Hubs; -
In
app.UseEndpoints, just aboveendpoints.MapFallbackToFile("index.html");, add the following:endpoints.MapHub<BlogNotificationHub>("/BlogNotificationHub");这里我们配置
BlogNotificationHub应该使用什么 URL。在这种情况下,我们使用与集线器名称相同的网址。这里的网址也是重要。我们会用一点点。
-
在
MyBlogWebAssembly.Client项目中,右键单击依赖关系节点,选择管理 NuGet 包。 -
Search for
Microsoft.AspNetCore.SignalR.Clientand click Install. Select theServicesfolder and press Shift+ F2. Name the fileBlazorWebAssemblyBlogNotificationService.cs.在这个文件中,我们将实现信号通信。
-
添加以下名称空间:
```cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using MyBlog.Data.Models;
using MyBlog.Shared.Interfaces;
using System;
using System.Threading.Tasks;
```
- Add this class:
```cs
public class BlazorWebAssemblyBlogNotificationService: IBlogNotificationService, IAsyncDisposable
{
NavigationManager _navigationManager;
public BlazorWebAssemblyBlogNotificationService (NavigationManager navigationManager)
{
_navigationManager = navigationManager;
_hubConnection = new HubConnectionBuilder().AddJsonProtocol
(options => {
options.PayloadSerializerOptions. ReferenceHandler = System.Text.Json. Serialization.ReferenceHandler.Preserve;
options.PayloadSerializerOptions. PropertyNamingPolicy = null;
})
.WithUrl(navigationManager.ToAbsoluteUri ("/BlogNotificationHub"))
.Build();
_hubConnection.On<BlogPost> ("BlogPostChanged", (post) =>
{
BlogPostChanged?.Invoke(post);
});
_hubConnection.StartAsync();
}
private HubConnection _hubConnection;
public Action<BlogPost> BlogPostChanged { get;set; }
public async Task SendNotification(BlogPost post)
{
await _hubConnection.SendAsync ("SendNotification", post);
}
public async ValueTask DisposeAsync()
{
await _hubConnection.DisposeAsync();
}
}
```
这里发生了很多事情。该类正在实现`IBlogNotificationService`和`IAsyncDisposable`。
在构造函数中,我们使用依赖注入来获取`NavigationManager`,这样我们就可以计算出服务器的网址。
然后我们配置到集线器的连接。与服务器一样,我们需要配置 JSON 序列化来处理实体框架。然后我们指定集线器的网址;这应该与我们在*步骤 7* 中指定的相同。
现在我们可以配置集线器连接来监听事件,在这种情况下,我们监听`BlogPostChanged`事件,与我们在*步骤 3* 中指定的名称相同。当有人发送事件时,我们指定的方法将运行。
这种情况下的方法只是触发我们在`IBlogNotificationService`中的事件。然后我们开始连接。因为构造函数不能是异步的,所以我们不会等待`StartAsync`方法。
`IBlogNotificationService`也实现了`SendNotification`方法,我们只需在 hub 上触发同名事件,这将导致 hub 向所有连接的客户端发送`BlogPostChanged`事件。
我们要做的最后一件事是确保我们处理掉集线器连接。
- 在
Program.cs文件中,我们需要配置依赖注入。就在await builder.Build().RunAsync();上方,添加如下:
```cs
builder.Services.AddSingleton<IBlogNotificationService, BlazorWebAssemblyBlogNotificationService>();
```
- 添加以下命名空间:
```cs
MyBlogWebAssembly.Client.Services;
```
- Now it's time to carry out testing and we do that in the same way as for the Blazor Server project.
右键单击**我的博客程序集。服务器**,选择**设置为启动项目**,按 *Ctrl* + *F5* 运行项目。
- Copy the URL and open another web browser. We should now have two web browser windows open showing us the blog.
在第一个窗口,打开一篇博文(不管是哪篇),在第二个窗口,登录编辑同一篇博文。
- 当我们在第二个窗口中更改博客文章的文本时,更改应该在第一个窗口中实时反映出来。
在 16 个步骤中,我们实现了服务器和客户端之间的实时通信。网络浏览器中运行的代码。
而且没有 JavaScript!
总结
在本章中,我们学习了如何在应用中处理状态,以及如何使用本地存储来存储数据,包括加密数据和非加密数据。我们研究了不同的方法,并确保包含 SignalR,以便能够使用与服务器的实时通信。
几乎所有的应用都需要以某种形式保存数据。也许可以是设置或偏好。我们在这一章中介绍的东西是最常见的,但是我们也应该知道有很多开源项目可以用来保持状态。我们可以使用索引数据库保存信息。
在下一章中,我们将看一下调试。希望你不需要事先阅读那一章。
十二、调试
在本章中,我们将看一下调试。Blazor 的调试体验很好,希望你没有被困在任何地方,不得不跳到这一章。
调试代码是解决 bug、理解工作流或简单查看特定值的真正好方法。Blazor 有三种不同的调试代码的方法,我们将看看其中的每一种。
在本章中,我们将介绍以下内容:
- 让事情破裂
- 调试 Blazor 服务器
- 调试 Blazor 网络程序集
- 在浏览器中调试 Blazor WebAssembly
- 热重装(几乎是真的)
要调试什么东西,首先要让什么东西坏掉!
技术要求
确保您已经阅读了前面的章节,或者使用Chapter11文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 12找到本章最终结果的源代码。
注意
如果您使用 GitHub 中的代码进入本章,请确保使用电子邮件注册用户,并按照说明添加用户和向数据库添加管理员角色。可以在 第八章认证授权中找到说明。
让事物破碎
埃德格·迪杰斯特拉曾经说过,
“如果调试是清除软件 bug 的过程,那么编程一定是把它们放进去的过程。”
这在本节中肯定是正确的,因为我们将添加一个会引发异常的页面:
-
在
MyBlog.Shared项目中,选择Pages文件夹,按 Shift + F2 。命名新文件ThrowException.razor。 -
Replace the contents of the file with the following code block:
@page "/ThrowException" <button @onclick="@(()=> {throw new Exception("Something is broken"); })">Throw an exception</button>这个页面只显示一个按钮,当你按下按钮时,它会抛出一个异常。
太好了。我们有我们的应用的伊万德拉戈(他想打破你,但我们可能只是用一些花哨的调试击败他)。
下一步是看一下 Blazor 服务器调试。
调试 Blazor 服务器
如果您已经调试了任何类型的.NET 应用过去,你会有宾至如归的感觉。如果你没有,别担心,我们会完成的。调试 Blazor Server 正如我们可能期望的那样,并且是我们将介绍的三种不同类型的最佳调试体验。
我通常将我的 Razor 页面保存在一个共享库中,在构建我的项目时,我使用 Blazor Server 有两个原因——首先,运行项目会快一点,其次,调试体验更好。
让我们试一试!
-
右键点击 MyBlogServerSide ,点击设置为启动项目。
-
按 F5 开始项目(这次是调试)。
-
使用网络浏览器,导航至
https://localhost:5001/throwexception(端口号可能有所不同)。 -
按 F12 显示网页浏览器开发者工具。
-
在开发者工具中,点击控制台。
-
Click the Throw exception button on our page.
此时,Visual Studio 应请求焦点,并应显示异常,如图图 12.1 :
![Figure 12.1 – Exception in Visual Studio]()
图 12.1–Visual Studio 中的异常
-
按 F5 继续,切换回网页浏览器。我们现在应该能够在开发人员工具中看到异常消息,如图 12.2所示:

图 12.2–网络浏览器中的异常
正如我们在图 12.1 和图 12.2 中所看到的,我们在 Visual Studio 中调试时以及在开发人员工具中都得到了异常。
这使得在生产中的应用出现异常时很容易发现问题(打消这个念头)——这个特性已经拯救了我们很多次。
现在让我们尝试一个断点:
- 在 Visual Studio 中,打开
MyBlog.Shared/Pages/Index.razor。 - 在
LoadPosts方法的任何地方,通过点击最左边的边框(使一个红点出现)来设置断点。我们也可以通过按下 F9 来添加断点。 - 返回网络浏览器,导航至
https://localhost:5001/(端口号可能有所不同)。
Visual Studio 现在应该到达断点,通过悬停在变量上,我们应该能够看到当前值。
断点和异常调试都像我们预期的那样工作。接下来,我们将看一下调试 Blazor WebAssembly。
调试 Blazor WebAssembly
Blazor WebAssembly 当然也可以调试,但是有些事情我们需要思考。由于我们的共享库中有我们的异常页面,我们可以直接进行调试。
但是让我们从断点开始:
- 右键单击我的博客程序集。服务器,选择设置为启动项目。
- 按 F5 调试项目。
这里我们可以注意到第一个区别——假设我们仍然有在调试 Blazor 服务器部分(在LoadPosts方法中)设置的断点,断点没有被命中。
在 Blazor WebAssembly 中,断点不会在初始页面加载时被命中。我们需要导航到另一个页面,然后再次返回到索引页面。
我们不能像在 Blazor Server 中那样仅仅更改 URL,因为这将再次重新加载应用,并且不会触发断点,因为这是一个初始页面加载。
通过launchsetting.json文件中的以下代码行,可以调试 Blazor 网络程序集:
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
但是它是在我们创建项目时为我们提供的,所以我们不需要手动添加它。
如果我们愿意的话,我们也可以在我们的MyBlogWebAssembly.Server服务器项目中设置断点,它们会像我们预期的那样被命中。
现在让我们看看我们的异常会发生什么:
- 在网页浏览器中,导航至
https://localhost:5001/throwexception。 - 点击抛出异常按钮。
- 在 Visual Studio 中,未处理的异常不会被命中。我们在 web 浏览器的开发者工具中得到异常,如图图 12.3 :

图 12.3–网络组装错误
Blazor WebAssembly 中的调试体验并不像 Blazor Server 那样完美,但是已经足够完美,能够完成工作。
我们还有一个方法要探索——在网络浏览器中调试。
在网页浏览器中调试 Blazor WebAssembly
Blazor WebAssembly 的第一次调试体验是在 web 浏览器中调试的能力:
-
在 Visual Studio 中,按 Ctrl + F5 启动项目(不调试运行)。
-
In the web browser, press Shift + Alt + D.
我们将收到一条错误消息,其中包含如何在调试模式下启动 web 浏览器的说明。
我正在运行 Edge,因此启动 Edge 的方法如下所示:
msedge --remote-debugging-port=9222 --user-data-dir="C:\Users\Jimmy\AppData\Local\Temp\blazor-edge-debug" --no-first-run https://localhost:5001/复制命令。
-
按 Win + R 粘贴命令。
-
将会打开 Chrome 或 Edge 的新实例。在这个新实例中,按下Shift+Alt+D。
-
我们现在应该看到一个源代码选项卡,其中包含我们项目中的 C#代码。从这里,我们可以放置将要命中的断点,并且可以悬停在变量上。
调试界面见图 12.4:

图 12.4–浏览器内调试的屏幕截图
在浏览器中调试 C#代码是相当惊人的,但是由于我们一直在 Visual Studio 中直接调试,我个人认为这种调试没有太大的用处。
接下来,我们将看看一些可能不在调试范围内,但在开发 Blazor 应用时非常有用的东西。
热重装(几乎是真货)
和.NET 5,当我们对代码文件进行更改时,我们能够重新加载我们的 Blazor 站点。用户要求热重装,微软的目标是在 2010 年发布热重装.NET 6 时间框架。
要进行设置,请执行以下操作:
-
在 Visual Studio 中,选择工具菜单,然后选择选项。
-
选择项目和解决方案,然后选择ASP.NET Core。
-
在常规标题下的右框中,保存更改后,将自动构建和刷新选项的值更改为自动构建和刷新浏览器。
-
右键点击 MyBlogServerSide ,选择设置为启动项目。
-
现在按 Ctrl + F5 运行项目(只工作不调试)。
-
在网页浏览器中,通过在网址中添加
/counter调出计数器页面。 -
Make a change to the
Pages/Counter.razorfile and click Save.我们的网络浏览器现在应该重新加载,并且将显示更改。
这也可以从命令行运行以下命令:
dotnet watch run
不过,这种方法有几个限制:
- 它不适用于运行 ASP.NET 服务器后端的 Blazor WebAssembly(正如我们在项目中所做的那样)。为此,我们需要手动重新加载浏览器。
- 应用的状态将重新启动。
- 共享项目中的更改不会得到反映。
因此,对于我们的设置来说,这个特性并不是非常有益,但是如果我们的项目没有落入前面提到的任何限制中,这确实是很好的。
总结
在这一章中,我们研究了调试 Blazor 应用的不同方法。总会有这样的时刻,我们需要一步一步地检查代码,要么找到一个 bug,要么看看发生了什么。当这些时刻到来时,Visual Studio 会提供世界一流的功能来帮助我们实现目标。
好的一面是调试 Blazor 应用,无论是 Blazor 服务器还是 Blazor WebAssembly,都可以像您对微软产品的期望一样工作。我们得到的 C#错误(在大多数情况下)很容易理解和解决。
在下一章中,我们将看看如何测试我们的 Blazor 组件。
十三、测试
在本章中,我们将看一下测试。为我们的项目编写测试将帮助我们快速开发东西。
我们可以运行测试,并确保我们没有破坏任何与最新的变化,而且我们也不必投资我们自己的时间来测试组件,因为这一切都是由测试完成的。测试将会提高产品的质量,因为我们知道以前有效的东西仍然可以正常工作。
但是为用户界面元素编写测试并不总是那么容易;最常见的方法是旋转网站,使用点击按钮的工具,然后读取输出来确定事情是否有效。这种方法的好处是我们可以在不同的浏览器和设备上测试我们的网站。缺点是做这些测试通常需要很多时间。我们需要旋转网络,启动网络浏览器,验证测试,关闭网络浏览器,然后重复下一个测试。
我们也可以在 Blazor 中使用这种方法(和任何 ASP.NET 站点一样),但是有了 Blazor,我们就有了测试的其他机会。
史蒂夫·桑德森为 Blazor 创建了一个测试框架的胚胎,微软 MVP 埃吉尔·汉森捡起了这个胚胎,并继续开发。
Egil 的框架被称为 bUnit ,已经成为 Blazor 社区中测试 Blazor 组件的行业标准。
本章涵盖以下主题:
- 什么是 bUnit?
- 设置测试项目
- 嘲笑美国石油学会
- 写作测试
技术要求
确保您已经阅读了前面的章节,或者使用Chapter12文件夹作为起点。
你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 13找到本章最终结果的源代码。
注意
如果您使用 GitHub 中的代码进入本章,请确保使用电子邮件注册用户,并按照说明添加用户和向数据库添加管理员角色。可以在 第八章 【认证授权】中找到说明。
什么是 bUnit?
正如在介绍中提到的,一些测试旋转网络浏览器来测试页面/组件,但是 bUnit 采用了另一种方法。
bUnit 是专门为 Blazor 制造的。它可以使用 C#或 Razor 语法定义和设置测试。它可以模拟 JavaScript 互操作以及 Blazor 的身份验证和授权。为了使我们的组件更加可测试,有时我们需要从一开始就考虑这些事情,或者对我们的代码进行一些小的更改。
bUnit 不依赖于网络浏览器,而是在内部呈现输出,并将其展示给我们,以便我们可以根据预定义的输出进行测试。
是我们动手的时候了,所以让我们创建一个测试项目。
设置测试项目
为了能够做测试,我们需要一个测试项目:
-
T o 安装 bUnit 模板,打开 PowerShell,运行如下命令:
dotnet new --install bunit.template -
确保在 bUnit 网页上查看哪个是模板的最新版本:https://bunit.egilhansen.com/。
-
在 Visual Studio 中,右键单击我的解决方案,选择添加 | 新项目。
-
搜索
bUnit并在结果中选择 bUnit 测试项目,然后点击下一步。有时候找到一个模板需要时间。我们还可以将项目类型下拉列表更改为 bUnit 来查找模板。我们可能需要重新启动 Visual Studio 才能找到它。 -
命名项目
MyBlog.Shared.Tests,保持位置不变,点击下一步。 -
选择。下拉列表中的 NET 5 。
太好了。我们现在有一个测试项目。
在我们开始嘲笑这个应用编程接口之前,让我们看看我们可以使用的不同方法,这样我们就可以了解一下 bUnit 是如何工作的。
在MyBlog.Shared.Tests中,我们应该有以下三个文件:
_Imports.razor包含我们希望所有 Razor 文件都可以访问的名称空间。Counter.razor是我们在 Blazor 模板中默认获得的相同Counter组件的副本。CounterCSharpTest.cs包含用 C#编写的测试。
让我们从CounterCSharpTest.cs文件开始,它包含两个测试:一个检查计数器是否从0开始,另一个点击按钮并验证计数器现在是1。这两个简单的测试对测试Counter组件很有意义。
CounterStartsAtZero测试如下:
[Fact]
public void CounterStartsAtZero()
{
// Arrange
var cut = RenderComponent<Counter>();
// Assert that content of the paragraph shows counter
// at zero
cut.Find("p").MarkupMatches("<p>Current count: 0</p>");
}
让我们把它分解一下。Fact属性告诉测试运行人员这是一个不需要参数的正常测试。我们也可以使用Theory属性告诉测试运行人员测试方法需要参数值,但是对于这个用例,我们不需要参数。
首先,我们安排测试。简单地说,我们设置了做测试所需的一切。Egil 使用cut作为组件的名称,代表正在测试的组件。
我们在组件类型中运行RenderComponent方法和传递,在这种情况下是Counter组件。接下来,我们断言组件是否输出了正确的东西。我们使用Find方法找到第一个段落标签,然后验证 HTML 看起来像<p>Current count: 0</p>。
第二个测试稍微高级一点,看起来像这样:
[Fact]
public void ClickingButtonIncrementsCounter()
{
// Arrange
var cut = RenderComponent<Counter>();
// Act - click button to increment counter
cut.Find("button").Click();
// Assert that the counter was incremented
cut.Find("p").MarkupMatches("<p>Current count: 1</p>");
}
就像前面的测试一样,我们从渲染我们的Counter组件开始安排。下一步是在我们点击按钮的地方行动。我们寻找按钮,然后点击counter组件中的按钮。只有一个按钮,所以在这种情况下,以这种方式寻找按钮是安全的。
然后是再次断言的时候了,在这里,我们以与前面测试相同的方式检查标记,但是我们寻找1而不是0。
现在让我们运行测试,看看它们是否通过:
-
在 Visual Studio 中,通过使用 Ctrl + Q 搜索来调出测试资源管理器。我们也可以在视图 | 测试浏览器中找到。
-
Press Run all test in the view. Test Explorer should look like Figure 13.1:
![Figure 13.1 – Visual Studio Test Explorer]()
图 13.1–Visual Studio 测试资源管理器
太好了,现在我们进行了第一次测试,希望能通过。
接下来我们来看看嘲讽 API。
嘲讽 API
有种不同的方式来测试我们的应用。测试 API 超出了本书的范围,但是我们仍然需要测试组件,组件依赖于 API。我们可以加速应用编程接口,并针对应用编程接口进行测试,但是在这种情况下,我们只对测试 Blazor 组件感兴趣。
然后,我们可以模拟应用编程接口,或者创建一个不从数据库读取而是从预定义数据集读取的应用编程接口的假副本。这样,我们总是知道输出应该是什么。
幸运的是,我们为 API 创建的接口正是我们创建模拟 API 所需要的。
我们不会为项目实现 100%的测试,所以我们不必模仿所有的方法。请在这一章的最后随意对所有的方法进行测试。
我们可以通过两种方式实现模拟 API。我们可以创建一个内存数据库,但为了简单起见,我们会选择其他选项,并在需要时生成帖子:
-
在
MyBlog.Shared.Tests下,右键单击依赖关系节点,选择添加项目引用。 -
查看我的博客。分享点击确定。现在我们的测试项目可以访问共享项目中的所有类以及共享项目引用的所有类,例如
MyBlog.Data.Shared项目中的接口。 -
选择我的博客。共享测试项目。按 Shift + F2 创建一个新文件并命名文件
MyBlogApiMock.cs。 -
添加以下名称空间:
using MyBlog.Data.Interfaces; using MyBlog.Data.Models; -
Implement the
IMyBlogApiinterface; the class should look like this:public class MyBlogApiMock :IMyBlogApi { }现在我们将实现每种方法,这样我们就可以获得数据。
-
For
BlogPost, add the following code in the class:public async Task<BlogPost>GetBlogPostAsync(int id) { BlogPost post=new() { Id = id, Text = $"This is a blog post no {id}", Title = $"Blogpost {id}", PublishDate = DateTime.Now, Category = await GetCategoryAsync(1), }; post.Tags.Add(await GetTagAsync(1)); post.Tags.Add(await GetTagAsync(2)); return post; } public Task<int>GetBlogPostCountAsync() { return Task.FromResult(10); } public async Task<List<BlogPost>>GetBlogPostsAsync(int numberofposts, int startindex) { List<BlogPost> list = new(); for (int a = 0; a <numberofposts; a++) { list.Add(await GetBlogPostAsync(startindex + a)); } return list; }当我们得到一个博客帖子时,我们简单地创建一个,并用预定义的信息填充它,我们可以在以后的测试中使用。同样的事情也适用于获得博客文章的列表。
我们还说数据库里总共有
10篇博文。对于类别,添加以下代码:
public async Task<List<Category>>GetCategoriesAsync() { List<Category> list = new(); for (int a = 0; a < 10; a++) { list.Add(await GetCategoryAsync(a)); } return list; } public Task<Category>GetCategoryAsync(int id) { return Task.FromResult(new Category() { Id = id, Name = $"Category {id}" }); }在这里我们做同样的事情:我们创建名为
Category后跟一个数字的类别。 -
The same thing goes for tags; add the following code:
public Task<Tag>GetTagAsync(int id) { return Task.FromResult(new Tag() { Id = id, Name = $"Tag {id}" }); } public async Task<List<Tag>>GetTagsAsync() { List<Tag> list = new(); for (int a = 0; a < 10; a++) { list.Add(await GetTagAsync(a)); } return list; }我们不会在 API 中添加其他方法的测试。我们确实需要将它们添加到模拟类中来实现接口。添加以下方法:
public Task<BlogPost>SaveBlogPostAsync(BlogPost item) { return Task.FromResult(item); } public Task<Category>SaveCategoryAsync(Category item) { return Task.FromResult(item); } public Task<Tag>SaveTagAsync(Tag item) { return Task.FromResult(item); } public Task DeleteBlogPostAsync(BlogPost item) { return Task.CompletedTask; } public Task DeleteCategoryAsync(Category item) { return Task.CompletedTask; } public Task DeleteTagAsync(Tag item) { return Task.CompletedTask; }
我们现在有了一个模拟应用编程接口,它一遍又一遍地做同样的事情,这样我们就可以进行可靠的测试。
写作测试
是时候写一些测试了。正如我在本章前面提到的,我们不会为整个站点创建测试;如果你想的话,我们会让你稍后完成。这只是为了了解如何编写测试:
-
Right-click and select MyBlog.Shared.Tests, then select Add | New folder. Name the folder
Pages.这只是为了我们可以保留一点结构(与我们正在测试的项目相同的文件夹结构)。
-
选择
Pages文件夹。按下 Shift + F2 创建一个新的 Razor 组件并命名文件IndexTest.cs。只要记住不要将其命名为与我们正在测试的组件相同;否则,将很难确保我们测试的是正确的。 -
打开
IndexTest.cs并添加 bUnit 命名空间:using Bunit; using Microsoft.Extensions.DependencyInjection; using MyBlog.Data.Interfaces; using Xunit; -
通过添加以下代码从
TestContext继承:public class IndexTest: TestContext { } -
Now we will add the test. Add the following code:
[Fact(DisplayName ="Shows 10 blog posts")] public void Shows10Blogposts() { var cut = RenderComponent <MyBlog.Shared.Pages.Index>(); Assert.Equal(10,cut.FindAll("article").Count()); }我们给我们的测试一个显示名称,以便我们理解它的作用。测试非常简单;我们知道我们有 10 篇博文来自模拟 API。我们也知道每篇博文都呈现在一个
article标签中。我们找到所有article标签,并确保总共有 10 个。由于我们使用注入,我们需要配置依赖注入,这是我们可以在构造函数中做的事情。
-
We need to add the
IndexTestmethod:public IndexTest() { Services.AddScoped<IMyBlogApi, MyBlogApiMock>(); }这个方法将在创建类时运行,这里我们声明,如果组件请求
IMyBlogApi的实例,它将返回我们的模拟 API 的实例。这与 Blazor Server 的工作方式相同,在 Blazor Server 中,我们返回一个直接与数据库对话的 API,在 Blazor WebAssembly 中,我们返回一个与 web API 对话的 API 实例。
在这种情况下,它将返回我们的模拟 API,该 API 返回易于测试的数据。现在我们需要编写实际的测试。
-
In Visual Studio, bring up Test Explorer by searching for it using Ctrl + Q. We can also find it in View | Test Explorer.
运行我们的测试,看看我们是否得到绿灯,如图图 13.2 :
![Figure 13.2 – Test Explorer with IndexTest]()
图 13.2–带索引测试的测试资源管理器
现在我们有一个测试,测试第一个帖子和第十个帖子。考虑到我们所拥有的测试数据,假设中间的帖子按照预期呈现是可以的,但是当然也有可能进一步进行测试。
bUnit 是一个很好的测试框架,而且它是专门为 Blazor 编写的,因此它可以利用 Blazor 的强大功能,这一事实使得它的使用非常令人惊讶。
现在我们有一个简单的测试来测试我们的博客,但是 bUnit 也支持更高级的功能,例如身份验证。
认证
使用 bUnit,我们可以测试认证和授权。
然而,进行认证的并不是组件本身。我们在 第八章认证和授权中添加到app.razor的是AuthorizeRouteView,所以在单个组件中测试不会有什么不同。
但是我们可以使用AuthorizeView,例如,在我们这样的组件中:
<AuthorizeView>
<Authorized>
<strong>Authorized</strong>
</Authorized>
<NotAuthorized>
<strong>Not Authorized</strong>
</NotAuthorized>
</AuthorizeView>
我们可以使用AddTestAuthorization方法授权我们的测试,如下所示:
[Fact(DisplayName = "Shows not authorized")]
public void ShowsNotAuthorized()
{
var authContext = this.AddTestAuthorization();
var cut = RenderComponent <MyBlog.Shared.Pages.AuthorizedOrNot>();
var content = cut.Find("strong").TextContent;
Assert.Equal("Not Authorized", content);
}
这个方法增加了TestAuthorization但是没有授权。然后页面将显示文本“未授权”。为了测试用户何时被授权,我们只需将用户设置为已授权:
[Fact(DisplayName = "Shows authorized")]
public void ShowsAuthorized()
{
var authContext = this.AddTestAuthorization();
authContext.SetAuthorized("Testuser", AuthorizationState.Authorized);
var cut = RenderComponent <MyBlog.Shared.Pages.AuthorizedOrNot>();
var content = cut.Find("strong").TextContent;
Assert.Equal("Authorized", content);
}
我们可以添加声明、角色等等。我们用于测试的用户与数据库中的用户或角色无关;授权被 bUnit 嘲笑。
认证和授权可能很难测试,但是使用 bUnit,它真的很简单。更难做的事情是测试 JavaScript,但是 bUnit 对此也有很好的支持。
测试 JavaScript
bUnit 不支持测试 JavaScript,可以理解。然而,我们可以自己测试互操作。
在这本书里,我们使用了新的.NET 5 语法。在我们的MyBlog.Shared\Components\ItemList.razor组件中,我们进行一个 JavaScript 互操作来确认一个项目的删除。
JavaScript 调用如下所示:
jsmodule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "/_content/MyBlog.Shared/ItemList.razor.js");
return await jsmodule.InvokeAsync<bool>("showConfirm", "Are you sure?");
我们确保加载了 JavaScript 模块,然后执行showConfirm方法。
bUnit 中的 JavaScript 测试可以在两种不同的模式下完成–strict和loose。默认值是strict,这意味着我们需要指定每个模块和每个方法。
如果我们选择loose,所有的方法都会返回默认值。例如,对于布尔值,它将返回false。
为了测试前面的 JavaScript 调用,我们可以添加以下代码:
var moduleInterop = fixture.JSInterop.SetupModule ("/_content/MyBlog.Shared/ItemList.razor.js");
var showconfirm = moduleInterop.Setup<bool> ("showConfirm", "Are you sure?").SetResult(true);
我们建立了一个模块,其路径与之前的 JavaScript 相同,然后我们指定方法和任何in参数。
最后,我们具体说明结果应该是什么。在这种情况下,我们返回true,如果我们想删除该项目,这将是 JavaScript 的结果。
干得好!我们现在在项目中有测试。即使我们没有涵盖所有的组件,我们也应该拥有完成测试的所有构件。
总结
在这一章中,我们着眼于测试我们的应用。我们研究了如何模拟应用编程接口来进行可靠的测试。我们还介绍了如何测试 JavaScript 互操作以及身份验证。
测试可以加速我们的开发,最重要的是,可以提高我们构建的质量。使用 bUnit 结合依赖注入,可以很容易地构建测试,帮助我们测试组件。
因为我们可以单独测试每个组件,所以我们不必像许多其他测试框架那样登录、导航到站点中的特定位置,然后测试整个页面。
现在我们有了自己的站点,包含可重用组件、身份验证、API、Blazor Server 和 WebAssembly、身份验证、共享代码、JavaScript 互操作、状态管理和测试。我们只有一件事要做:运送它!
下一章 第十四章部署到生产了,该出货了。
十四、生产部署
在本章中,我们将了解在将 Blazor 应用部署到生产环境时,我们有哪些不同的选择。因为有许多不同的选择,把它们全部浏览一遍会是一本书。
我们不会详细讨论,而是涵盖我们需要考虑的不同事情,以便我们可以部署到任何提供商。
最后,部署是我们需要做的事情,以利用我们构建的东西。
在本章中,我们将介绍以下内容:
- 连续交付选项
- 部署数据库
- 托管选项
技术要求
本章是关于一般部署的,所以我们不需要任何代码。
连续交付选项
在将任何东西部署到生产中时,我们应该考虑确保消除不确定因素。例如,如果我们从自己的机器上部署,我们如何知道它是最新版本?我们如何知道我们的队友最近没有解决问题,我们的分支没有修复?说实话我们怎么知道源码控制的版本在制作上是一样的?还是生产中的版本甚至存在于源码控制中?
这就是持续集成和持续交付/部署 ( CI/CD )进入画面的地方。我们只是确保部署到生产环境中的其他东西。部署本身就是一本书,所以我们不会深入探讨这个主题。
GitHub Actions 和 Azure DevOps(或 Azure Pipelines)是微软做 CI/CD 的两种方式。还有很多,比如詹金斯、团队城市和 git lab——名单很长。如果我们目前使用的 CI/CD 系统支持部署 ASP.NET,它将能够处理 Blazor,因为最终 Blazor 只是一个 ASP.NET 站点。
如果我们有测试(我们应该有),我们还应该确保将测试设置为 CI/CD 管道的一部分。好的一点是,我们不需要添加任何特定的硬件来测试我们的组件;如果我们的 CI/CS 管道能够运行单元测试(n unit、xUnit),它将会工作。
在我们的工作设置中,当我们执行拉取请求时,我们构建并运行所有的测试。如果构建和测试通过,团队中的其他人会进行代码评审并批准变更。如果团队成员批准了变更,那么它将触发一个发布,该发布将站点部署到我们的测试环境中。我们的测试人员运行测试协议并批准变更。
冲刺结束后,测试人员将运行完整的测试协议并批准站点。然后,我们会触发另一个版本,将站点部署到生产环境中。
既然 Blazor 是 ASP.NET,没有什么能阻止我们对网站进行自动化测试。
部署数据库
说到部署我们的数据库,实体框架为我们做了很多。如果需要,我们可以让实体框架应用迁移,但是我有点控制狂。
实体框架为应用和移除变更创建代码,所以让它做它的事情应该是非常安全的。还有一个选择,那就是让实体框架生成我们自己可以应用的 SQL 脚本。
通过添加script标志,我们将获得一个可以对数据库运行的 SQL 脚本:
dotnet ef migrations script 20180904195021_InitialCreate
我们可以使用许多不同的数据库,例如微软的 SQL、MySQL,以及我们在本书中使用的 SQLite。
我们也可以选择非关系型数据库。Blazor 支持这一切,所以任何适合这个项目的东西都是我们应该使用的。
托管选项
说到托管 Blazor,有很多选择。任何能够托管 ASP.NET Core 站点的云服务都应该能够毫无问题地运行 Blazor。
有一些的事情需要我们去思考,那么我们就一个一个的来看选项。
托管 Blazor 服务器
如果云提供商有启用/禁用网络套接字的选项,我们希望启用它们,因为是 SignalR 使用的协议。根据负载的不同,我们可能希望使用像 Azure SignalR Service 这样的服务,它将负责所有的连接,并使我们的应用能够处理更多的用户。
在某些情况下,云提供商可能支持.NET Core 3.x 但不支持.NET 5 开箱即用。但不用担心;通过确保以独立的部署模式发布我们的应用,我们确保部署还添加了运行项目所需的任何文件(这可能不适用于所有宿主提供程序)。
这也是一件好事,以确保我们运行在我们期望的确切框架版本上。
托管 Blazor 网络组装
如果我们使用的是一个. NET Core 后端(就像我们为博客做的那样),我们托管的是一个. NET Core 网站,所以规则与托管 Blazor Server 相同。对于我们的博客,我们还添加了 SignalR,所以我们也需要启用 WebSockets。
在托管 Blazor WebAssembly 时,还有一些其他考虑事项,例如:
- 我们可能需要一个. NET Core 后端。
- 我们获得的数据可能是静态的,或者托管在其他地方。
在这两种情况下,我们都可以在 Azure 静态网站甚至 GitHub Pages 中托管我们的应用。
我们的博客使用身份服务器(这也是 Blazor WebAssembly 身份验证的默认实现),当我们在开发过程中运行它时,它会使用开发人员证书运行。如果我们想将使用身份服务器的站点部署到生产环境中,我们还需要创建一个证书。
在本书中,我们不打算讨论如何做到这一点或如何设置它,但值得一提的是,我们知道要寻找什么。
在 IIS 上托管
我们也可以在互联网信息服务器 ( IIS )上托管我们的应用。安装托管捆绑包,如果安装在有 IIS 的机器上,它还将确保包含 ASP.NET Core IIS 模块。
确保在服务器上启用 WebSocket 协议。
我们目前在 IIS 上运行我们的网站,并使用 Azure DevOps 来部署我们的网站。由于我们使用的是 Blazor 服务器,因此停机时间非常明显。一旦网站失去 SignalR 连接,网站将显示重新连接消息。
对于我们正在使用的站点,部署新版本时大约需要 8 到 10 秒的停机时间,这相当快。
总结
在本章中,我们讨论了为什么我们应该使用 CI/CD,因为它对确保应用的质量有着巨大的影响。我们研究了在任何支持的云提供商上运行我们的 Blazor 应用所需做的一些事情.NET 5。
部署可能是应用最重要的一步。不部署我们的应用,它只是代码。有了本章中提到的内容,例如 CI/CD、托管和部署,我们现在就可以部署代码了。
在下一章中,我们将看看我们将从这里走向何方。
十五、从此何去何从
这本书就要结束了,我想给大家留下一些自从 Blazor 预览版以来,我们在制作中运行 Blazor 时遇到的一些事情。我们还将讨论从这里到哪里去。
在本章中,我们将涵盖以下主题:
- 在生产中运行 Blazor 的经验
- 后续步骤
技术要求
在这一章中,我们没有使用我们在整本书中所写的代码。
在生产中运行 Blazor 的经验
自从 Blazor 进入预览版以来,我们就一直在生产中运行 Blazor Server。在大多数情况下,一切都没有问题。偶尔,我们会遇到一些问题,我将在本节中与您分享这些知识。
我们将查看以下内容:
- 解决记忆问题
- 解决并发问题
- 解决错误
- 旧浏览器
这些是我们遇到的一些事情,我们已经用一种对我们有用的方式解决了它们。
解决记忆问题
我们最新的升级确实增加了很多用户,随之而来的是服务器上更大的负载。服务器很好地管理内存,但是在这个版本中,后端系统有点慢,所以用户最终按下 F5 来重新加载页面。接下来发生的是电路断开,一个新的电路产生了。旧电路等待用户可能再次连接到服务器 3 分钟(默认情况下)。
用户现在有了一个新的电路,并且永远不会再连接到旧的电路,但是在 3 分钟内,用户的状态仍然会占用内存。对于大多数应用,这可能不是问题,但是我们正在将大量数据加载到内存中;数据、渲染树和所有周围的东西都将保存在内存中。
那么,我们能从中学到什么呢?Blazor 是一个单页应用。重新加载页面就像重新启动一个应用,这意味着我们应该始终确保添加从页面内部更新数据的可能性(如果这对应用有意义的话)。我们还可以确保在数据发生变化时进行更新,就像我们在第 11 章管理州中所做的那样。
在我们的例子中,我们最终向服务器添加了更多内存,然后确保用户界面中有重新加载按钮,可以在不重新加载整个页面的情况下刷新数据。最终目标是添加实时更新,当数据发生变化时,实时更新会持续更新用户界面。
如果向服务器添加更多内存不是一个选项,我们可以尝试将垃圾收集从服务器更改为桌面。那个.NET 垃圾收集有两种模式:
- 工作站模式针对在通常没有太多内存的工作站上运行进行了优化。它每秒运行多次垃圾收集。
- 服务器模式针对通常有大量内存的服务器进行了优化,它优先考虑速度,这意味着它只会每 2 秒运行一次垃圾收集器。
通过更改ServerGarbageCollection节点,可以在项目文件或runtimeconfig.json文件中设置垃圾收集器的模式:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
不过,增加内存可能是个更好的主意。
我们注意到的另一点是处理数据库上下文的重要性。在本书中,我们使用了IDbContextFactory来创建数据上下文的实例,并且在我们处理完它之后,使用Using关键字。
通过使用这个方法,它将只在短时间内可用,然后被处理掉,快速释放内存。
解决并发问题
我们经常遇到问题,数据上下文已经在使用中,无法从两个不同的线程访问数据库。
这是通过使用IDbContextFactory并在我们使用完它时处理数据上下文来解决的。
在非 Blazor 网站中,同时加载多个组件从来都不是问题(因为 web 一次只做一件事),所以 Blazor 可以同时做多件事的事实是我们在设计架构时需要考虑的。
解决错误
Blazor 通常会给我们一个很容易理解的错误,但在一些罕见的情况下,我们确实会遇到难以理解的问题。我们可以通过在Startup.cs中添加以下选项来为我们的电路(对于 Blazor 服务器)添加详细的错误:
services.AddServerSideBlazor().AddCircuitOptions(options => { options.DetailedErrors = true; });
通过这样做,我们将获得更详细的错误。然而,我不建议在生产场景中使用详细的错误。也就是说,我们已经为生产中的内部应用打开了设置,因为内部用户已经了解了它并了解了如何处理它。它让我们更容易帮助我们的用户,错误信息只在网页浏览器的开发者工具中可见,而不是面对用户。
旧浏览器
我们的一些客户在旧系统上运行旧浏览器,尽管 Blazor 支持所有主要浏览器,但这种支持并不包括真正的旧浏览器。我们最终帮助这些客户升级到 Edge 或 Chrome,只是因为我们认为他们不应该使用不再接收安全补丁的浏览器浏览网页。
就连我们家里的电视都可以运行 Blazor WebAssembly,所以旧浏览器可能不是什么大问题,但是说到浏览器支持,还是值得思考的。我们需要/想要支持哪些浏览器?
下一步
至此,我们知道了 Blazor Server 和 Blazor WebAssembly 的区别。我们知道如何创建可重用组件、制作 API、管理状态等等。但是我们从这里去哪里;接下来的步骤是什么?
社区
Blazor 社区并不像其他框架那样庞大,但它正在快速发展。许多人以博客或视频的形式与社区分享内容。YouTube 和 PluralSight 有很多教程和课程。Twitch 拥有越来越多的 Blazor 内容,但在庞大的内容目录中并不总是容易找到。
有几个资源值得一提:
-
我的博客:我的博客有很多 Blazor 的内容,还会有更多(http://engstromjimmy.se/)。
-
Blazm :我们写的 Blazm 组件库可以在这里找到(http://blazm.net/)。
-
Coding after Work: We have many episodes of our podcast and our stream covering Blazor.
下班后编码播客:http://codingafterwork.com/。
-
Awesome-Blazor :这里可以找到大量 Blazor 相关的链接和资源(https://github.com/AdrienTorris/awesome-blazor)。
-
Jeff Fritz: Jeff Fritz shares Blazor knowledge (among other things) on Twitch. He also maintains a Blazor library that helps Web Forms developers to adopt Blazor.
抽仔 : https://www .抽仔. tv/csharpfritz
github:https://github . com/fritz and friends/blazowebformomponents
组件
大多数第三方组件供应商,如 Progress Telerik、DevExpress、Syncfusion、Radzen、ComponentOne 等,都投资了 Blazor。有的花钱,有的免费。我们还可以使用许多开源组件库。
这个问题经常出现:我是 Blazor 的新手。我应该使用哪个第三方供应商?我的建议是,在投资图书馆(金钱和/或时间)之前,试着弄清楚我们需要什么。
许多供应商可以做我们需要的所有事情,但是在某些情况下,要让它工作起来需要付出更多的努力。我们开始自己开发网格组件,过了一段时间,我们决定让它开源。
布拉兹姆就是这样出生的。我们有一些特殊的需求(不是什么花哨的东西),但它要求我们必须一遍又一遍地编写大量代码,才能使它在第三方供应商组件中工作。
我们从编写我们的组件中学到了很多,这真的很容易做到。我的建议是不要总是编写自己的组件。专注于我们试图解决的实际业务问题要好得多。
对我们来说,构建一个相当高级的网格组件教会了我们很多关于 Blazor 的内部工作。
想想你需要什么,尝试不同的供应商,看看什么最适合你,也许最好自己构建组件,至少在开始时,了解更多关于 Blazor 的信息。
总结
在这一章中,我们看了我们在生产中运行 Blazor 期间遇到的一些事情。我们还讨论了从这里到哪里去。
在整本书中,我们学习了 Blazor 是如何工作的,以及如何创建基本和高级组件。我们通过身份验证和授权实现了安全性。我们创建并使用了一个连接到数据库的应用编程接口。
我们进行了 JavaScript 调用和实时更新。我们调试了我们的应用并测试了我们的代码,最后但同样重要的是,我们着眼于部署到生产中。
我们现在准备好把所有这些知识带到下一个冒险,下一个应用。我希望你读这本书和我写这本书一样开心。成为 Blazor 社区的一员非常有趣,我们每天都在学习新的东西。
感谢您阅读这本书,请保持联系。我很想了解更多关于你建造的东西!
欢迎来到 Blazor 社区!
第一部分:基础
本节的目标是让您了解项目结构,了解不同托管模型之间的差异,并简要了解 Blazor 的来源。此外,您将学习如何设置您的开发环境和创建您的第一个应用。
本节包括以下章节:
第二部分:使用 Blazor 构建应用
在本节中,您将学习 Razor 语法、验证表单、构建和共享组件、理解依赖注入,以及从 JavaScript 调用 JavaScript 和 C#等。
本节包括以下章节:
- 第三章引入实体框架核心
- 第四章了解基本 Blazor 组件
- 第五章创建高级 Blazor 组件
- 第六章建筑形式验证
- 第七章创建 API
- 第八章认证授权
- 第九章共享代码和资源
- 第十章JavaScript Interop
- 第十一章管理州
第三部分:调试、测试和部署
在本节中,您将看到如何使用客户端和服务器端 Blazor 调试应用。我们将介绍如何添加测试,以及在部署应用时应该考虑什么。
本节包括以下章节:
















浙公网安备 33010602011771号