ASP-NET-Core-和-VueJS-全-
ASP.NET Core 和 VueJS(全)
零、前言
Vue.js 3 比之前的版本更快、更小,而且 TypeScript 的全面开箱即用支持使它成为 Vue.js 的一个更易于维护、更易于使用的版本。 然后,ASP.NET Core 5,这是当今最快的。NET web 框架。 一起,Vue.js 为前端和 ASP.NET Core 5 为后台做了一个强大的组合。 这本书遵循了实践的方法来实现使用 ASP 构建健壮的应用的实际方法.NET Core 5 和 Vue.js 3。 这里的主题并不深入,本书面向的是忙碌的。net 开发人员,他们时间有限,想要用流行的库快速实现一个干净的体系结构。
你将从设置 web 应用的后端开始,在干净的架构、命令查询职责隔离(CQRS)、中介模式和实体框架 Core 5 的指导下。 然后,本书向您展示了如何使用最佳实践构建前端应用,使用 Vuex 进行状态管理,使用 Vuetify UI 组件库,使用 vueidate 进行输入验证,使用 Vue Router 进行延迟加载,以及使用 JWT 认证。 稍后,您将重点关注测试和部署,在 ASP 中执行负载测试等任务.NET Core 5 和部署容器应用到云。 本书中的所有教程都支持 Windows 10、macOS 和 Linux 用户。
在这本书的最后,你将能够构建一个企业全栈的 web 应用,使用最常见的 npm 包 Vue.js 和 NuGet 包 ASP. js。 部署 Vue.js 和 asp.net Core。 使用 GitHub 操作到 Azure 应用服务的 NET Core。
这本书是写给谁的
这本书是为那些忙碌的。net 开发人员准备的,他们想要开始使用 Vue.js 并构建真实世界的全栈企业 web 应用。 开发人员希望使用他们现有的 ASP 知识快速并实用地构建一个概念验证应用.NET Core,以及那些想要使用 TypeScript 和 c#编程语言编写可读且可维护代码的开发人员也会发现这本书很有用。 本书假设读者具有中级水平的。net 知识,并了解 c#编程、JavaScript 和 ECMAScript。
这本书的内容
第一章,ASP 入门 NET Core 和 Vue.js,作为对 ASP 当前状态的一个简要概述.NET Core 和 Vue.js 让你对 ASP 的 web 开发有一个大致的了解.NET Core 和 Vue.js。
第二章,设置一个开发环境,将教你如何设置你的计算机的开发环境来构建后端和前端 web 应用。 在继续应用开发之前,您将遍历不同的 ide 和文本编辑器来编写代码,并确保一切都已设置好。
第三章, asp.NET Core 项目,展示了创建一个 asp.NET Core 项目的一步一步的过程.NET Core 5 Web API 项目。 本章还描述了一个新创建的 ASP. conf 中的默认文件夹和文件.NET Core 5 Web API,特别是Program.cs和Start.cs,包括依赖服务和中间件。
第四章,在 ASP 中应用 Clean Architecture.NET Core 解决方案,教你真实世界中文件、文件夹、项目和 ASP 的组织.NET Core 应用依赖,为你未来的大的和可扩展的 ASP.NET Core 5 企业应用。
第五章,设置 DbContext 和控制器,将教会你如何建立一个数据库,实体框架核心,DbContext,如何编写干净的建筑实体和枚举。 本章还教你如何用 Swagger UI 编写控制器和路由来测试控制器。
第 6 章,深入 CQRS,讲述了 CQRS 模式,中介体模式,以及流行的用于 CQRS 和管道行为的 MediatR NuGet 包。
第七章,CQRS in Action,展示了如何实现 CQRS,使用 FluentValidation 和 AutoMapper,以及编写查询、命令和IServiceCollection。
第八章、ASP 中 API 版本控制与登录 NET Core,教你 API 版本控制,这对于创建可维护的 API 有时是必要的,但如果做得不正确,可能会有问题。
第九章,固位 ASP asp.net Core的集成.NET Core 5 后端与 Vue.js 3 前端。 本章探讨了 ASP 中的认证和授权.NET Core 5 Web API 通过创建和处理 JWT。 然后本章解释如何使用 JWT 构建器,编写自定义 JWT 中间件,开发基本认证,并在 GET、POST、PUT 和 DELETE 方法上添加基于角色的授权。
第 10 章,Redis 的性能增强,介绍了 ASP 的内存缓存.NET Core,分布式缓存,Redis 的实现。
第 11 章,Vue.js 基本面 Todo 应用,是完全致力于 Vue.js,节点包管理器(【显示】npm),Vue CLI。 这些工具帮助开发人员根据用户的选项为 Vue.js 项目提供不同的配置。 本章还描述了 Vue 组件的特性以及使用它们可以做什么。
第十二章,Using a UI Component Library and Creating Routes and Navigations教你如何使用由不同 Vue.js 社区构建的开源 UI 库。 您将使用 Vue.js 中流行的库之一,这将节省您花费大量时间构建组件。 然后,你将根据最佳实践来设置你的 Vue.js 3 应用的导航和路由。
第 13 章、用 ASP 集成 Vue.js 应用 NET Core,说明了如何把 ASP.NET Core Web API 和 Vue.js 应用作为一个单元。 您将了解 CORS 策略如何工作以及如何启用它。
第 14 章,与 Vuex 简化状态管理和发送 HTTP 请求,是关于发送 HTTP 请求和解决问题最常见的问题在大型 web 应用的同步组件与另一个组件的状态。 在大型和复杂的应用中,您需要一个工具来集中应用的状态,并使数据流透明和可预测。
第 15 章,在 Vue.js 中使用 Vuex发送 POST、DELETE 和 PUT HTTP 请求,展示了前端和后端同步获取、删除、创建和更新数据的步骤。 本章以最简单的方式解释了 Vue.js 3 应用的有效状态管理。
第 16 章,在 Vue.js 中添加认证,解释了 Vue.js 中认证的设置和 Auth Guard 的编写。 本章还涵盖了编写 HTTP 拦截器和设置应用中的自动登录。
第 17 章,输入验证形式,讨论了安装一个输入验证库称为 Vuelidate,解释了如何使用验证器的形式,以防止用户输入无效的输入。
第 18 章,使用 xUnit 编写集成测试*,探讨了如何有效地测试 ASP.NET Core 5 和 Vue.js 应用。 本章是在用户使用应用之前检测应用 bug 的指南。
19 章,自动部署使用 GitHub 行动和 Azure,解释了什么是 GitHub 的行动,在部署应用,以及如何实现自动部署到 Azure 应用服务使用 GitHub 动作。
为了最大限度地了解这本书
你可以安装 Node.js、VS Code、Vue CLI 和。net 5 SDK,或者在相关章节中等待进一步的说明。

请注意您的机器所需的操作系统版本和所需的软件版本。
如果你正在使用这本书的数字版本,我们建议你自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中)。 这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。
下载示例代码文件
你可以从 GitHub 上的https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js下载这本书的示例代码文件。 如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还可以在https://github.com/PacktPublishing/中找到丰富的图书和视频目录中的其他代码包。 检查出来!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含了本书中使用的屏幕截图/图表的彩色图像。 你可以在这里下载:https://static.packt-cdn.com/downloads/9781800206694_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
Code in text:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”
一段代码设置如下:
html, body, #map {
height: 100%;
margin: 0;
padding: 0
}
当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出都写如下:
$ mkdir css
$ cd css
粗体:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“从管理面板中选择系统信息。”
小贴士或重要提示
出现这样的。
联系
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送电子邮件至customercare@packtpub.com。
Errata:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上发现我们的作品以任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过copyright@packt.com与我们联系,并附上资料链接。
如果你有兴趣成为一名作家:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问authors.packtpub.com。
评论
请留下评论。 一旦你阅读和使用这本书,为什么不在你购买它的网站上留下评论? 潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以理解您对我们的产品的看法,我们的作者可以看到您对他们的书的反馈。 谢谢你!
更多关于 packt.com 的信息,请访问packt.com。*
一、ASP.NET Core 和 Vue.js 入门
首先,我想感谢你能买到这本书。 本书旨在教导繁忙的开发人员如何构建一个真实世界的全栈 web 应用,从开发到部署。 这本书是根据我多年来从我的工作室开发的一步一步的过程量身定制的。 所以,让我们开始旅程吧。
本章简要概述了 ASP 的现状.NET Core 和 Vue.js 让你对 ASP 的 web 开发有一个大致的了解.NET Core 和 Vue.js。 您还将看到 Vue.js 作为一个应用是多么的稳定和可靠,并了解编写和维护 Vue.js 框架背后的团队。
在本章中,我们将涵盖以下主题:
- 介绍 ASP.NET Core
- .NET 有什么新特性?
- 什么是新的 ASP.NET Core?
- 引入 Vue.js
技术要求
您将在这个 URL:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js找到我们将构建的应用的存储库。
每个章节都有自己的目录,每个目录有一个名为start的文件夹和一个名为finish的文件夹。
start文件夹是存储库在写入任何代码之前的状态。 finish文件夹是每一章结束时存储库的状态。
介绍 ASP.NET Core
【参考译文】 NET Core 是一个开放的源代码的 web 应用框架,来自微软,旨在快速、高性能,并可在 Windows、macOS 和 Linux 等平台上工作,用于构建现代云服务和互联网连接应用。 你可以使用跨平台的 VS Code 来构建你的应用,而不需要安装 Parallels 或 VMware 等虚拟化软件。 你只需要在另一个操作系统git clone上安装另一个 VS Code 实例,安装。net Core SDK,然后继续编写代码。
开发人员可以从 ASP 带来的较小的应用表面区域中获得好处.NET Core 的框架结构具有更强的安全性、更好的性能和更少的服务。
然而,在我们讨论 ASP 的新特性之前.NET Core 5,我们必须首先知道。NET 5 是什么。
.NET 有什么新内容?
. net 是一个开放的源代码开发平台,由 Microsoft 创建,用于构建许多不同类型的应用。
微软现在使用一个单一的框架来统一所有的。net 平台,从网页应用,移动,云,到桌面。net 5 包括 Xamarin 和它的网页组装平台,为了使它更好, 微软还能够将对Windows Presentation Foundation(WPF)和 Windows Forms 的支持转移到框架中。
请看图 1.1,它显示了新的。net 5 平台提供了一组公共的 api 来支持不同的运行时实现:

图 1.1 - .NET:统一平台
您可以使用。net 5 的相同的 api,针对不同的操作系统、应用类型和芯片架构。 加上,你可以配置或编辑您的构建配置使用您喜欢的集成开发环境(IDE)和文本编辑器可以使用 Visual Studio 等流行的 IDE, Visual Studio Mac,或骑士, 或文本编辑器(如 Visual Studio Code)或普通的旧命令行来构建应用。****
**.NET 5 的亮点是,如下:
- 它包括新的 c# 9 和 f# 5。
- 一个新的单文件发布类型,在一个二进制文件中执行你的应用。
- 在 Windows ARM64 上运行。net。
- 在 JIT 和 BCL 库中提高了 ARM64 (Linux 和 Windows)的性能。
- 减少容器映像大小,并实现新的容器 api,使。net 能够跟上容器运行时的变化。
- 它可以更容易地从
Newtonsoft.Json迁移到System.Text.Json。
现在我们可以看看 ASP 的新特性.NET Core 5。
ASP 的新特性 NET Core?
以下是添加到新 ASP 的内容的粗略列表。 核心 web 框架:
-
HTTP/2 的性能改进:. net 5 通过在 Kestrel 中添加支持 HPack 动态压缩 HTTP/2 响应头来提高 HTTP/2 的性能。
-
减少容器图像的大小:在两个图像之间共享图层将显著减少你所获取的聚合图像的大小。 这种减少是通过在 ASP 上重新镀 SDK 图像来实现的。 网络运行时的形象。
-
通过配置 Kestrel 的可重新加载端点:Kestrel 现在可以观察到传递给
KestrelServerOptions.Configure的配置的更改。 然后,它可以应用于任何新的端点,而无需重新启动应用。 -
HttpRequest 和 HttpResponse 的JSON 扩展方法:使用新的
ReadFromJsonAsync和WriteAsJsonAsync扩展方法,您现在可以轻松地使用来自HttpRequest和HttpResponse的 JSON 数据。 JSON 扩展方法也可以通过端点路由来编写,以创建 JSON api,如下所示: -
扩展方法允许匿名访问端点:
AllowAnonymous扩展在使用端点路由时允许匿名访问端点。 在下面的代码中,在调用MapGet方法后,扩展方法AllowAnonymous()被链结: -
授权失败的自定义处理:通过
AuthorizationMiddleware调用新的IAuthorizationMiddlewareResultHandler接口,授权失败的自定义处理现在比以前更容易。 现在,您可以在依赖注入容器中注册一个自定义处理程序,它允许开发人员定制 HTTP 响应。 -
SignalR 集线器过滤器:类似于中间件如何让您在 HTTP 请求之前和之后运行代码,ASP 中的集线器管道.NET SignalR 是允许您在调用 Hub 方法之前和之后运行代码的特性。
-
更新了 Blazor WebAssembly 的调试:开发 Blazor WebAssembly 应用不需要 VS Code JS 调试器扩展。
-
Blazor 可访问性改进:来自
InputBase的输入组件现在在验证失败时自动呈现aria-invalid(一个 HTML 验证属性)。 -
Blazor 性能改进:这包括优化的。net 运行时执行、JSON 序列化、JavaScript 互操作和组件渲染。
-
Kestrel 套接字传输支持其他端点类型:Kestrel 中的
System.Net.Sockets传输现在允许您绑定到 Unix 域套接字和现有的文件句柄。 -
Azure Active Directory 认证与 Microsoft.Identity.Web NET Core 项目模板现在可以很容易地与
Microsoft.Identity.Web集成,以处理 Azure AD 的认证。 -
发送 HTTP/2 PING 帧:微软增加了在 Kestrel 中发送周期性 PING 帧的能力,通过设置限制
KestrelServerOptions,即Limits.Http2.KeepAlivePingInterval和Limits.Http2.KeepAlivePingTimeout,如下代码所示:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.ConfigureKestrel(options => { options.Limits.Http2. KeepAlivePingInterval = TimeSpan .FromSeconds(10); options.Limits.Http2. KeepAlivePingTimeout = TimeSpan .FromSeconds(1); }); webBuilder.UseStartup<Startup>(); }); -
Kestrel 自定义报头解码:微软还增加了指定哪个
System.Text.Encoding用于根据报头名称来解释传入的报头,而不是默认为 UTF-8,如下所示: -
Blazor 组件的 CSS 隔离:Blazor 现在支持在组件中有范围的 CSS 样式。
-
在 Blazor WebAssembly 中延迟加载:使用
Router组件上的OnNavigateAsunc事件为特定页面延迟加载程序集。 -
Set UI focus on Blazor apps:使用
ElementReference的FocusAsync方法将 UI 焦点设置在一个元素上。 -
控制 Blazor 组件实例化:
IComponentActivator可以用来控制 Blazor 组件的实例化方式。 -
影响 Blazor 应用的 HTML 头部:使用 Blazor 应用的
head标签中内置的Title、Link、Meta组件添加动态链接和元标签。 -
受保护的浏览器存储:
ProtectedLocalStorage和ProtectedSessionStorage可用于在本地或会话存储中创建安全的持久应用状态。 -
Model binding and validation with C#9 record types: You can use
Recordtypes to model data transmitted over the network like so:public record Person([Required] string Name, [Range(0, 150)] int Age); public class PersonController { public IActionResult Index() => View(); [HttpPost] public IActionResult Index(Person person) { // ... } }您可以在公共访问修饰符之后看到
record类型。 -
对 DynamicRouteValueTransformer的改进:您现在可以将状态传递给
DynamicRouteValueTransformer并过滤选择的端点集。 -
dotnet 手表自动刷新 当您在运行
dotnet``watch时更改代码时,NET Core 项目现在将启动默认浏览器并自动刷新它。 -
控制台记录器格式化器:控制台记录器格式化器让开发人员完全控制控制台输出的格式化和着色。
-
JSON控制台日志记录器:微软增加了一个内置的 JSON 格式化器,将结构化的 JSON 日志发送到控制台。
这就是 ASP 的新特性列表.NET Core 5。 那么破坏更改呢? 在 ASP 中有什么突破性的变化吗? NET Core 5 ? 是的,让我们在下一节中检查一下。
打破从 ASP 迁移的更改。 从 asp.net Core 3.1 到 asp.net。 5.0 NET Core
如果你正计划将现有的应用或正在开发的项目从。net Core 3.1 迁移到 Core 5,你可能需要暂停一下,并阅读下面的快速更改列表。
认证
在集成 Azure 和 ASP 中有一个新行为.NET Core 来确定用户的身份。 AzureAD.UI和AzureADB2C.UIapi 和包现在在框架中已经过时了。 AzureAD.UI和AzureADB2C.UI迁移到Microsoft Authentication Library(或MSAL),该库位于Microsoft.Identity.Web下。
授权
ASP 的端点路由有一点的变化。 净的核心。 传递给授权端点的资源现在保证为HttpContext类型。 新的更改将允许开发人员从非端点路由中使用HttpContext的功能。
Azure
Azure 前缀取代了集成包中的 Microsoft 前缀。 这些软件包如下:
Microsoft.Extensions.Configuration.AzureKeyVault,开发人员使用它将 Azure Key Vault 连接到配置系统。Microsoft.AspNetCore.DataProtection.AzureKeyVault,它将 Azure Key Vault 连接到 ASP.NET Core 数据保护系统。Microsoft.AspNetCore.DataProtection.AzureStorage,它允许开发人员将 Azure Blob 存储移植到 ASP.NET Core 数据保护系统。
Blazor
微软基于浏览器的。net 应用的新框架最近有一些变化:
- 编译器将在编译期间修剪 Blazor 组件中的任何空白。 调整编译器可以提高呈现和 DOM 差异的性能,这是将虚拟 DOM 的前一个版本与虚拟 DOM 的新版本进行比较。
ProtectedBrowserStorage特性现在是 ASP 的一部分.NET Core 共享框架,为开发人员提供更好的体验。 共享框架为Microsoft.NETCore.App、Microsoft.AspNetCore.App和Microsoft.AspNetCore.All。- . net 5.0 是 Blazor Server 和 Blazor WebAssembly 项目的新目标框架,以更好地符合. net 目标框架的需求。
HTTP
在如何处理错误的 HTTP 请求异常和记录 HTTP 请求和响应方面有一些变化:
Microsoft.AspNetCore.Http.BadHttpRequestException是Microsoft.AspNetCore.Server.Kestrel.BadHttpRequestException和Microsoft.AspNetCore.Server.IIS.BadHttpRequestException的新派生类。 这些包被标记为过时的,并被设置为在未来的版本中删除,以合并重复的类型并统一跨服务器实现的包。- 代码作为整数现在是
IHttpClientFactory到日志 HTTP 的接口所使用的状态代码,而不是名称,这为开发人员查询值范围提供了更大的灵活性。
红隼
这是对 Kestrel (ASP 的跨平台 web 服务器)的的修改.NET Core:
SslProtocols.None现在是HttpsConnectionAdapterOptions.SslProtocols的默认 TLS 协议版本,而不是SslProtocols.Tls12 | SslProtocols.Tls11,默认支持 TLS 1.3 和未来版本。- 由于基于套接字的传输是 Kestrel 的默认传输方式,libuv api 现在被标记为过时的,并将在下一个版本中删除。
中间件
中间件,是处理请求和响应的管道,有一个新的行为。 DatabaseErrorPageMiddleware及其相关扩展被标记为已过时,并被DatabaseDeveloperPageExceptionFilter所取代。
SignalR
SignalR库,在应用中使用实时 web 功能,有几个变化:
- ASP.NET Core 5.0 将
MessagePack集线器协议的包版本从 1 升级。 x 2。 X,有最新的改进。 UseSignalR和UseConnections方法不再可用,因为它们的自定义逻辑不能与 ASP 中的其他路由组件交互。 净的核心。
静态文件
将静态文件text/csv直接提供给客户机应用有一个新的头值。 为了符合 RFC 7111 标准,text/csv替换application/octet-stream作为.csv文件的 Static File Middleware 的 Content-Type 头值。 您可以在https://tools.ietf.org/html/rfc7111#section-5.1找到 RFC 7111 标准的完整细节。
何时使用 ASP.NET Core
因为 ASP.NET Core 提供了一个 web 框架,可以在不同的用例场景中使用,您可以使用该框架来构建动态 web 应用。 这包括 web 应用的在线商店等企业内部应用,内容,多租户应用,内容管理系统(cms),软件即服务(SaaS【显示】),或者与 ASP 的 rest 式服务。 净的核心。 我们将专注于在 ASP 中构建 RESTful 服务.NET Core,因为这是后端,我们将在本书的第三部分中集成 Vue.js 应用。****
**ASP.NET Core 还包含管理认证、授权、数据保护、HTTPS 实施、应用秘密、XSRF/CSRF 预防、CORS 管理的功能,并使开发人员能够构建健壮而安全的 ASP.NET Core 应用。
为什么要学习 ASP ? NET Core?
除了 ASP。 asp.netCore 的性能; NET Core 是企业、保险、银行和其他类型企业的流行选择。 使用 IT JobsWatch(https://www.itjobswatch.co.uk/),您可以按 2020 年日期搜索工作。 根据 ZipRecruiter 的数据(https://www.ziprecruiter.co.uk/),自 2019 年以来,. NET Core 职位空缺趋势一直在增加,平均收入为 95,657 美元/年。
基于 Stackoverflow 的 2020 调研(https://insights.stackoverflow.com/survey/2020) NET Core 是他们最喜爱和最想要的 web 框架的赢家。 它获得了最高的票数,达到了 70.7%,这意味着这些是使用特定语言或技术进行开发并表示有兴趣继续使用它进行创建的开发人员,其次是 React、Vue 和 Express。 这些都是尝试使用 ASP 的原因。 asp.NET Core,因为工作的巨大可用性,和 ASP.NET Core 将在未来几年继续存在。
至此完成了对 ASP 的快速概述.NET Core 和什么是新的 ASP.NET Core 5。 你已经了解了 ASP 的当前状态.NET Core 以及它如何是构建高性能 RESTful 服务的正确选择。 现在是时候见见 Vue.js 了。
让我们看看为什么 Vue.js 突然成为最热门的 JavaScript 框架之一。
Vue.js 简介
Vue.js 是一个用于构建用户界面的 JavaScript 框架。 简而言之,Vue.js 为前端开发人员提供了他们想要的一切。 Vue.js 具有良好的性能、高效的大小、渐进的、对开发者友好的特点,并且如果您是前端开发新手,那么您进入 Vue.js 的障碍是最小的。
如今,Vue.js 拥有超过 130 万周活跃用户(基于 Vue.js Devtool 扩展的统计数据),每月的npm下载量超过 800 万次。
今天,Vue.js 被世界上一些最具标志性和影响力的组织使用,如苹果,IBM,微软,谷歌,耐克,维基媒体,美国宇航局,皮克斯,路易威登,L'Oréal,以及成千上万的各种规模的企业。
在本章接下来的几个章节中,我们将检查 Vue.js 核心团队在新的 Vue.js 3 中添加了什么,以及学习 Vue.js 是否值得你投入时间。
让我们找出答案。
Vue.js 有什么新内容?
经过 2 年的开发,Vue.js 核心团队终于发布了最新的 Vue.js 版本 3,代号为 One Piece。 这些改变列在这里:
- 更具可维护性:Vue.js 的代码库已经用 TypeScript 重新编写,以提高可维护性,内部也更加模块化。
- Faster:Vue.js 3 比 Vue.js 2 更快,性能更好。 新版本有一个新的基于代理的反应系统。
- small:Vue.js 有摇树功能; 摇树是一种从项目中自动删除未使用的库的方法。 这个功能对于使文件大小比以前的版本更小至关重要。 Vue.js 3 也有一些编译时标志,允许你删除不能自动摇树的内容。
- 扩展性更好:Vue.js 现在提供了 Composition API,这是一种更容易重用 Vue.js 组件逻辑片段的方法。 复合 API 是一个令人兴奋的新特性,它解决了复杂的用例,例如在组件之间共享业务逻辑。
- 更好的开发体验:对我来说,Vue.js 已经提供了无与伦比的开发体验,但是 Vue.js 在 Vue.js 3 中改进了它(通过引入新的单文件组件改进、模板表达式的类型检查和子组件的道具)。
为什么学习 Vue.js 是正确的选择?
与 Angular 和 React 一起,Vue.js 是构建现代 web 应用的三大 JavaScript 工具之一。 Vue.js 不支持技术公司,如微软或Facebook、亚马逊、苹果、Netflix、和Alphabet(FAANG)公司。 然而,通过多年的优秀工具和 Vue.js 提供的出色文档,它已经获得了全球范围内的众多赞助商(您可以在https://github.com/vuejs/vue上查看赞助商列表)。 拥有多个赞助商是件好事,因为这样 Vue.js 就可以不断地进行维护和改进。
第三方库,如 UI 库、路由库、表单、状态管理、静态站点生成器,正在变得越来越好。 因此,使 Vue.js 成为一个可靠的、值得信赖的、可靠的、稳定的、可靠的、开发者友好的构建企业应用的框架。 更不用说 Vue.js 现在有 100 多个贡献者,他们增加了新的功能,改进了,并修复了 GitHub Vue.js 上出现的所有问题。
这就结束了我们对 Vue.js 和 Vue.js 3 新特性的快速概述。 你已经了解了 Vue.js 的当前状态,以及为什么考虑 Vue.js 3 作为你的前端应用最适合开发当今的现代 web 应用。
总结
总结一下你从第一章中学到的东西,你已经学习了 ASP.NET Core 是一个开源的、跨平台的 web 框架,因其安全性和性能而受到全球企业的信任。 最重要的是,你已经学会了 ASP.NET Core 是一个经过实战测试的 web 框架,它可以让你在未来的不同业务逻辑场景中安心。
另一方面,Vue.js 是开源的,易于使用和学习,易于集成,有优秀的文档,快速,小,性能,稳定,适合任何 web 应用。 如果您为您的应用选择 Vue.js 3,那么您将永远不会出错,无论它们是大是小。
在下一章中,您将逐步学习在计算机上安装和设置开发环境所需的软件。****
二、建立开发环境
在上一章中,你学习了 ASP.NET Core 简介及其最新特性。 Vue.js 也是如此; 你已经了解了 Vue.js 及其在 Vue.js 3 中最新添加的功能。
本章将教你如何设置您的计算机的开发环境,以构建后端和前端 web 应用。 我们将使用不同的 ide 和文本编辑器来编写代码,并确保在进行应用开发之前一切都已设置好。
从头开始安装所有东西将使我们在编写代码时不受干扰。
在本章中,我们将涵盖以下主题:
- 安装 VS Code, Visual Studio 2019, VS for Mac 和 Rider
- 安装。net 5 SDK、Node.js 和
npm - 设置。net Core CLI 和 Vue CLI
- 正在安装 Postman 和 Vue DevTool
- 安装实体框架核心工具
- 安装不同的数据库提供程序
- 安装 Git 版本控制
技术要求
以下是你需要安装的软件的链接:
- 下载 Visual Studio Code(适用于 Windows, Mac 和 Linux):https://code.visualstudio.com/download
- Visual Studio 2019:https://visualstudio.microsoft.com/vs/
- :https://visualstudio.microsoft.com/vs/mac/
- 骑手:https://www.jetbrains.com/rider/
- 下载。net 5.0:https://dotnet.microsoft.com/download/dotnet/5.0
- Node.js and Node Package Manager:https://nodejs.org/en/
- 下载邮差:https://www.postman.com/downloads/
- Vue.js DevTools:https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd
- Vue.js DevTools (Firefox 浏览器插件):https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/
- DB Browser for SQLite:https://sqlitebrowser.org/dl/
- SQLiteStudioSQLite Studio:https://sqlitestudio.pl/
- Git 源码控制:https://git-scm.com/
安装和配置 VS Code, Visual Studio 2019, VS for Mac 和 Rider
本节将指导您安装和配置您所选择的 IDE 或文本编辑器。 下面是根据您的机器或操作系统可以使用的内容的快速分解。
VS Code
通过https://code.visualstudio.com/download下载 VS Code 安装程序。 我建议不管你的机器的操作系统是什么,都安装 VS Code,因为这是我在编写 JavaScript 应用时推荐的理想文本编辑器。
编辑器内置了对 TypeScript、智能感知、格式化、代码导航的支持,并且有大量的扩展供你使用。 在安装 VS Code 后添加以下基本 VS 扩展:
- 代码拼写检查:这是一个源代码的拼写检查器,它可以帮助您避免由拼写错误引起的 bug。
- Prettier:这是一个代码格式化器,它会在每次保存时重新格式化文件的代码。
- Vetur:这个扩展为 VS Code 提供了语法高亮、代码片段、emmet、linting 和编写 Vue.js 应用的智能感知等特性。
这就结束了 VS Code 的安装和配置。 你的 VS Code 文本编辑器现在已经为 Vue.js 开发设置好了。
Visual Studio 2019
当使用 c#或 f#编写应用时,这个 IDE 是每个。net 开发人员首选的。 Visual Studio 2019 有很多不同的功能来满足你的需求。 以下是 VisualStudio 2019 中强大的工具列表:
- 开发:如果你卡住了,使用智能感知来提供代码建议。
- Analyze:使用 CodeLens 查看代码、单元测试、提交历史等等的变化。
- Debug:检查 bug 的断点。
- Test:一个测试套件查看器,这样您可以轻松地导航和组织您的测试。
- 版本控制:你有两个版本控制引擎可以选择——Git,默认的版本控制和 Team Foundation。
- 协作:使用 Live Share 实时编辑和调试同事的代码。 这个特性对远程工作的开发人员很有帮助。
- Deploy:使用Publish按钮来部署 ASP 等应用.NET Core 无需离开 Visual Studio。
您应该到https://visualstudio.microsoft.com/vs/下载安装程序。 你可以选择社区版本,这是 Visual Studio 的免费版本。
下载后,运行安装程序,只安装您需要的部分.NETCore 5 开发。 选择以下选项:
- NET&web 开发
- 数据存储&处理
- .NET Core 跨平台开发
所有组件包的总文件大小约为 20 GB,因此不要对这些包感到太激动,否则您可能会在机器上安装过多的 IDE 组件。
另一件要记住的事情是,一个糟糕的互联网连接将增加安装 Visual Studio 2019 和 IDE 组件的时间。
安装完成后,创建一个控制台 App(.NET Core)项目。 请不要运行这个项目; 你只需要打开它,点击顶部导航栏上的Extensions选项卡,然后点击Manage Extensions,它就会弹出。 向下滚动并下载生产力动力工具 2017/2019。 生产力动力工具扩展将安装以下扩展,这将提高您的开发效率:
- 对齐作业:在 Visual Studio 中添加命令来格式化作业。
- 复制为 HTML:增加了以 HTML 格式复制文本到剪贴板的支持。
- 双击最大化:允许您最大化和停靠窗口标题。
- Fixed Mixed Tabs:一个可以修复混合制表符和空格的工具。
- 匹配边距:在滚动条中为插入符号下的单词的匹配绘制标记。
- 中单击滚动:允许您使用鼠标中单击按钮在编辑器窗格中滚动。
- Peek Help:在代码中显示一个小的F1-Help指南。
- Power Commands:在保存时格式化文档,清除所有窗格,折叠项目,打开包含文件夹(意味着您将在目录中看到当前文件的位置),清除最近的文件列表,等等。
- 解决方案错误可视化工具:显示并突出显示解决方案资源管理器中的错误和警告。
- 收缩空行:收缩包含数字或文本的行,在编辑器中显示更多行。
下载完Productivity Power工具后,关闭 Visual Studio 2019 以启动扩展安装。 您将看到 VSIX 安装程序正在初始化您下载的扩展; 单击Ok和Modify开始安装。
这就结束了 Visual Studio 2019 的安装和配置; 您的 Windows 机器中的 IDE 已经全部设置好了。
Visual Studio for Mac
Visual Studio for Mac是 VisualStudio 2019 for Mac 机器。 你可以使用这个 IDE 为 iOS、Android 和。net 开发应用和游戏。 下面的是你在 Visual Studio for Mac 中会注意到的主要特性:
- 你可以使用这个 IDE 来编写 c#, f#, Razor, HTML, CSS, JS, TypeScript, XAML 和 XML。
- IDE 拥有由 Roslyn (. net 编译器平台分析器)提供的高级智能感知。
- IDE 有一个功能强大的调试工具,它允许您进入和退出函数并检查代码栈状态。
- IDE 具有强大的内置重构选项,比如
extract方法和代码中的函数或方法重命名。 - Visual Studio for Mac IDE 提供了一个集成的源代码控件来管理 Git 或 SVN 存储库中的代码。
- IDE 支持主要的测试框架,如 NUnit、MSTest 和 xUnit。 该 IDE 允许您高效地运行和调试单元测试和 UI 测试。
- IDE 允许您与使用 Windows 或 macOS 的团队成员共享 c#和 f#项目。
要下载 VisualStudio for Mac,你需要去https://visualstudio.microsoft.com/vs/mac/,然后安装它。 与带有扩展的 Visual Studio 2019 不同,VisualStudio for Mac 只有很少的扩展可以安装。 不幸的是,在我看来,没有一个扩展对于开发 ASP 是有用的.NET 核心。
这一节就到此结束了。现在让我们看看如何安装 Rider。
骑手
如果您正在使用 Linux,那么 Rider 最适合您。 来自 JetBrains 的 Rider是一个跨平台的。net IDE,这意味着你可以开发。net、。net Core、Xamarin、Unity 或 ASP.NET 应用在 Windows、Mac 和 Linux 上。 JetBrains 也是 ReSharper(一个流行的 Visual Studio 扩展)的创建者。
由于 Visual Studio 2019 并不存在于基于 Linux 的操作系统中,而我们需要一个快速且强大的 IDE 来编写后端,因此 Rider 将是 Linux 机器的 IDE 候选。
Rider 中的功能与 Visual Studio 2019 提供的功能几乎相同。 你可以使用代码分析、代码编辑、重构、单元测试运行器、调试器、数据库管理工具、代码导航、准备好的前端框架和插件。
下载 Rider,请进入https://www.jetbrains.com/rider/并开始安装。 不需要配置 Rider 或添加插件,因为 Rider 功能丰富且快速。
现在,您已经安装和配置了用于编写 Vue.js 3 应用的文本编辑器和用于构建 ASP 的 IDE.NET Core 5 api。 你已经了解了在哪里可以下载 VS Code、Visual Studio IDE 和 Rider 的安装程序,以及它们应该使用哪些特定的操作系统。 你也知道应该安装哪些 Visual Studio 和 VS Code 扩展来提高开发效率。
在下一节中,你将安装。net Core SDK、Node.js 和 npm (Node Package Manager)。
安装。net SDK, Node.js 和 npm
本节将解释。net Core SDK、Node.js runtime 和npm是什么。 本节还将指导您在 Windows、Mac 和 Linux 下安装。net Core SDK 和 Node.js 运行时。
.net SDK
. net Core SDK 由一些库和工具组成,这些库和工具可以让你编写。net Core 应用。 如果您正在使用 Visual Studio 2019 和 Visual Studio Mac,你不需要安装 5 . net 运行时从 Visual Studio IDE 中包括. NET Core SDK 安装。NET Core SDK 也有。net 命令行界面和. NET Core 运行时(底层层为。net 应用运行)运行. NET Core 应用。
因此,如果您正在使用 Linux 并安装了 Rider,您可以从https://dotnet.microsoft.com/download下载并安装。net 5 SDK 开始。
Node 和 npm
Node(或 Node.js)是一个开源的跨平台运行环境,用于在浏览器外执行 JavaScript 代码。 更重要的是要知道,构建 Vue.js 是在npm的帮助下构建应用。
npm或节点包管理器是一个 CLI 工具,是第三方 JavaScript 库的注册表,可以添加到节点应用中。 因此,对于您希望在应用中包含的任何功能,我相信可以从npm注册表中使用开源库或模块。
这里,我将 Node.js 和npm的安装结合起来,因为安装 Node.js 也会自动安装npm。 因此,转到https://nodejs.org/en/下载安装程序。 请下载并安装LTS 版本,这是长期支持的缩写,而不是当前版本,因为一些云服务和npm库只使用 LTS 版本,这将不能使您的应用与它们的兼容。
最后,对于 Linux 用户,你可以在你的终端中运行以下命令:
sudo apt install nodejs
它会在你的机器上安装 Node.js 的最新 LTS 版本。 在 Linux 中也有其他安装 Node.js 的方法,但我发现这比其他方法更简单。
现在你已经了解了。net Core SDK 是什么,以及从哪里获取。net Core SDK 5(最新版本)。 你能够理解什么是 Node.js 运行时以及npm是如何工作的。 您也有安装它们的经验。
现在,我们将继续下一节,设置. net Core 命令行界面和 Vue 命令行界面。
.NET Core CLI 和 Vue CLI 设置
. net CoreCLI 是用于编写、构建、运行和发布. net Core 应用的跨平台命令行接口。 . net Core SDK 安装还会在后台安装。net Core CLI。
另一方面,Vue CLI,在中,是开发 Vue.js 应用的标准工具。 在创建项目和添加第三方库(如 Vue.js UI 库和 Vue.js 状态管理库)时,您将使用 Vue CLI。 要安装 Vue.js CLI,只需运行npm install -g @vue/cli命令,该命令将在您的机器上全局安装 Vue.js CLI。
您将能够在第 10 章、中使用 Vue CLI增强 Redis 的性能。
现在,您已经了解了. net Core CLI 和 Vue CLI 是什么,以及它们如何加快开发速度,并且已经体验了它们的安装过程。
在下一节中,您将安装 Postman(用于测试 api 的工具)和 Vue DevTool(用于 Vue.js 应用的浏览器扩展)。
安装 Postman 和 Vue DevTool
Postman是一个 API 开发平台。 无论何时构建 RESTful 服务,Postman 都是向正在编写的 api 发送 HTTP 请求以查看 api 控制器行为的优秀工具。 当你在第 9 章中开始编写 api 时,你将学习如何使用它.NET Core。
切换到 VueDevTool,这个工具通过在 DevTool 中提供一个用户界面来帮助你调试你的应用,在这里你可以查看你的 Vuex 商店,事件,路由,和你的 Vue 应用的性能。
- Chrome 插件图文教程https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd
- 这里是 Firefox 插件的链接:https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/。
您将使用这个动作当你开始建立你的路线在第十二章,使用 UI 组件库和创建路线和导航和【T7 Vue.js 店】第 14 章【显示】,与 Vuex 简化状态管理和发送 HTTP 请求。
现在,您知道在开发后端时使用哪个工具来测试 HTTP 请求了。 您还了解了要安装什么浏览器扩展,以及它如何帮助您在 Vue.js 应用中进行调试。
在下一节中,您将切换到数据库部分并安装 Entity Framework Core 工具。
安装实体框架核心工具
实体框架核心工具是实体框架核心的 CLI 工具。 简而言之,它使您能够运行常用的实体框架命令,如以下:
Add-Migration:添加新的迁移Drop-Database:删除数据库- 列出并获取有关
DbContext类型的信息 Script-Migrations:创建迁移 SQL 脚本Update-Database:更新数据库到最新的迁移
您可以在 Windows、Mac 或 Linux 机器上的终端上运行以下命令来全局安装此工具:
dotnet tool install --global dotnet-ef
您将在本书的第五章,设置 DbContext 和控制器中看到 EF Core CLI 工具的实际应用。
*现在您已经了解了 EF Core tools CLI 是什么以及它可以运行的命令。
在下一节中,我们将讨论安装哪个数据库提供程序、为什么要安装它以及如何安装它。
安装数据库提供程序
EF Core 通过使用 NuGet 库作为插件数据库提供者,可以使用多个不同的数据库。 你可以找到 MS SQL Server, MySQL, PostgreSQL, Oracle DB 和 SQLite 的提供商。
因为安装任何 SQL 数据库服务器都很麻烦,所以您将使用 SQLite,它也具有与在其他数据库中使用的相同的查询命令。 实体框架作为一个 ORM,处理不同数据库之间查询命令的所有兼容性。
SQLite 不需要数据库服务器来运行,可以在 Windows、Macbook 和 Linux 上轻松运行。 在学习 EF Core 和DbContext的同时,SQLite 的简单设置和可移植性非常适合您的数据库提供商。
那么你现在需要什么软件呢? 您可以从https://sqlitebrowser.org/下载安装 DB Browserfor SQLite,或者从https://sqlitestudio.pl/下载安装 SQLiteStudio,这是一个跨平台的开源浏览器。
重要提示
建议在 Docker 容器中安装数据库服务器,以便在团队中所有开发人员之间推广相同的版本和环境。 但是在 Docker 容器中使用数据库服务器超出了本书的范围。
然而,我们仍将在第 19 章、、【GitHub Actions Automatic Deployment Using GitHub Actions and Azure中探讨 Docker 和容器的更多信息。
现在我们差不多到了这一章的末尾。 接下来,我们来看看 Git 版本控制。
安装 Git 版本控制
安装 Git(一个 T0)分布式版本控制系统)将是您要做的最后一项设置。 但这并不意味着它是最不重要的。 您将需要它来保存存储库的不同版本和阶段。 Git 还可以帮助您回滚存储库的最新工作版本,如果您在代码中做出了破坏应用的更改,并且您无法再修复它们。
到http://git-scm.com/和点击屏幕上的下载按钮下载并安装 Git。
现在,您已经从最后一节中了解了 Git 版本控制、从哪里获得它、它的作用以及为什么它如此重要,让我们总结一下所有内容。
总结
至此,我们已经到了本章的末尾。 让我们回顾一下你所学到的基本知识。 你已经了解了 VS Code 文本编辑器和 Rider IDE 可以在任何操作系统上使用。 Visual Studio 2019 适用于 ASP。 asp.NET Core 开发在 Windows,而 Visual Studio 为 Mac 适合 ASP.NET 内核开发的 macOS。
您还了解了。net 5 SDK 是构建。net Core 应用所必需的。 另一方面,Node Package Manager 是一个 JavaScript 库包管理器。 除此之外,它还是一个 CLI 工具,用于创建像 Vue.js 这样的现代 JavaScript 应用。
Postman 是用于测试 REST API 调用的 API 工具,而 Vue CLI 是用于搭建 Vue.js 应用的工具。
也有不同的数据库提供商,如 MS SQL Server、MySQL、PostgreSQL、Oracle DB 和 SQLite,它们都有可用于 ASP 的 NuGet 包库.NET Core 5。
最后但并非最不重要的一点是,Git 版本控制是保存和创建代码历史记录的必备工具,您可以轻松地回滚或创建应用的新版本。
在下一章中,您将构建您的第一个 ASP.NET Core 5 项目。*
三、开始你的第一个 ASP.NET Core 项目
其他(Representational State Transfer)是一个接口系统之间使用 HTTP 获取数据并生成 HTTP 操作在所有可能的格式,比如 JSON,这是常用的格式发送和获取数据。
**在本章结束时,你将已经创建了一个 ASP.NET Core 5 Web API,了解了Program.cs文件在 asp.net 中的职责.NET Core 项目,学习了如何使用该项目的Startup.cs文件,并尝试了新的内置 API 文档.NET Core Web API。
在本章中,我们将涵盖以下主题:
- 创建一个 ASP.NET Core 项目
- 理解
Program.cs文件 - 解密
Startup.cs文件 - 开始与 Swashbuckle
技术要求
以下是完成本章所需要的内容。
- 用于构建后端:Visual Studio 2019, Visual Studio For Mac,或 Rider
- 搭建一个项目:.NET Core CLI
创建 ASP.NET Core 项目
您将在这里创建的项目还不是真实的后端 ASP.NET Core 应用,你将与 Vue.js 连接。 这样做的目的是查看在新创建的 ASP 中有哪些默认文件和文件夹。 网的核心项目。
您将使用命令行而不是 IDE 来确保其他开发人员尝试使用 ASP.NET Core 在 Windows、Mac 或 Linux 上也会得到相同的结果。
要启动该项目,请遵循以下简单说明:
-
在电脑的任何地方创建一个文件夹; 它可以在您的桌面或您的
Download目录中。 我通常把我的测试或演示应用放在Download文件夹中,以便轻松找到并删除它们。 -
将文件夹命名为
DemoProject,然后打开该文件夹。 -
Next, open your command-line terminal and navigate to the directory. There are many ways to do this efficiently. The following are the tools that I can recommend for opening a command line in a specific folder:
Windows 用户:https://hyper.is/
Mac 用户:https://hyper.is/或https://iterm2.com/,openinteral 工具:https://github.com/Ji4n1ng/OpenInTerminal。
Ubuntu Linux 用户:默认内置 openinternal。
如图图 3.1所示,在指定的文件夹上右键点击鼠标,打开我的命令行:

图 3.1 -在这个文件夹中打开 Hyper
导航到创建应用的DemoProject文件夹后,使用上一章安装的。net Core 命令行运行以下命令:
dotnet new slndotnet new webapi --name Webdotnet sln add [csproj-file-location]dotnet run Web.csproj
第一个命令- dotnet 新的 sln
第一个命令创建一个解决方案文件,该文件将跟踪和组织 Visual Studio 中的项目。 您的 IDE 将使用此解决方案文件作为不同项目的容器,例如类库、可执行应用、网站或单元测试项目。 CLI 使用您创建的文件夹的名称,即DemoProject:
dotnet new sln
在使用。net CLI 运行这个命令之后,您应该在DemoProject文件夹中看到一个DemoProject.sln文件。
第二命令- dotnet 新 webapi -name Web
第二个命令创建一个 ASP.NET Core 5 Web API 项目,包含运行应用的样板:
dotnet new webapi --name Web
这个命令有点不说明问题,它指示 CLI 创建一个新的 Web API 项目并将其命名为Web。
您应该会看到一个名为Web的文件夹,其中包含 ASP.NET Core Web API 项目称为Web,也匹配它的文件夹。
第三个命令- dotnet sln add [csproject -file-location]
第三个命令将Web项目添加到解决方案文件中。 在现实的企业开发中,经常可以看到多项目解决方案:
dotnet sln add [csproj-file-location]
dotnet sln add ./Web/Web.csproj适用于 Linux 和 Mac 用户,dotnet sln add .\Web\Web.csproj适用于 Windows 用户。
现在导航到Web目录。 你可以运行以下命令:
cd Web
如果您在 Windows 中使用本机 CMD,则cd命令将不可用。 我建议使用包含在 Windows Git 安装中的bash 终端。 bash 终端在目录树中使用正斜杠。
好的,你应该已经在路径上了-./DemoProject/Web。 现在让我们移动到下一个命令。
运行- dotnet 的最后一个命令,运行 Web.csproj
这个命令在开发应用时非常有用,它将运行源代码应用。 该命令通过在dotnet run命令前隐式运行dotnet restore来自动解析应用的 NuGet 依赖:
dotnet run Web.csproj
现在转到浏览器并输入以下 URL:https://localhost:5001/weatherforecast。 您的浏览器可能会提示您一条消息,说此站点不安全。 允许 ASP 开发证书.NET Core 创建后,单击浏览器弹出的Accept按钮。 您只需要这样做一次,因为您的浏览器将为您记住证书。
JSON 响应,由JSONviewChrome/Firefox 扩展格式化,应该在您的浏览器中可见。 不要期望每个对象的每个属性都有相同的值; controller方法在返回响应之前将这里的值随机化。 如果您对JSONView扩展感兴趣,可以从https://jsonview.com/获得。
下面的代码是 JSON 响应,你将从点击https://localhost:5001/weatherforecast使用你的浏览器:
[
{
"date": "2020-10-12T12:21:55.5119059+02:00",
"temperatureC": -19,
"temperatureF": -2,
"summary": "Scorching"
},
{
"date": "2020-10-13T12:21:55.5123846+02:00",
"temperatureC": 32,
"temperatureF": 89,
"summary": "Sweltering"
},
{
"date": "2020-10-16T12:21:55.5123876+02:00",
"temperatureC": 6,
"temperatureF": 42,
"summary": "Sweltering"
},
]
您可以通过查看WeatherForecastController.cs文件(您可以在Web/Controllers/中找到)来查看 JSON 响应是如何生成并返回到客户机的。 我们将在第 5 章,设置 DbContext 和控制器中更多地讨论控制器。
我们已经创建了一个 ASP.NET Core 5 项目使用。NET CLI。 现在,让我们转到下一节,了解Program.cs文件及其在 ASP 中的工作.NET Core Web API 项目。
了解 Program.cs 文件
Program.cs文件位于Web项目根目录下的,负责执行Main方法,作为应用的入口点并启动应用。
以下代码存在于Program.cs文件中,我们将其分解为可消化的内容:
namespace Web
{
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>();
});
}
}
如您所见,namespace是以项目的名称命名的。 接下来是Program课。 您可能会注意到,带有Main方法的Program类类似于. net 控制台应用,因为 ASP.NET Core Web API 确实是一个控制台项目,在构建 Web 应用时具有不同的依赖关系。
看看static Main方法的内部。 您可以看到,Main只是简单地调用CreateHostBuilder(args).Build().Run(),这是以下代码块:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
前面的代码使用来自Startup类的配置创建了WebHostBuilder的实例,我们将在下一节中讨论。 静态的CreateHostBuilder方法最终由Main方法构建并运行。
简化 web 应用的初始引导并不是Program.cs文件的唯一任务。 您可以设置其他配置,例如自定义日志记录、环境变量、Azure Key Vault 配置、内存中的。net 对象、Azure App 配置、命令行参数、目录文件、客户提供商等等。
现在您已经知道了Program.cs对 ASP 的贡献.NET Core 项目,我们可以继续下一节,它涉及到Startup.cs文件。
解密 Startup.cs 文件
Startup.cs文件位于 web 项目的根文件夹,在应用启动时首先执行。 文件内部的代码如下:
namespace Web
{
public class Startup
{
public Startup(IConfiguration configuration)…
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)…
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)…
}
}
让我们一个一个来看看。
您将看到的第一个代码块是Startup构造函数,它接受IConfiguration接口并将其赋值给公共IConfiguration``Configurationgetter。 Configuration属性表示每当Startup类实例化时,都会出现Configuration实参。
继续,您将发现包含在Startup.cs文件中的两个公共方法——ConfigureServices和Configure。
让我们先来看看ConfigureServices。 您会发现许多在线博客将ConfigureServices方法描述为一个位置,在该位置您可以用内置的控制反转(IoC)容器插入类。 每当我做一些研讨会和培训时,我发现我的听众很难理解。 所以,我尽量简化它。
ASP.NET Core 提供了一个内置的 IoC 容器,它可以帮助你在类的任何部分中使用服务。 我将给您一个可以使用ConfigureServices方法的用例。 假设你喜欢在你的 ASP 中使用库或 sdk,比如 GraphQL、OData 和 Identity.NET Core 应用。 你可能会去谷歌和搜索如何实现提到的库或 sdk,对吗? 你最终会进入 NuGet 包库。
在大多数情况下,您必须安装 NuGet 包,然后在ConfigureServices方法中注册这个包。 通常,你会写services.AddTheFeatureOfTheInstalledNuget。 键入services,然后是句号,您将看到通过 IDE 的智能感知特性的帮助安装的 NuGet 包的接口。
services.AddSomething是 ASP 的 IoC 部分.NET Core,在这里注入将要使用的工具的接口。 简单地说,在程序中注入依赖关系就是 IoC。 IoC 本身是一个很大的话题,所以我建议在https://auth0.com/blog/dependency-injection-in-dotnet-core/阅读更多关于它的内容,以避免我们偏离本章的主题太远。
方法还内置了 ASP 的服务或实用程序。 净的核心。 你现在可以在你的Startup.cs文件中看到一些好的例子如下:
services.AddControllers(),其中为 API 配置带有控制器的 MVC 服务。services.AddSwaggerGen(),其目的是在您的 ASP. php 中添加 API 文档。 自动 NET Core。 您将在开始使用 Swashbuckle一节了解更多信息。
以下是Startup.cs文件中ConfigureServices方法的代码块或逻辑:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Web",
Version = "v1" });
});
}
这里,ConfigureServices接受一个IServiceCollection接口,该接口公开了 ASP 中所有可用的服务。 在 NET Core 中,任何服务只需要一个调用就可以启用。 IServiceCollection启用Controllers和SwaggerGen,如您在ConfigureServices方法内部所看到的。
现在让我们检查Configure方法。
Configure方法允许将中间件添加到应用的请求管道中。 在前端应用或另一个服务到达控制器之前,您可以在此方法中修改它的 HTTP 请求。
这个过程是一个请求被您已经安装或创建的函数或组件修改。 这些功能或组件的示例是日志记录器或认证,它们的顺序可能是有序的,也可能是无序的,这取决于您为解决方案构建的逻辑。 现在让我们来看看Configure方法的代码块:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
c.SwaggerEndpoint("/swagger/v1/swagger.json",
"Web v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
这里的Configure方法带有一个IApplicationBuilder接口,该接口提供了修改或更改 ASP 的请求管道的机制.NET Core 和IWebHostEnvironment,它提供了关于 web 托管环境的信息,其中您的 ASP. NET Core 和IWebHostEnvironment NET Core 正在运行。
现在让我们检查一下这个默认的中间件.NET Core 5 提供:
UseDeveloperExceptionPage(),它显示一个用于调试应用的有用错误页面。UseSwagger()用于 API 文档。 我将在开始使用 Swashbuckle小节的内置 Swagger 集成小节中进一步讨论这个问题。UseSwaggerUI(),允许您查看 api 的一些元信息。
您可以看到if env.IsDevelopment条件只包括开发环境中的前面三个中间件。
这里是在 ASP 中其余的默认中间件.NET Core Web API:
UseHttpsRedirection():重定向 HTTP 请求到 HTTPSUseRouting():添加定义端点的路由功能UseAuthorization():启用 HTTP 请求授权UseEndpoints():定义端点并使用MapControllers()将控制器映射到各自的端点
现在我们已经讨论完里面有什么Startup.cs。 让我们转到本章的最后一个主题,在 ASP 中默认添加的最新特性.NET Core 5 OpenAPI,也被称为 Swagger。
开始使用 Swashbuckle
什么是虚张声势? Swashbuckle 是一个 NuGet 包,提供了将 Swagger 添加到 ASP 的简单方法.NET Core Web API 项目。
后端开发人员应该做的事情之一就是创建对开发人员友好的 api。 您可以通过在开发 api 时记录它们来实现这一点。 编写 api 文档对于将要使用您的 api 的开发人员来说是非常重要的,也是很有帮助的。 例如,文档消除了您和 api 消费者之间不必要的问题和答案,从而节省了所有人的时间。
在另一个场景中,您将看到 API 文档是如何充实的,想象一下在一个大团队中工作。 通过编写 api 文档,您的团队成员将有机会快速检查现有的 api,从而避免重复编写相同的 api 浪费时间和金钱。
好的,我已经提到了您可以从 API 文档中获得的主要好处。 下面是我们将要发现的一些东西。
介绍 OpenAPI 和 Swagger
OpenAPI 规范,以前称为Swagger 规范,是一个规范,用于描述与语言无关的文本、JSON 和 YAML 格式的 api 功能。
**现在我们知道 OpenAPI 是一个规范,有一个流行的工具使用它产生的 JSON 格式实现它。 这个工具也是 Swagger,但不要与 Swagger 规范混淆; 我现在谈论的 Swagger 是一种工具,它可以使您更容易地创建 API 文档,并帮助您快速集成 API。
现在让我们转到下一节,看看 ASP 的内置 Swagger 集成.NET Core 5 优惠。
内置 Swagger 集成
ASP.NET Core 5 已经正式包含了Swashbuckle包,它将 Swagger 集成到的 ASP. NET Core 5 中。 Microsoft.Extensions.DependencyInjection命名空间中的 NET Core Web API。 在搭建应用时,Swagger 会通过。net CLI 自动为你安装。你可以在Web.csproj的ItemGroup中看到它:
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore. Authentication.JwtBearer" Version="5.0.2" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore. Authentication.OpenIdConnect" Version="5.0.2" NoWarn="NU1605" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
在前面的代码中,您可以看到有一个对Swashbuckle.AspnetCore version 5.*.*的包引用。 因此,它可以随时使用。
此外,在 ASP 中启用 Swagger。 默认情况下。NET Core 5 Web API 项目。 打开Startup.cs文件,查看以下代码中的AddSwaggerGen扩展名:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {Title="Web", Version="v1" });
});
AddSwaggerGen是一个服务的扩展,可以让 Swagger 生成器构建SwaggerDocument对象。 您还可以找到SwaggerDoc扩展,它添加了一个 url 友好的名称,通过传递以下两个参数来识别文档:
"v1"new OpenApiInfo { Title = "Web", Version = "v1" });
当使用SwaggerDoc扩展时,这个v1字符串类型和OpenApiInfo对象是必需的参数。 此外,OpenApiInfo要求您向Title和Version属性中添加值,以便向 Swagger 文档中添加一些元数据。 现在让我们进入中间件部分。
在Configure方法中有两个现成的中间件:
UseSwagger():生成 OpenAPI 文档的中间件UseSwaggerUI():渲染 Swagger UI 网页
你可以在这里看到UseSwagger和UseSwaggerUI中间件的作用:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(
c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
"Web v1"));
}
不过,在UseSwaggerUI中间件中还需要添加一个。 您必须通过SwaggerEndpoint扩展在UseSwaggerUI中指定文档的路径和版本。
在下一节中,我们将了解如何打开或查看由 ASP 提供的 Swagger 文档样本。 净的核心。
Swagger 文档和运行中的 Swagger UI
在查看了Startup.cs文件的代码之后,让我们检查一下 Swagger 文档和 Swagger UI。
我们将运行应用来查看 Swagger 文档和 Swagger UI。 我们还将测试 API,以确定它向我们这样的开发人员显示的用户界面。 请依次执行以下步骤完成本节操作:
-
检查您的 ASP.NET Core 5 仍在运行。 如果您的项目没有运行,请在
Web文件夹中运行以下命令:dotnet run Web.csproj -
Head to the following URL,
https://localhost:5001/swagger/v1/swagger.json, in your browser. As shown in Figure 3.2, you will see the generated Swagger JSON document describing theWeatherForecastendpoint:![Figure 3.2 – Swagger documentation]()
图 3.2 - Swagger 文档
如果您注意到前面截图中的,那么 URL 版本与 Swagger 文档版本、OpenAPI 版本、标题以及
OpenApiInfo对象定义的版本相匹配。您还将在 ASP 中找到单个默认控制器.NET Core 5 项目
WeatherForecast,以及它的GET方法。 -
Now, navigate to the Swagger UI: https://localhost:5001/swagger/index.html. The Swagger UI in Figure 3.3 is the interactive web form of the Swagger documentation. You should see on your screen the same image as Figure 3.3:
![Figure 3.3 – Swagger UI]()
图 3.3 - Swagger UI
Swagger UI 允许开发人员发现控制器的细节和控制器的模式。 在前面的截图中,你只看到一个控制器和一个模型,因为这就是 ASP.NET Core 5 为您提供了开箱即用的。
-
Click the
GETmethod to see its info. Your screen should look like this:![Figure 3.4 – Swagger UI]()
图 3.4 - Swagger UI
屏幕截图描述了所选 HTTP 方法的参数、响应码、媒体类型、示例值、模式等信息。 您将注意到,媒体类型默认设置为文本/普通。 如果需要,可以将其更改为application/json。
-
Click the Try it out button to show the UI to test the
GETmethod:![Figure 3.5 – Swagger UI]()
图 3.5 - Swagger UI
点击Try it out按钮后,会出现宽的Execute按钮,正如在图 3.5中所看到的。 我们可以尝试 HTTP
GET方法以查看它是否命中端点并使用200进行响应。 -
Click the Execute button, and then observe the UI:
![Figure 3.6 – Swagger UI]()
图 3.6 - Swagger UI
单击Execute按钮后,您应该看到一个与图 3.6相似的屏幕。 您可以看到 curl CLI,这是图像顶部区域中用于 URL 操作的工具,这意味着您可以通过在终端上运行
curl命令来点击端点,并将看到相同的响应。您还可以在
curl命令之后找到准确的请求 URL 或端点,对于服务器响应,您将在图像上发现 JSON 格式的响应体。 -
在这一部分中,除了滚动之外,您不需要做任何事情。 在 UI 中向下滚动查看其余响应。 您还可以在这里找到响应头,它告诉您内容类型、数据和服务器。
-
Now let's move down to the Swagger UI schemas to investigate what we can find there. Click the WeatherForecast dropdown, as shown in Figure 3.7. You should see that the properties of
WeatherForecastare the same as the image, which aredate string,temperatureC integer,temperatureF integer, andsummary string:![Figure 3.7 – Swagger UI]()
图 3.7 - Swagger UI
-
最后,回到你的 ASP。
WeatherForecast类在WeatherForecast.cs文件中:public class WeatherForecast { public DateTime Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } } -
将
WeatherForecast类的形状与 Swagger UI 中的WeatherForecast模式进行比较。 它们应该是一样的。
以上就是我们对来自Swashbuckle包的 Swagger 文档和 Swagger UI 的讨论。 现在,让我们总结一下本章所学的内容。
总结
通过这些,您已经学会了如何轻松地创建 ASP.NET Core 5 项目,没有 IDE。
您已经了解了Program.cs文件的工作方式、执行主方法和启动应用。 您还发现了Startup.cs文件的职责:通过内置的依赖注入来启用服务,并按顺序收集所有运行的中间件。
最后,您已经看到了 ASP 的新特性.NET Core Web API 项目。
ASP。 当您创建 Web API 项目时,NET Core 5 会立即为您设置 Swagger 文档。 您已经看到了 Swagger 文档在帮助开发人员查看现有 api 并检查其详细信息方面的重要性。
在下一章中,我们将创建另一个。net 解决方案和项目,并记住清晰的体系结构。****
四、在 ASP.NET Core 解决方案中应用干净的架构
您还记得前面的章节,在这一章中,您将把干净的体系结构应用到 ASP.NET Core 5 解决方案。 本章教你真实世界中文件、文件夹、项目和 ASP 的组织.NET Core 应用依赖,为你未来的大的和可扩展的 ASP.NET Core 5 企业应用。
组织您的代码、文件和文件夹可以帮助其他开发人员理解您的代码,重构它,并添加未来的特性,这在现实世界的应用中经常发生。
我们将涵盖以下议题:
- 引入清洁架构
- 芯层
- 基础设施层
- 表示层
- 管理
Tests文件夹 - 构建一个干净的体系结构解决方案
技术要求
以下是你完成本章所需要的东西:
- 用于构建后端:Visual Studio 2019, Visual Studio For Mac,或 Rider
- 搭建一个项目:.NET Core CLI
- Bash 终端为 Mac 和 Linux 用户
- Windows 用户使用 PowerShell 或 Git Bash 终端
以下是本章已完成存储库的链接:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter04。
介绍干净的架构
那么,洁净建筑到底是什么? 简而言之,干净的架构是一种组织软件架构的原则,可以让开发人员在未来避免对应用进行困难的重构。 清晰的体系结构可以帮助您为特定的领域模型构建服务,从而为微服务体系结构做好准备。
这是一个简洁的 ASP 架构图。 网络应用:

图 4.1 -清晰的架构图
在干净的建筑中,两个层必须是结构的核心或中心。 这些层就是域层,它包含了您的大多数实体、枚举和设置。 应用层保留了您的数据传输对象(dto)、接口、映射、异常、行为和业务逻辑的大部分。
区别在于,企业逻辑可以在多个系统之间共享,而应用逻辑或业务逻辑是特定的。 现在,我们不再让核心依赖于数据访问和基础设施,而是将这些依赖关系倒置。 因此,表示和基础设施依赖于核心,但核心不依赖于任何一层。 这个结构是通过在应用层中添加抽象或接口来实现的,这些抽象或接口是在应用层之外的其他层中实现的。 例如,如果我们想实现存储库模式,我们将在基础设施应用和实现中添加一个IRepository接口。
按照这个设计原则,所有依赖项都指向中间。 因此,您可以看到,内核与其他层没有依赖关系。 随后,表示和基础设施依赖于核心,而不是相互依赖。 这一点非常重要,因为我们希望确保为该系统创建的逻辑保持在核心中。 例如,如果表示依赖于基础设施来发送一些通知,那么它就是逻辑。 这个逻辑现在必须出现在表示层中。 因此,它必须协调表示和基础设施之间的交互。 通常,我们不希望发生这种情况,因为我们不能重用那个逻辑。 我们想要一个至少能持续十年的系统。
事情是这样的。 如果我们有一个前端 web 应用和一个后端应用,很有可能它在 10 或 15 年内不会存在。 我们需要核心内部的逻辑,在那里它是与所有那些东西隔离的。
如果清晰架构的圆形图(参见图 4.1)有点难以理解,我们可以看看下面的插图:

图 4.2 -一个干净的架构的平面图
上面的图是简洁架构的简化版本。
此图说明您的应用核心在最底部,它是清晰的架构圆图的中心(参见图 4.1)。
正如你所见,应用核心没有依赖关系。 另一方面,UI 或表示层依赖于应用核心。 您还会注意到,项目基础设施依赖于应用核心。
最后,整个应用变得高度可测试。 您可以快速编写单元测试、集成测试和功能测试。
现在让我们把这些分解成更容易消化的内容。 我们将这些层分为三个文件夹,其中包含。net 项目:
core文件夹将有Application和Domain项目。infrastructure文件夹将有Data和Shared项目。presentation文件夹将有WebApi项目。
这是设置项目和安排文件夹的 ASP.NET Core 5 解决方案。
核心层-目录
核心层是干净架构的中心。 我们将把这个实现为 Visual Studio 解决方案中的一个目录或文件夹,这个解决方案可以使用。net Core CLI 创建。 所有其他项目依赖关系都必须指向特定的域和应用项目的核心。 类似地,核心层将永远不依赖于任何其他层。
要设置核心层,我们需要两个项目——Domain和Application项目。
域-项目
这个干净的体系结构部分是一个. net 标准 2.1 类库项目,包含实体、接口、枚举、dto 等等。
Domain项目必须有一个空的项目引用,这表明它不依赖于任何项目。
应用-项目
clean 体系结构的这一部分也是一个. net Standard 2.1 类库项目。 它定义了接口,但实现在这一层之外。 该项目还具有 CQRS 模式的命令和查询、MediatR 的行为、AutoMapper 的概要文件、异常、模型等等。
我们将在本章的构建一个干净的架构解决方案小节中创建上述项目。
如果您刚刚开始构建微服务,并意识到核心层中有代码需要在其他服务或域中重用,该怎么办?
共享内核- NuGet 项目
理想情况下,Shared Kernel项目是一个私有的 NuGet 包,用于在多个项目之间共享代码,例如在微服务中。 这个项目帮助我们轻松地切换我们所依赖的版本,而不会破坏我们的同事或其他团队构建其他解决方案的工作。
现在让我们看一下基础设施层。
基础架构层—目录
基础设施层具有Application项目中定义的接口的类实现。 资源,如 SMTP、文件系统或 web 服务是应用的外部依赖,但在这一层实现。
**这一层是解决方案中保存多个项目的另一个目录。 我们在这里添加的项目是针对数据和共享项目的。 我们也可以添加一个名为Identity for authentication的项目,但是我们将在第 9 章,安全 ASP.NET Core,以保持这个干净架构的结构部分最小化。 它们在这里。
要设置基础设施层,我们需要两个项目——Data和Shared。
数据-项目
基础设施层的这一部分是一个用于数据库的。net 5.0 类库项目。 您也可以将此数据项目命名为持久性项目; 持久性和数据是难以处理的。
共享-项目
基础结构层的这个部分也是一个 NET 5.0 类库项目。 该项目将包括不同服务之间的共享代码,如电子邮件、SMS 或日期时间。
同样,您将在本章的构建一个干净的架构解决方案一节中创建基础设施层项目。 同时,让我们进入下一节,即表示层。
表示层-目录
表示层是构建 web 应用的地方。 web 应用可以使用 ASP。 asp.NET Core MVC NET Core Web API,一个单页****应用(SPA),或者一个移动应用。
在本书的各个章节中,我们将构建的实际应用需要一个 Web API 项目和一个网站。
WebApi – project
WebApi是一个 ASP.NET Web API 项目,使用。NET 5.0。 该项目与任何客户端应用交互,如 web、移动、桌面和物联网(物联网)。
此外,WebApi取决于Application和infrastructure层。
客户端应用-非项目 web 应用
client-app是作为应用用户界面的 web 应用。 我们将在第十一章、的 Todo App中创建 Vue.js 的基础应用,它将在presentational文件夹中。 命名client-app。
这就是表示层的内容。 那么,你认为我们应该把应用的测试放在哪里? 让我们看下一节。
管理测试目录
本节不是干净架构原则的一部分,但是最佳实践是使用关注点分离。 因此,根据测试项目所做的测试(如单元测试、功能测试、集成测试和负载测试)来组织测试项目是最佳实践。
我将在下面的小节中列出测试项目,但我们不会在本章中创建它们。 下面是测试项目。
单元测试-项目
一个单元测试项目测试代码的一小部分和特定部分。 可以使用 XUnit、NUnit 或 MSTest 项目创建该项目。
集成测试项目
集成测试项目测试组件是否在一起工作。 可以使用 XUnit、NUnit 或 MSTest 项目创建该项目。
现在我们已经完成了开发人员用 ASP 编写测试的部分.NET Core,是时候为 asp.net 创建层和项目了.NET Core 5 解决方案。
构建一个干净的架构解决方案
在本节中,我们将创建一个解决方案、目录和项目,为应用构建一个干净的体系结构解决方案。 该应用是关于旅行列表和世界各地的地方,管理员可以添加新的,删除,更新和读取旅行目的地列表中的地方。
在我们开始之前,打开你的终端并导航到你的桌面。 确保您的终端或命令行在Desktop目录下。
对于 Windows 用户,使用 PowerShell 或 Git Bash 终端。 Git Bash 终端是您在第 2 章、设置开发环境中使用的 Git 版本控制安装程序的一部分。 如果你要使用 PowerShell,记住使用反斜杠而不是正斜杠:
-
Let's start with the folder of the solution by running the following command:
mkdir Travel该命令创建一个名为
Travel的文件夹。 -
The next step is to go inside the
Travelfolder:cd Travel您的终端现在应该在
Travel目录中。 -
Now, use the
dotnetCLI:dotnet new sln该命令创建一个解决方案文件,并从该文件所在的文件夹中获取其名称。
-
The next step is to create a folder named
src:mkdir srcsrc目录在Travel目录中。 -
Now, navigate to the
srcdirectory or folder:cd src在
src目录中,我们将在这里创建三个文件夹,即三个层:core、infrastructure和presentation。 -
Now let's create the
corefolder:mkdir core该命令创建了
core目录。 -
Now create the folder for
infrastructure:mkdir infrastructure这个命令创建
infrastructure目录或文件夹。 -
And now, create the
presentationdirectory or folder:mkdir presentation此命令创建
presentation目录或文件夹。 -
Go inside the
coredirectory:cd core在
core目录中,我们将创建两个项目。 第一个项目将为Domain层:dotnet new classlib -f netstandard2.1 --name Travel.Domain这个命令创建了一个新的. net Standard 2.1 类库
Travel.Domain。 第二个项目将为Application层:dotnet new classlib -f netstandard2.1 --name Travel.Application这个命令创建了一个新的. net Standard 2.1 类库
Travel.Application。 -
Then, go inside the
Travel.Applicationdirectory:
```
cd Travel.Application
```
该命令将导航到`Travel.Application`文件夹。
- Then we need to add a reference to the
Travel.Domainproject:
```
dotnet add reference ../Travel.Domain/Travel.Domain.csproj
```
现在,`Travel.Application`项目依赖于`Travel.Domain`项目,但只依赖于这个项目。
- Now, navigate to the
infrastructuredirectory:
```
cd ../../infrastructure
```
当位于`infrastructure`目录中时,我们将为`Data`和`Shared`创建项目。
- Let's now create the infrastructure for
Dataor persistence:
```
dotnet new classlib -f net5.0 --name Travel.Data
```
这个命令创建一个新的。net 5.0 类库项目`Travel.Data`。
- Let's now create the infrastructure for
Shared:
```
dotnet new classlib -f net5.0 --name Travel.Shared
```
这个命令创建另一个名为`Travel.Shared`的。net 5.0 类库项目。
- Then, go inside the
Travel.Datadirectory:
```
cd Travel.Data
```
在`Travel.Data`目录中,我们需要使`Travel.Data`依赖于`Domain`和`Application`层。
- Let's add a reference to the
Travel.Domainproject first:
```
dotnet add reference ../../core/Travel.Domain/Travel.Domain.csproj
```
现在,`Travel.Data`依赖于`Travel.Domain`项目。
- Now, let's add a reference to the
Travel.Applicationproject:
```
dotnet add reference ../../core/Travel.Application/Travel.Application.csproj
```
该命令创建了从`Travel.Data`到`Travel.Application`的依赖项。
- Now, go to the
Travel.Sharedproject:
```
cd ../Travel.Shared
```
这个命令将把您带到`Travel.Shared`项目的目录。
- Now we can add a reference to the
Travel.Applicationproject:
```
dotnet add reference ../../core/Travel.Application/Travel.Application.csproj
```
该命令创建了`Travel.Shared`项目对`Travel.Application`项目的依赖关系。
- Now, let's go to the
presentationlayer:
```
cd ../../presentation
```
现在我们进入了`presentation`层,我们将在这里创建一个 Web API 项目。
- We will create the latest ASP.NET Core 5
webapihere:
```
dotnet new webapi --name Travel.WebApi
```
这个命令创建一个名为`Travel.WebApi`的新的 Web API 项目。
- Let's navigate inside the
Travel.WebApifolder:
```
cd Travel.WebApi
```
现在,在`Travel.WebApi`里面,我们将添加一些参考。
- Let's add a reference to the
Applicationlayer:
```
dotnet add reference ../../core/Travel.Application/Travel.Application.csproj
```
此命令向`Travel.Application`项目添加一个依赖项。
- Now let's add a reference to
Datafor persistence:
```
dotnet add reference ../../infrastructure/Travel.Data/Travel.Data.csproj
```
此命令向`Travel.Data`项目添加一个依赖项。
- The next step is to add a reference to the
Sharedproject:
```
dotnet add reference ../../infrastructure/Travel.Shared/Travel.Shared.csproj
```
这个命令向`Travel.Shared`项目添加了一个依赖项。
- Now we can go back to the root folder of the application solution:
```
cd ../../../
```
由于解决方案文件在根文件夹中,我们可以将所有项目注册到解决方案文件中。
- The first project we will add to the solution file is the
Domainlayer:
```
dotnet sln add src/core/Travel.Domain/Travel.Domain.csproj
```
这个命令让解决方案文件知道`Travel.Domain`项目。
- We will also add the
Applicationlayer of the app:
```
dotnet sln add src/core/Travel.Application/Travel.Application.csproj
```
这个命令让解决方案文件知道`Travel.Application`项目。
- The next project to add is
Datafor persistence:
```
dotnet sln add src/infrastructure/Travel.Data/Travel.Data.csproj
```
这个命令让解决方案文件知道`Travel.Data`项目。
- We also have to add the
Sharedproject:
```
dotnet sln add src/infrastructure/Travel.Shared/Travel.Shared.csproj
```
这个命令让解决方案文件知道`Travel.Shared`项目。
- Now, the last project to add is the Web API project:
```
dotnet sln add src/presentation/Travel.WebApi/Travel.WebApi.csproj
```
这个命令让解决方案文件知道`Travel.WebApi`项目。
现在,您已经完成了构建 ASP.NET Core 5 应用与一个干净的体系结构。 关闭终端并双击解决方案文件以打开整个应用。
让我们看看 Visual Studio 2019、Visual Studio for Mac 或 Rider 中的应用。
Visual Studio 2019
在 Visual Studio 2019 中,应该是这样的:

图 4.3 - Visual Studio 2019 中的文件夹结构
Visual Studio 2019 显示Travel作为带有src目录的解决方案。 core、infrastructure和presentation层必须在src目录下。 您还将看到,Application和Domain项目位于core目录下,而Data和Shared项目位于infrastructure目录下。
类似地,您可以找到 ASP.NET Core 5WebApi在presentation目录下。
现在让我们看看 Mac 用户的应用。
Visual Studio for Mac
在 Mac 的 Visual Studio 中,如果你正确地遵循了,它应该是这样的:

图 4.4 - Mac Visual Studio 中的文件夹结构
Travel是应用解决方案的名称,其下面是src目录,在干净的体系结构中有core层、infrastructure层和presentation层。
此外,Application和Domain项目位于core目录下,而Data项目和Shared项目位于infrastructure目录下。 最后,ASP.NET Core 5WebApi项目必须在presentation目录中。
以上都是针对 Mac 用户的。 结构应该与 Visual Studio 2019 相同。 同样,结构应该与下一节相同,下一节是为 Rider 的用户准备的。 现在让我们看看 Rider IDE 如何显示应用解决方案。
骑手
最后,如果你正在使用 Rider,下面的截图显示了你的应用解决方案的结构:

图 4.5 - Rider 中的文件夹结构
同样,屏幕截图显示Travel作为解决方案的名称,并且它有src目录,其中包含core层、infrastructure层和presentation层。
Application和Domain项目在core目录中,而Data和Shared项目在infrastructure目录中。
在presentation文件夹中,您应该可以看到 ASP.NET CoreWebApi项目
好的,这是一个层次分明、结构清晰的架构解决方案。 下面是完成本章后的源代码的 GitHub 库:
https://github.com/PacktPublishing/ASP.NET-Core-5-and-Vue.js-3/tree/master/Chapter-4/Finish/Travel。
现在让我们总结一下在这里所学到的一切。
总结
通过这些,我们了解了什么是干净的架构,以及它将如何帮助开发人员构建可以持续 10 年甚至更长时间的应用。 我们还了解到,干净的架构使我们的服务可测试,并为微服务做好准备。
我们已经处理了包含干净架构的内容。 核心层与基础设施和表示层没有任何依赖关系。 基础结构层与外部源和表示层(用户使用和交互)进行通信。
您还学习了如何在清晰的体系结构中构建测试,最后,我们学习了如何使用dotnetCLI 来构建一个 ASP.NET Core 解决方案和项目。
现在,通过学习如何将干净的架构应用到 ASP,您已经提高了您的技能.NET Core 5,这将帮助你在未来构建一个高度可伸缩和可测试的应用。
在下一章中,我们将设置数据库并构建路由和控制器,以了解它们如何处理 HTTP 请求。**
五、设置DbContext和控制器
路由和控制器负责接收、验证和处理传入的 HTTP 请求。 在处理过程中,控制器可能通过DbContext持久化或不持久化并读取数据库中的记录。 了解控制器和DbContext如何一起工作对于构建预期工作的 web 应用项目至关重要。
本章结束后,您将能够理解以下主题:
- 编写实体和枚举
- 建立数据库 EF Core 和
DbContext - 写入控制器和路由
- 使用 Swagger UI 测试控制器
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019, Visual Studio for Mac,或 Rider
dotnet-ef工具- SQLite 浏览器或 SQLiteStudio
下面是本章的入门库,也是第 4 章、application Clean Architecture ASP. php 的完整源代码.NET Core Solution:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter05。
写入实体和枚举
在我们开始在域项目中编写实体和枚举之前,下面是它们的快速定义。 什么是实体? 实体是应用中领域对象模型的表示。 数据库将实体转换为数据库表中的一行。 类似地,实体的属性是数据库表中的列。
另一方面,枚举**是类的一种类型,表示一组常量或只读变量。
我们已经快速地了解了实体和 enum 的定义; 现在,让我们看看他们的行动。
为 Travel Tour 应用创建实体和枚举
在您的解决方案应用中,转到Travel.Domain项目并创建两个目录:
EntitiesEnums
Entities目录将用于所有 Travel Tour 应用实体,而Enums目录将用于所有 Travel Tour 应用枚举:
-
In the
Entitiesfolder, create a C# file,TourPackage.cs, and write this code:namespace Travel.Domain.Entities { public class TourPackage { public int Id { get; set; } public int ListId { get; set; } public string Name { get; set; } public string WhatToExpect { get; set; } public string MapLocation { get; set; } public float Price { get; set; } public int Duration { get; set; } public bool InstantConfirmation { get; set; } } }类是带有
Travel.Domain.Entities的namespace的TourPackage实体。TourPackage实体代表包访问数据库中的数据,Id,ListId,Name,WhatToExpect、【显示】,Price,Duration和InstantConfirmation。 它还需要一个 enum 用于Currency,一个对象关系用于List。 现在让我们创建这两个类。 -
In your
Enumsfolder, createCurrency.csand write the following code:namespace Travel.Domain.Enums { public enum Currency { PHP, USD, JPY, EUR, NOK } }Currency枚举文件有Travel.Domain.Enums的namespace,枚举有PHP、USD、JPY、EUR和NOK。 -
Now switch back to the
TourPackageclass to add theCurrencyenum like so:using Travel.Domain.Enums; namespace Travel.Domain.Entities { public class TourPackage { … public int Duration { get; set; } public bool InstantConfirmation { get; set; } public Currency Currency { get; set; } } }引入
Travel.Domain.Enums``namespace,这样您就可以使用刚刚使用Travel.Domain.Enums创建的Enum类。 现在在InstantConfirmation属性之后添加Currency属性。 -
Next, let's create a
TourList.csC# file inside theEntitiesfolder and write the following code:using System.Collections.Generic; namespace Travel.Domain.Entities { public class TourList { public TourList() { Tours = new List<TourPackage>(); } public IList<TourPackage> Tours { get; set; } public int Id { get; set; } public string City { get; set; } public string Country { get; set; } public string About { get; set; } } }让我们引入并定义
TourList实体。TourList实体有一个构造函数,该构造函数使用新的TourPackage``new List<TourPackage>()类型初始化Tours属性,该类型设置一对多关系。 其余属性为Id、City、Country、About。 -
Now go back to
TourPackage.csand add aListproperty of typeTourListbelow theCurrencyproperty:using Travel.Domain.Enums; namespace Travel.Domain.Entities { public class TourPackage { … public bool InstantConfirmation { get; set; } public Currency Currency { get; set; } public TourList List { get; set; } } }List类型TourList表示一对一关系,也称为对象-对象关系。
为域项目编写实体和枚举到此结束。 现在让我们转到下一个部分,设置实体框架核心(EF 核心)、DbContext和我们将在应用中使用的数据库。
建立数据库、EF Core 和 DbContext
您通常需要一个持久化框架,这是一个中间件,如果需要访问应用中的数据库,它可以帮助开发人员将数据存储在数据库中。 使用持久性框架,您可以轻松地使用实体来查询或将对象保存到数据库中。 现在让我们来看看 EF Core 和DbContext是什么。
英孚核心
在 ASP.NET Core,您可以从头构建持久性框架,但您不必这样做,因为这既耗时又昂贵。 为什么? 编写许多存储过程很难维护; 通过 ADO 读取数据.NET 对象将它们映射到应用中的表是很痛苦的。
所以,这里出现实体框架来拯救。 实体框架是一个持久性框架,它为您完成所有的管道工作。 通常,您不需要编写任何存储过程或将表映射到您的实体。
什么是英孚核心? EF Core是一个跨平台对象关系映射器(O/RM)。 EF Core 本身是实体框架的一个版本,它是一个使用 O/RM 在后台存储和检索对象的持久性框架。
这就是 EF Core 的概述; 让我们前往DbContext。
DbContext
EF Core 提供了一个名为DbContext的类,它是我们的数据库的接口。 DbContext可以有一个或多个DbSet来表示数据库中的表。 我们使用语言集成查询(LINQ)来查询DbSet实体,EF Core 会在运行时将我们的 LINQ 查询转换为 SQL 查询。
DbContext打开到数据库的连接,读取数据并映射到对象,并将其添加到DbContext的DbSet。
当我们在DbSet中保存、更新或删除对象时,EF Core 会跟踪这些更改。 当我们要求持久化更改时,EF Core 自动生成 SQL 语句并在数据库中执行它们。
以上就是对DbContext的简要概述; 现在是在应用中写入DbContext和一些DbSet实体的时候了。
设置
这个设置是将是一个简单的 EF Core 和DbContext设置。 然而,我们将在此过程中进行重构和改进:
-
First things first, install the
dotnet-eftool globally in your Terminal or CMD:dotnet tool install --global dotnet-efdotnet-ef工具是。net CLI 的 EF Core 工具。 您可以访问nuget.org/packages/dotnet-ef链接查看dotnet-ef工具的最新版本。我们还需要安装两个 NuGet 包。 我们将使用
dotnet cli,因为它可以在任何操作系统上运行,但如果您更喜欢使用管理 NuGet 包,即 IDE 中的 NuGet 包安装程序 UI,您也可以这样做。 -
Now go to the
Travel.WebApiproject and run this command:dotnet add package Microsoft.EntityFrameworkCore.Design在运行时使用 EF Core 迁移数据库时需要
Microsoft.EntityFrameworkCore.Design。 -
Next, go to the
Travel.Dataproject and run this command:dotnet add package Microsoft.EntityFrameworkCore.SqliteMicrosoft.EntityFrameworkCore.Sqlite是 EF Core 的 SQLite 数据库提供商。 -
Now, still inside the
Dataproject, let's writeDbContextwe will need here. Create a folder namedContextsinside theTravel.Dataproject. Inside the folder, create aTravelDbContext.csfile and write the following code:using Microsoft.EntityFrameworkCore; using Travel.Domain.Entities; namespace Travel.Data.Contexts { public class TravelDbContext : DbContext { public TravelDbContext (DbContextOptions<TravelDbContext> options) : base(options) { } public DbSet<TourList> TourLists { get; set; } public DbSet<TourPackage> TourPackages { get; set; } } }前面的代码导入了
Microsoft.EntityFrameworkCore包和Travel.Domain.Entities包。 我们将类命名为TravelDbContext,由 EF Core 的DbContext衍生而来。TraveDbContext的构造器有一个TravelDbContext类型的DbContextOptions参数选项,该选项指向 base 关键字。您将在构造函数下面看到两个
DbSet实体——TourLists和TourPackages。 -
现在转到
Travel.WebApi项目的Startup.cs文件,并引入这两个名称空间:using Microsoft.EntityFrameworkCore; using Travel.Data.Contexts; -
Write the following code inside the
ConfigureServicesmethod:services.AddDbContext<TravelDbContext>(options => options .UseSqlite("Data Source=TravelTourDatabase.sqlite3"));代码在
IServiceCollection中将TravelDbContext注册为服务,而UseSqlite将上下文配置为连接到 SQLite 数据库。ConfigureServices方法应该看起来像这样:public void ConfigureServices(IServiceCollection services) { services.AddDbContext<TravelDbContext>(options => options .UseSqlite("Data Source=TravelTourDatabase.sqlite3")); services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Travel.WebApi", Version = "v1" }); }); } -
Now let's go back to the
Travel.Dataproject for database migration.请注意
所有命令均为一行命令。 书页的宽度会给任何太长的命令增加一行。
-
After navigating to the
Travel.Dataproject, you are now ready to run your first migration:dotnet ef migrations add InitialCreate --startup-project ../../presentation/Travel.WebApi/EF Core 会在你的项目中创建一个名为
Migrations的文件夹,并生成 c#文件。 这些文件是名为InitialCreate的迁移构建器和数据库当前模式的快照。 -
Next, create a database and schema from the migration files:
dotnet ef database update --startup-project ../../presentation/Travel.WebApi/该命令在 Web API 项目中创建一个 SQLite 数据库文件
TravelTourDatabase.sqlite3。 -
要查看您所做的成功迁移,请使用 SQLiteStudio 或 SQLite Browser。
-
在 SQLiteStudio 或 SQLite 浏览器中添加
TravelTourDatabaseSQLite 数据库文件来查看表:

alicia alicia 工作室
上面 SQLiteStudio 的截图显示TravelTourDatabase,下面 SQLite Browser 的截图显示TravelTourDatabase:

图 5.2 - SQLite 浏览器
您已经了解了实体、实体框架、EF Core 和DbContext是什么。 你也学会了在哪里获得它们以及如何建立它们。 现在我们有了一个数据库,让我们继续下一节,写入控制器和路由。
写入控制器和路由
现在到了部分,我们将编写处理来自客户端应用的 HTTP 请求的控制器。 我们还将编写将 HTTP 请求重定向到其各自控制器的路由。
这将是两个简单的控制器,但我们将在下一章中重构它们,第六章,深入 CQRS。
TourPackagesController
我们将为TourPackages创建控制器,该控制器将处理任何检索和更新数据库中TourPackages表的请求:
-
Now go back to the
Travel.WebApiproject and createTourPackagesController.csinside of theControllersdirectory and write the following code:using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; using Travel.Data.Contexts; using Travel.Domain.Entities;代码导入前面的
namespaces,这是此控制器所需要的。 -
Next, write the
controllerclass:namespace Travel.WebApi.Controllers { [ApiController] [Route("api/[controller]")] public class TourPackagesController : ControllerBase { } }我们将使用
ApiController属性,该属性为装饰类提供一些 API 行为,如 HTTP API 响应,然后是另一个属性Route—重定向 HTTP 请求的路由。TourPackagesController类派生自ControllerBase,一个 MVC 控制器的基类,但不支持视图。 -
Next, write the following code inside
TourPackagesController:private readonly TravelDbContext _context; public TourPackagesController(TravelDbContext context) { _context = context; }我们将
TravelDbContext注入TourPackagesController的构造函数中,然后将其赋值给后台字段_context。 现在让我们来看看控制器中的第一个公共方法。 -
Next, we need to write the
Getmethod under the constructor method:[HttpGet] public IActionResult Get() { return Ok(_context.TourPackages); }HttpGet属性标识一个可以支持HttpGet方法的操作。 另外,Get方法返回一个200状态代码和TourPackage集合。 在使用GET HTTP动词请求时,当请求的端点为api/tourpackages时,此方法将被触发。现在让我们转向下一个公共方法。
-
Next, we need to write the
Createmethod under theGetmethod:[HttpPost] public async Task<IActionResult> Create([FromBody] TourPackage tourPackage) { await _context.TourPackages.AddAsync (tourPackage); await _context.SaveChangesAsync(); return Ok(tourPackage); }在
Create方法之上的HttpPost属性确定了一个可以支持HttpPost方法的动作。post方法采用带有FromBody装饰的TourPackage对象,指定应该使用请求体绑定参数。Create方法返回一个200状态码以及新创建的TourPackage。 当使用POST HTTP动词请求时,请求的端点为api/tourpackages时,此方法将被触发。你还会注意到这里的类型为
IActionResult的Task。IActionResult是一个接口,它定义了一个契约,表示一个操作方法的结果,而Task用于异步操作。现在让我们继续下一个公共方法
TourPackagesController。 -
Next, we write the
Deletemethod under theCreatemethod:[HttpDelete("{id}")] public async Task<IActionResult> Delete([FromRoute] int id) { var tourPackage = await _context.TourPackages.SingleOrDefaultAsync (tp => tp.Id == id); if (tourPackage == null) { return NotFound(); } _context.TourPackages.Remove(tourPackage); await _context.SaveChangesAsync(); return Ok(tourPackage); }Delete方法之上的HttpDelete属性标识一个可以支持HttpGet方法的操作。 当使用DELETE HTTP动词请求时,请求的端点为api/tourpackages/{id}时,此方法将被触发。您将注意到方法的
HttpDelete属性中有一个"{id}"字符串。SingleOrDefaultAsync使用id参数,将其与现有对象匹配,然后返回要从表中删除的对象。DbContext必须调用SaveChangesAsync()以完成指定TourPackage对象的删除。最后,
controller包括200 - OK应答中删除的TourPackage。现在我们来看看方法。
-
We need to write the
Updatemethod under theDeletemethod:[HttpPut("{id}")] public async Task<IActionResult> Update([FromRoute] int id, [FromBody] TourPackage tourPackage) { _context.Update(tourPackage); await _context.SaveChangesAsync(); return Ok(tourPackage); }前面的代码是一个方法,更新一个
TourPackage对象。
这就是TourPackagesController。 现在我们可以继续TourListsController。
TourListsController
现在在Controllers目录中创建TourListsController。 TourListsController与TourPackagesController具有相同的结构。
下面是TourListsController代码的摘要。 查看应用的 GitHub repo第 5 章|Finish directory中的代码,并将其写入您的文件:
using Microsoft.AspNetCore.Mvc;
…
using Travel.Domain.Entities;
namespace Travel.WebApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TourListsController : ControllerBase
{
private readonly TravelDbContext _context;
public TourListsController(TravelDbContext context){…}
[HttpGet]
public IActionResult Get(){…}
[HttpPost]
public async Task<IActionResult> Create([FromBody]
TourList tourList){…}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromRoute]
int id){…}
[HttpPut("{id}")]
public async Task<IActionResult> Update([FromRoute]
int id, [FromBody] TourList tourList){…}
}
}
如您所见,代码也是一个派生自ControllerBase的类,具有Get、Create、Delete和Update公共方法。
现在我们已经完成了为应用编写的控制器,让我们运行它来看看我们的工作是如何进行的,通过运行下面的命令仍然在WebApi项目中:
dotnet run
上述命令将启动或运行应用。 在应用运行时,我们可以继续下一节,使用 Swagger UI测试控制器,以查看控制器的工作情况。
使用 Swagger UI 测试控制器
我们已经构建了控制器并运行了应用,但是还没有测试它,对吧? 在本节中,我们将向TourList控制器发送GET、POST、PUT和DELETE请求。
首先,确保您正在通过进入Web Api项目并运行以下命令来运行应用:
dotnet run
现在检查 Swagger UI,在应用运行时转到 https://localhost:5001/swagger/index.html 链接。
您将看到旅游列表和旅游套餐端点。 Swagger UI 向您显示每个控制器的 HTTP 方法的文档。 有GET,POST,DELETE,和PUT方法准备尝试:

图 5.3 - Swagger UI
下面的截图显示了应用的模式,它们也是由 Swagger UI 为你生成的:

图 5.4 - Swagger UI 中的模式
现在让我们尝试一下TourLists控制器的GET方法。 它将返回一个空的集合或数组,因为我们还没有在其中添加任何数据。
现在转到POST,点击试试按钮。 在单击Execute按钮之前,用以下代码替换请求体:
{
"city": "Oslo",
"country": "Norway",
"about": "Oslo, the capital of Norway, sits on the country's southern coast at the head of the Oslofjord. It's known for its green spaces and museums. Many of these are on the Bygdøy Peninsula, including the waterside Norwegian Maritime Museum and the Viking Ship Museum, with Viking ships from the 9th century. The Holmenkollbakken is a ski-jumping hill with panoramic views of the fjord. It also has a ski museum."
}
你可以复制前面的请求体中,我们将使用发送POST 请求到服务器从Commands.txt文件,您可以找到在第五章应用文件夹的 GitHub 回购。
我们将只在请求体的文本区域中包含city属性、country属性和about属性,如下所示:

图 5.5 - POST 方法的请求体
在替换了POST部分的默认请求体后,可以看到图 5.5,点击Execute按钮:

图 5.6 - TourLists 控制器的响应体
您可以在图 5.6中看到响应体具有我们发布的属性和1的id。
如果您想更新Oslo City条目或删除它,您可以到 Swagger UI 并在那里发出请求。 但是,由于我们也将在应用中使用它,所以我将保持原样。
你可以在本书的 GitHub 库中查看章节的完整解决方案。
现在我们已经完成了这一部分,让我们回顾一下所学的内容。
总结
作为本章的总结,我们已经介绍了实体在应用中表示域模型,并且我们在应用的域层中编写实体。
我们还介绍了 EF Core,它帮助开发人员在不使用 O/RM 的 SQL 查询的情况下从数据库中持久化和查询数据。
我们了解到 Entity Framework 的DbContext是数据库的接口,而DbSet表示数据库中的一个表。
最后,我们用DbContext编写了两个控制器,并通过通过 Swagger UI 发送 HTTP 请求对它们进行了测试。
现在,您已经掌握了使用 EF Core 处理任何客户端应用发送的任何 HTTP 请求并处理它们并将它们保存在任何类型的关系数据库中的技能。
在下一章中,我们将学习如何使用CQRS或命令和查询职责隔离来编写可维护和可测试的代码。 那么,回头见。**
六、深入 CQRS
如果你打算构建大型应用,分离数据源的读写操作可以帮助应用在未来快速扩展。 命令和查询职责隔离(CQRS)模式将帮助您编写可维护和可测试的代码。 对于如何分离所有读取请求和所有修改数据请求,该模式也是理想的候选模式。
这一章是关于 CQRS 模式、中介模式和流行的用于 CQRS 和管道行为的 MediatR NuGet 包。
我们将涵盖以下主题:
- CQRS 是什么?
- 什么是中介模式?
- MediatR 包是什么?
- 为什么学习 CQRS ?
- 何时使用 CQRS
- CQRS 的缺点
技术要求
你将需要 Visual Studio 2019, Visual Studio for Mac,或 Rider 来完成本章。
什么是 CQRS?
CQRS表示命令与查询职责隔离。 这意味着命令和查询应该有单独的职责和明确的域边界。
让说你有一个控制器,控制器有几个端点获取数据,建立数据,更新数据和删除数据,这些都是你的,,,和删除方法简而言之。****
**控制器中获取数据或未突变数据的一切都属于查询; 而其他所有改变数据的请求,如 POST、PUT 和 DELETE 请求,则被归类为Command。
现在,您的应用应该有一个查询模型来处理从数据库获取数据的查询。 它还应该有一个命令模型,用于处理在数据库中写入或删除数据的命令。
下面是应用中的命令图:

图 6.1 -应用发送 HTTP 请求
前面的图 6.1显示 UI 通过命令处理程序向 API 端点发送 POST/UPDATE/DELETE 请求。 在命令处理程序之后,请求转到域模型和基础设施以修改数据库。
查询和命令的分离就是我们所追求的分离。 然而,中介(一种模式)将帮助我们解耦一切。
现在,让我们看另一个例子,我们可以看到一个控制器:

图 6.2 -控制器发送命令
在图 6.2中,最终的目标是带来一个 GET 请求,并有一个 post,就像在前面的图中一样,最终在中介中结束。 这个中介将找到一个查询处理程序和一个命令处理程序来处理请求,通过这个方法,控制器将不需要知道是否有任何服务被注入到处理程序中。
设计使应用更易于管理,而且更易于测试,因为您可以对独立于整个控制器的处理程序进行单元测试。 毕竟,控制器中什么都没有。 控制器只向中介发送一个查询,并最终到达处理程序中,但这是一种非常解耦的方法,对您的应用具有显著的可预测性。
这就是 CQRS。 现在,让我们转到下一节,了解中介模式是什么。
中介模式是什么?
假设您的应用中有四个服务、对象或元素,这些服务、对象或元素通常需要相互通信。 ServiceA需要跟ServiceB和服务,【显示】和ServiceC 需要跟ServiceB和服务【病人】,在ServiceB还需要跟【t16.1】提供服务。 这些服务、对象和元素现在紧密耦合在一起。**
调解人来拯救你了。 我们将在中间放置一个中介,而不是服务、对象或元素相互调用。 调解员就像一个机场交通控制塔,它知道我们希望我们的服务如何相互通信,如下图所示:

图 6.3 -调解员扮演交通管制塔
图 6.3图 6.3中的中介器是处理服务、对象或元素(本例中为飞机)之间通信的交通控制塔。
那么在 ASP 中如何实现中介模式呢? NET Core? 让我们在下一节中找到答案。
MediatR 包是什么?
那么,NuGet MediatR 包做什么呢? MediatR 包是. net 中中介模式的一个实现,可以随时使用。
MediatR 包使用起来并不复杂。 你只需要记住一个请求有一个相应的处理程序,它返回一个响应,如下图所示:

图 6.4 - MediatR 是中介模式的一个实现
您可以在图 6.4 中看到,您可以有多个请求。 一个请求进入 MediatR 包,后面跟着另一个请求。 中间有中介,它接受请求并找到需要处理这些请求的适当处理程序。 每个请求都有一个处理程序; 处理程序完成所有的工作,并为正确的请求返回正确的响应。
MediatR 包就像一个魔盒,您在其中推送请求,但不知道下游将由谁来处理请求。 现在处理请求成为 MediatoR 的职责。 因此,使用 MediatR 的一个原因是它将控制器从应用和业务逻辑完全解耦。
这就是 MediatR 包,一个可以用于 CQRS 的包。 为什么要学习 CQRS? 让我们在下一节中找到答案。
为什么要学习 CQRS?
所有控制器都只有一个依赖项,即 MediatR 包。 你的每个动作都会调用一个方法Mediator.send,并返回一个结果,这使得你的控制器比不使用 CQRS 模式的控制器更精简。
那么什么时候使用 CQRS 呢? 让我们看看下一节。
什么时候使用 CQRS
为什么要使用中介或 CQRS 模式? 原因有很多,但我们只讨论一些明显的原因,如下:
-
服务相互调用:所有读和写请求进入中间的盒子(中介),然后出来。 如果您想从任何地方触发任何请求,请求必须击中中间的方框,即中介。
-
大型项目中的干净代码:中介和中介管道的使用将帮助您缩小控制器大小并将业务逻辑移动到它们各自的文件中。 因此,轻松地遍历文件夹结构并找到要查找的逻辑。
-
A boundary between writes and reads: Any UI team can efficiently implement a UI that requires more data from the database due to the separation of reads and writes.
UI 团队可以自由地工作,而不必担心影响后端工作,以及逻辑如何将通知分发或写入其他通道以触发进一步的服务。
-
代码的可移植性:中介可以触发您在单独项目中重用的服务。 您可以通过引入中介并导入带有服务的项目来实现这一点,并查看可以通过此框访问您的服务。
这些都是使用 CQRS 的明显原因; 下一个问题是使用 CQRS 是否有任何缺点。 当然,在大多数情况下,利也有弊。 缺点将在下一节中介绍。
CQRS 的缺点
如果您将 CQRS 与存储库模式结合起来,CQRS 将带来额外的代码。 我的建议是不要添加另一个抽象,比如存储库模式,原因如下:
- 实体框架已经实现了存储库模式。
- 有 99%的可能性,你不会改变你的实体框架实现与 NHibernate ORM 或 Dapper Micro ORM。
好的,那么我们刚刚完成了 CQRS、Mediator 和 MediatR 包是什么。 让我们总结一下所学内容。
总结
您已经了解了 CQRS 将命令(更改或写入数据的请求)和查询(读取数据的请求)分离开来。 您还了解了 CQRS 如何帮助您编写更苗条的控制器。
您还了解了 Mediator 设计模式的作用类似于命令/查询和处理程序之间的空中交通控制器。 您学习了可以在。net 中使用的中介模式的实现,即 MediatR NuGet 包,它节省了时间,因为您不需要自己实现它,并且使用 mediator 可以使您的代码更简洁、更易于维护。
您还看到了使用 CQRS 的缺点,包括编写额外的代码,但是额外的代码意味着实现 CQRS 以使代码更清晰、更易于维护。
在下一章中,我们将应用 CQRS 模式和中介模式,并在 ASP 中使用 MediatR NuGet 包.NET Core 5 构建一个高度可伸缩和可维护的 Web API。**
七、CQRS 的作用
了解了 CQRS 模式、Mediator 模式和MediatRNuGet 包是什么之后,现在是时候将它们应用到 ASP 中了.NET Core 5 的 Web API。 上面提到的模式和包将为我们的应用带来价值,使其具有很强的可伸缩性、可测试性和可读性。
在本章中,我们将在应用中编写代码时涵盖以下主题:
- 实现 CQRS
- 添加
MediatR包 - 创建
MediatR管道行为 - 使用
FluentValidation - 使用
AutoMapper - 编写查询
- 写命令
IServiceCollection
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019, Visual Studio for Mac,或 Rider
- . net CLI
你可以到以下链接查看本章已完成的源代码:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter07。
实施 CQRS
下面是如何在 ASP 中使用MediatR包的步骤.NET Core 应用。 MediatR包的任务是帮助您轻松实现 CQRS 和 Mediator 模式。
让我们首先通过删除应用中可以找到的所有Class1.cs文件来清理我们的解决方案。 Class1.cs文件是在我们创建项目时生成的。
添加 MediatR 包
我们现在要安装MediatR包:
-
Navigate to your
Travel.Applicationproject using thedotnetCLI. Then we need to install some NuGet packages by running the following commands:dotnet add package MediatR前面的命令将. net 中的中介实现安装到
Travel.Application项目中。下面的命令安装 ASP 的
MediatR扩展.NET Core:dotnet add package MediatR.Extensions.Microsoft.DependencyInjection下面的命令安装
Microsoft.Extensions.Logging的日志抽象:dotnet add package Microsoft.Extensions.Logging.Abstractions前面的命令安装
Microsoft.Extensions.Logging.Abstractions,这是一个用于在。net 应用中创建记录器的包。 -
While in the
Travel.Applicationproject, create a directory and name itCommon. Then, create a directory in theCommonfolder, and name itBehaviors.现在,让我们创建四个 c#文件,并将它们命名为
LoggingBehavior.cs、PerformanceBehavior.cs、UnhandledExeptionBehavior.cs和ValidationBehavior.cs。 所有的都将在Behaviors文件夹中,该文件夹将是一个管道,用于对进入处理程序的任何请求进行预处理和后处理。请注意
我将在这里截断任何不必要的代码。 请到本章的 GitHub 存储库查看每个文件的完整源代码。
创建 MediatR 管道行为
MediatR中的管道行为类似于请求和处理程序之间的中间件。 验证就是一个很好的例子,因此处理程序只处理必要且有效的请求。 您还可以在这里进行日志记录和其他操作,这取决于您正在解决的问题。
我们将在这里编写我们的第一个MediatR管道行为:
// LoggingBehavior.cs
using MediatR.Pipeline;
…
namespace Travel.Application.Common.Behaviors
{
public class LoggingBehavior<TRequest> :
IRequestPreProcessor<TRequest>
{
…
public async Task Process(TRequest request,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
_logger.LogInformation("Travel Request: {@Request}",
requestName, request);
}
}
}
LoggingBehavior.cs的代码用于使用Microsoft.Extensions.Logging和MediatR.Pipeline名称空间记录请求。
IRequestPreProcessor是一个接口,用于为处理程序预处理已定义的请求,而IRequest是一个标记,用于表示带有响应的请求,您也将在命令和以后的映射查询中找到该响应。
下面的代码记录响应的运行时间(以毫秒为单位PipelineBehavior)。 PipelineBehavior包装内部处理程序,并在请求中添加一个额外行为的实现:
// PerformanceBehavior.cs
using MediatR;
…
namespace Travel.Application.Common.Behaviors
{
internal class PerformanceBehavior<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
{
…
public async Task<TResponse> Handle(TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
…
_logger.LogWarning("Travel Long Running Request:
{Name} ({ElapsedMilliseconds} milliseconds)
{@Request}",
requestName, elapsedMilliseconds, request);
return response;
}
}
}
您还将在这里找到,它调用管道中的下一个东西。 关键字next在中间件实现中很常见,这意味着转到修改请求的下一个函数:
// UnhandledExceptionBehavior.cs
using MediatR;
…
namespace Travel.Application.Common.Behaviors
{
public class UnhandledExceptionBehavior<TRequest,
TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<TRequest> _logger;
…
public async Task<TResponse> Handle(TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
Try { return await next(); }
catch (Exception ex)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(ex, "Travel Request: Unhandled
Exception for Request {Name} {@Request}",
requestName, request);
throw;
}
}
}
}
还有一件事在PipelineBehavior中缺失了。 是一种验证机制ValidationBehavior.cs,我们接下来将添加该机制。
我们还在Travel.Application项目中。 我们再添加两个 NuGet 包,其中包含FluentValidation。
使用 FluentValidation
FluentValidation让完全控制创建数据验证。 因此,它对所有验证场景都非常有用:
-
Let's add the
FluentValidationpackage to our application:dotnet add package FluentValidation前面的命令安装。net 的流行验证库。 该包使用一个连贯的接口来构建强类型规则。
-
下面的命令安装
FluentValidation的依赖注入扩展:
现在我们可以创建另一个行为,这将用于验证。 下面的代码通过使用IValidator和ValidationContext验证PipeLineBehavior中的请求,IValidator定义了一个特定类型的验证器,ValidationContext从FluentValidation命名空间创建一个新的验证上下文实例:
// ValidationBehavior.cs
using FluentValidation;
using MediatR;
using ValidationException = Travel.Application.Common.Exceptions.ValidationException;
…
namespace Travel.Application.Common.Behaviors
{
public class ValidationBehavior<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>>
_validators;
public ValidationBehavior
(IEnumerable<IValidator<TRequest>> validators)
{ _validators = validators; }
public async Task<TResponse> Handle(TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
if (!_validators.Any()) return await next();
var context = new
ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll
(_validators.Select(v => v.ValidateAsync(context,
cancellationToken)));
var failures = validationResults.SelectMany(r =>
r.Errors).Where(f => f != null).ToList();
…
return await next();
}
}
}
现在创建另一个目录并命名为Exceptions在Common``Travel.Application项目,因为这个文件夹的目录将的位置没有找到和验证异常。
在创建Exceptions文件夹后,创建两个 c#文件——NotFoundException.cs和ValidationException.cs:
// NotFoundException.cs
using System;
namespace Travel.Application.Common.Exceptions
{
public class NotFoundException : Exception
{
public NotFoundException()
: base() { }
public NotFoundException(string message)
: base(message) { }
public NotFoundException(string message, Exception
innerException)
: base(message, innerException) { }
public NotFoundException(string name, object key)
: base($"Entity \"{name}\" ({key}) was not found.") { }
}
}
前面的代码是一个重载方法,用于抛出带有命令内部含义的NotFoundException。 您很快就会完成这项工作。
下面的代码是针对一个或多个验证失败发生的异常:
// ValidationException.cs
using FluentValidation.Results;
…
namespace Travel.Application.Common.Exceptions
{
public class ValidationException : Exception
{
public ValidationException()
: base("One or more validation failures have
occurred.")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException
(IEnumerable<ValidationFailure> failures)
: this()
{
var failureGroups = failures
.GroupBy(e => e.PropertyName, e => e.ErrorMessage);
foreach (var failureGroup in failureGroups)
{ … }
}
public IDictionary<string, string[]> Errors { get; }
}
}
让我们在Common文件夹中创建一个Interfaces目录。 这个目录将是两个简单服务的接口的位置。
创建完Interfaces文件夹后,让我们创建两个 c#文件:IDateTime.cs和IEmailService.cs:
// IDateTime.cs
using System;
namespace Travel.Application.Common.Interfaces
{
public interface IDateTime
{
DateTime NowUtc { get; }
}
}
前面的代码是我们稍后将创建的DateTime服务的契约:
// IEmailService.cs
…
namespace Travel.Application.Common.Interfaces
{
public interface IEmailService
{
Task SendAsync(EmailDto emailRequest);
}
}
前面的代码是我们稍后将创建的EmailService服务的契约。
现在,让我们在Travel.Application的Common文件夹中为DbContext创建一个接口,但首先我们需要安装EntityFrameworkCore包:
dotnet add package Microsoft.EntityFrameworkCore
上述dotnet命令将安装 Entity Framework Core NuGet 包。
现在让我们在IapplicationDbContext中创建接口:
…
namespace Travel.Application.Common.Interfaces
{
public interface IApplicationDbContext
{
DbSet<TourList> TourLists { get; set; }
DbSet<TourPackage> TourPackages { get; set; }
Task<int> SaveChangesAsync(CancellationToken
cancellationToken);
}
}
前面的代码是我们在第 5 章、设置 DbContext 和控制器中创建的TravelDbContext的契约。 一旦合同即将实施,我们将稍后更新TravelDbContext。
我们刚刚使用 Fluent API 完成了验证的设置,现在让我们继续将数据传输对象映射到实体,反之亦然。
使用 AutoMapper
AutoMapper 是一个流行的库,它使用基于约定的对象到对象映射器。 因此,AutoMapper 让您无需编写大量代码就可以映射对象,稍后您将看到这一点。
现在我们需要使用AutoMapperNuGet 包自动设置对象到对象的映射,该包是由编写MediatR库的同一个人编写的。 我喜欢AutoMapper,因为它简化了映射和投影。
下面的命令安装AutoMapper:
dotnet add package AutoMapper
下面的命令安装 ASP 的AutoMapper扩展.NET Core:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
现在让我们为映射器创建文件。 在Travel.Application项目的Common中创建一个新目录并将其命名为Mappings。
在创建Mappings文件夹后,创建两个 c#文件:ImapFrom.cs和MappingProfile.cs:
// IMapFrom.cs
using AutoMapper;
namespace Travel.Application.Common.Mappings
{
public interface IMapFrom<T>
{
void Mapping(Profile profile) =>
profile.CreateMap(typeof(T), GetType());
}
}
前面的代码是用于从程序集应用映射的接口。 您将注意到有一个从AutoMapper传递过来的Profile类型。 Profile是一个配置,它将根据命名约定为您执行映射。
前面的代码允许我们对映射配置进行分组:
// MappingProfile.cs
using AutoMapper;
…
namespace Travel.Application.Common.Mappings
{
public class MappingProfile : Profile
{
public MappingProfile()
{
ApplyMappingsFromAssembly
(Assembly.GetExecutingAssembly());
}
private void ApplyMappingsFromAssembly(Assembly
assembly)
{
var types = assembly.GetExportedTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition()
== typeof(IMapFrom<>)))
.ToList();
foreach (var type in types)
{
var instance = Activator.CreateInstance(type);
var methodInfo = type.GetMethod("Mapping")
??
type.GetInterface("IMapFrom`1").GetMethod
("Mapping");
methodInfo?.Invoke(instance, new object[] { this
});
}
}
}
}
接下来,我们在Travel.Application项目中创建一个名为TourLists的目录。 然后,在TourLists文件夹中创建名为的另一个目录。 最后,我们在Queries文件夹中创建一个名为ExportTours的目录。
在创建三个嵌套目录之后,在Queries文件夹中创建一个名为TourItemFileRecord.cs的 c#文件:
using Travel.Application.Common.Mappings;
…
namespace Travel.Application.TourLists.Queries.ExportTours
{
public class TourPackageRecord : IMapFrom<TourPackage>
{
public string Name { get; set; }
public string MapLocation { get; set; }
}
}
我们将要创建的CsvFileBuilder文件将需要前面的代码来创建参数的类型。
让我们在Common目录的Interfaces文件夹中创建另一个 c#接口文件ICsvFileBuilder.cs:
…
namespace Travel.Application.Common.Interfaces
{
public interface ICsvFileBuilder
{
byte[] BuildTourPackagesFile
(IEnumerable<TourPackageRecord> records);
}
}
前面的代码是CsvFileBuilder的契约,我们稍后将创建它。
现在让我们在ExportTours文件夹中再创建两个 c#文件。 一个是ExportToursVm.cs文件,另一个是ExportToursQuery.cs文件,我们将在下一节中看到:
// ExportToursVm.cs
namespace Travel.Application.TourLists.Queries.ExportTours
{
public class ExportToursVm
{
public string FileName { get; set; }
public string ContentType { get; set; }
public byte[] Content { get; set; }
}
}
前面的代码是一个视图模型,我们将在后面的文件构建器中使用它。
现在,在编写了ExportToursVm类之后,我们可以继续下一节,即如何编写查询。
编写查询
现在,我们将创建第一个查询,这是一个读取数据的请求,以及一个查询处理程序,它将解析查询所需的内容。 同样,控制器负责将查询发送或分派给相关的处理程序。
下面是ExportToursQuery及其处理程序的代码块。 稍后您将看到控制器如何在中介器的Send方法中使用ExportToursQuery作为参数:
// ExportToursQuery.cs
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
…
namespace Travel.Application.TourLists.Queries.ExportTours
{
public class ExportToursQuery : IRequest<ExportToursVm>
{
public int ListId { get; set; }
}
public class ExportToursQueryHandler :
IRequestHandler<ExportToursQuery, ExportToursVm>
{
…
public async Task<ExportToursVm>
Handle(ExportToursQuery request, CancellationToken
cancellationToken)
{
var vm = new ExportToursVm();
…
vm.ContentType = "text/csv";
vm.FileName = "TourPackages.csv";
return await Task.FromResult(vm);
}
}
}
您可以通过派生MediatR包的IRequestsHandler来创建处理程序。 如果您注意到了,我们在这里使用DbContext和AutoMapper来处理请求并为MediatR包创建响应。
接下来,在Travel.Application项目的root目录中创建一个Dtos文件夹。 Dtos文件夹将具有与Common目录相同的级别。
创建完Dtos文件夹后,让我们在里面创建两个 c#文件:
// TourListDto.cs
…
namespace Travel.Application.Dtos.Tour
{
public class TourListDto : IMapFrom<TourList>
{
public TourListDto()
{
Items = new List<TourPackageDto>();
}
public IList<TourPackageDto> Items { get; set; }
public int Id { get; set; }
public string City { get; set; }
public string About { get; set; }
}
}
前面的代码是一个数据传输对象,或DTO,用于TourList。 下面的代码是TourPackage的 DTO:
// TourPackageDto.cs
using AutoMapper;
…
namespace Travel.Application.Dtos.Tour
{
public class TourPackageDto : IMapFrom<TourPackage>
{
public int Id { get; set; }
…
public void Mapping(Profile profile)
{
profile.CreateMap<TourPackage, TourPackageDto>()
.ForMember(d =>
d.Currency, opt =>
opt.MapFrom(s =>
(int)s.Currency));
}
}
}
使用与Dtos目录相同的方式,让我们在Travel.Application项目中创建另一个目录,并将其命名为TourLists。 然后,在TourLists目录中创建一个名为GetTours的文件夹。
创建完GetTours文件夹后,在里面创建两个 c#文件:
// ToursVm.cs
using System.Collections.Generic;
using Travel.Application.Dtos.Tour;
namespace Travel.Application.TourLists.Queries.GetTours
{
public class ToursVm
{
public IList<TourListDto> Lists { get; set; }
}
}
前面的代码是我们将要创建的GetToursQuery的Tours视图模型:
// GetToursQuery.cs
…
namespace Travel.Application.TourLists.Queries.GetTours
{
public class GetToursQuery : IRequest<ToursVm> { }
public class GetToursQueryHandler :
IRequestHandler<GetToursQuery, ToursVm>
{
…
public async Task<ToursVm> Handle(GetToursQuery
request, CancellationToken cancellationToken)
{
return new ToursVm
{
Lists = await _context.TourLists
.ProjectTo<TourListDto>
(_mapper.ConfigurationProvider)
.OrderBy(t => t.City)
.ToListAsync(cancellationToken)
};
}
}
}
前面的代码是GetToursQuery的处理程序,中介从控制器发送该处理程序。 稍后我们将更新TourListsController以实现此功能。
现在让我们进入下一个有趣的部分,编写命令。
编写命令
现在我们将创建第一个命令,这是一个保存、更新或删除数据的请求,以及一个命令处理程序,它将解析该命令需要什么。 同样,控制器负责将命令发送或分派给相关的处理程序。
现在我们到了为TourLists和TourPackages创建命令的部分。
让我们在Tour.Application项目的TourLists目录中创建一个文件夹,并将其命名为Commands。 然后,让我们在该文件夹中创建三个文件夹,并将其命名为CreateTourList、DeleteTourList和UpdateTourList。
现在是时候创建一些命令和命令验证器了。 在CreateTourList文件夹中创建两个 c#文件:
// CreateTourListCommand.cs
using MediatR;
…
namespace Travel.Application.TourLists.Commands.CreateTourList
{
public partial class CreateTourListCommand :
IRequest<int>
{
…
}
public class CreateTourListCommandHandler :
IRequestHandler<CreateTourListCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateTourListCommandHandler
(IApplicationDbContext context)
{ _context = context; }
public async Task<int> Handle(CreateTourListCommand
request, CancellationToken cancellationToken)
{
var entity = new TourList { City = request.City };
_context.TourLists.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
return entity.Id;
}
}
}
前面的代码是创建新的TourList的命令,我们在这里使用的是MediatR包:
// CreateTourListCommandValidator.cs
using FluentValidation;
…
namespace Travel.Application.TourLists.Commands.CreateTourList
{
public class CreateTourListCommandValidator :
AbstractValidator<CreateTourListCommand>
{
private readonly IApplicationDbContext _context;
public CreateTourListCommandValidator
(IApplicationDbContext context)
{
_context = context;
RuleFor(v => v.City)
…
.NotEmpty().WithMessage("About is required");
}
}
}
前面的代码是CreateTourListCommand的验证器,我们这里使用的是FluentValidation包中的RuleFor。
RuleFor 是针对特定属性的验证规则的构建器。 也就是说,FluentValidation是一个替代数据注解的验证库。 您应该使用它而不是数据注释,因为它帮助您编写干净和可维护的代码。
现在,在DeleteTourList文件夹中创建一个 c#文件:
// DeleteTourListCommand.cs
using MediatR;
…
namespace Travel.Application.TourLists.Commands.DeleteTourList
{
public class DeleteTourListCommand : IRequest
{
public int Id { get; set; }
}
public class DeleteTourListCommandHandler :
IRequestHandler<DeleteTourListCommand>
{
private readonly IApplicationDbContext _context;
public DeleteTourListCommandHandler
(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteTourListCommand
request, CancellationToken cancellationToken)
{
var entity = await _context.TourLists
.Where(l => l.Id == request.Id)
.SingleOrDefaultAsync(cancellationToken);
…
_context.TourLists.Remove(entity);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
前面的代码是用于删除TourList的命令,我们在这里使用的是MediatR包。 这里的Unit类型来自MediatR包,它表示没有返回值。 因为void不是有效的返回类型,所以Unit类型代表一个 void 类型。
现在,在UpdateTourList文件夹中创建两个 c#文件:
// UpdateTourListCommand.cs
using MediatR;
…
namespace Travel.Application.TourLists.Commands.UpdateTourList
{
public class UpdateTourListCommand : IRequest
{
…
}
public class UpdateTourListCommandHandler :
IRequestHandler<UpdateTourListCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTourListCommandHandler
(IApplicationDbContext context)
{ _context = context; }
public async Task<Unit> Handle(UpdateTourListCommand
request, CancellationToken cancellationToken)
{
var entity = await
_context.TourLists.FindAsync(request.Id);
…
entity.City = request.City;
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
前面的代码是更新数据库中现有TourList数据的命令。 同样,这里使用的是Unit类型:
// UpdateTourListCommandValidator.cs
using FluentValidation;
…
namespace Travel.Application.TourLists.Commands.UpdateTourList
{
public class UpdateTourListCommandValidator :
AbstractValidator<UpdateTourListCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTourListCommandValidator
(IApplicationDbContext context)
{
_context = context;
RuleFor(v => v.City)
.NotEmpty().WithMessage("City is required.")
…
}
}
}
前面的代码是UpdateTourListCommand的验证器,我们在这里使用的是FluentValidation包。
现在是TourPackage。 让我们在Travel.Application项目的根文件夹中创建一个文件夹,就像我们对TourLists所做的那样,并将其命名为TourPackages。
在创建了TourPackages目录之后,让我们在TourPackages目录中创建一个文件夹并将其命名为Commands。 然后,让我们在该文件夹中创建四个文件夹,并将它们命名为CreateTourPackage、DeleteTourPackage、UpdateTourPackage和UpdateTourPackageDetail。
现在,在CreateTourPackage文件夹中创建两个 c#文件:
// CreateTourPackageCommand.cs
using MediatR;
…
namespace Travel.Application.TourPackages.Commands.CreateTourPackage
{
public class CreateTourPackageCommand : IRequest<int>
{
…
}
public class CreateTourPackageCommandHandler :
IRequestHandler<CreateTourPackageCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateTourPackageCommandHandler
(IApplicationDbContext context)
{ _context = context;}
public async Task<int> Handle(CreateTourPackageCommand
request, CancellationToken cancellationToken)
{
var entity = new TourPackage { … };
_context.TourPackages.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
return entity.Id;
}
}
}
前面的代码是创建新的TourPackage的命令,我们在这里使用的是MediatR包:
// CreateTourPackageCommandValidator.cs
using FluentValidation;
…
namespace Travel.Application.TourPackages.Commands.CreateTourPackage
{
public class CreateTourPackageCommandValidator :
AbstractValidator<CreateTourPackageCommand>
{
private readonly IApplicationDbContext _context;
public CreateTourPackageCommandValidator
(IApplicationDbContext context)
{
_context = context;
RuleFor(v => v.Name)
.NotEmpty().WithMessage("Name is required.")
…
}
public async Task<bool> BeUniqueName(string name,
CancellationToken cancellationToken)
{
return await _context.TourPackages
.AllAsync(l => l.Name != name);
}
}
}
前面的代码是CreateTourPackageCommand的验证器,我们在这里使用的是FluentValidation包。
现在在DeleteTourList文件夹中创建一个 c#文件:
// DeleteTourPackageCommand.cs
using MediatR;
…
namespace Travel.Application.TourPackages.Commands.DeleteTourPackage
{
public class DeleteTourPackageCommand : IRequest
{
public int Id { get; set; }
}
public class DeleteTourPackageCommandHandler :
IRequestHandler<DeleteTourPackageCommand>
{
private readonly IApplicationDbContext _context;
public DeleteTourPackageCommandHandler
(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteTourPackageCommand
request, CancellationToken cancellationToken)
{
var entity = await
_context.TourPackages.FindAsync(request.Id);
…
_context.TourPackages.Remove(entity);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
前面的代码是用于删除TourPackage的命令,我们在这里使用的是MediatR包。
现在在UpdateTourPackage文件夹中创建两个 c#文件:
// UpdateTourPackageCommand.cs
using MediatR;
…
namespace Travel.Application.TourPackages.Commands.UpdateTourPackage
{
public partial class UpdateTourPackageCommand : IRequest
{
…
}
public class UpdateTourPackageCommandHandler :
IRequestHandler<UpdateTourPackageCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTourPackageCommandHandler
(IApplicationDbContext context)
{ _context = context; }
public async Task<Unit> Handle(UpdateTourPackageCommand
request, CancellationToken cancellationToken)
{
var entity = await
_context.TourPackages.FindAsync(request.Id);
…
entity.Name = request.Name;
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
前面的代码是用于更新TourPackage的命令,我们在这里使用的是MediatR包。
// UpdateTourPackageCommandValidator.cs
using FluentValidation;
…
namespace Travel.Application.TourPackages.Commands.UpdateTourPackage
{
public class UpdateTourPackageCommandValidator :
AbstractValidator<UpdateTourPackageCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTourPackageCommandValidator
(IApplicationDbContext context)
{
_context = context;
RuleFor(v => v.Name)
…
.MustAsync(BeUniqueName).WithMessage("The specified
name already exists.");
}
public async Task<bool> BeUniqueName(string name,
CancellationToken cancellationToken)
{
return await _context.TourPackages
.AllAsync(l => l.Name != name);
}
}
}
前面的代码是UpdateTourPackageCommand的验证器,我们在这里使用的是FluentValidation包。
现在在UpdateTourPackageDetail文件夹中创建两个 c#文件:
// UpdateTourPackageDetail.cs
using MediatR;
…
namespace Travel.Application.TourPackages.Commands.UpdateTourPackageDetail
{
public class UpdateTourPackageDetailCommand : IRequest
{
…
}
public class UpdateTourPackageDetailCommandHandler :
IRequestHandler<UpdateTourPackageDetailCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTourPackageDetailCommandHandler
(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle
(UpdateTourPackageDetailCommand request,
CancellationToken cancellationToken)
{
var entity = await
_context.TourPackages.FindAsync(request.Id);
…
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
前面的代码是另一个用于更新TourPackage的命令,我们在这里使用的是MediatR包。
// UpdateTourPackageDetailCommandValidator.cs
using FluentValidation;
…
namespace Travel.Application.TourPackages.Commands.UpdateTourPackageDetail
{
public class UpdateTourPackageDetailCommandValidator :
AbstractValidator<UpdateTourPackageDetailCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTourPackageDetailCommandValidator
(IApplicationDbContext context)
{
_context = context;
…
RuleFor(v => v.Currency)
.NotEmpty().WithMessage("Currency is required");
}
public async Task<bool> BeUniqueName(string name,
CancellationToken cancellationToken)
{
return await _context.TourPackages
.AllAsync(l => l.Name != name);
}
}
}
前面的代码是UpdateTourPackageDetailCommand的验证器,我们在这里使用的是FluentValidation包。
现在让我们继续下一节,它是关于编写 iserviccollection 的。
编写 iserviccollection
IServiceCollection是来自DependencyInjection名称空间的接口。 我们将使用IServiceCollection进行依赖注入。
最后,有一个针对Travel.Application项目的依赖注入。 在Travel.Application项目的root文件夹中创建一个 c#文件:
// DependencyInjection.cs
…
namespace Travel.Application
{
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this
IServiceCollection services)
{
services.AddAutoMapper
(Assembly.GetExecutingAssembly());
services.AddValidatorsFromAssembly
(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>),
typeof(PerformanceBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>),
typeof(UnhandledExceptionBehavior<,>));
return services;
}
}
}
前面的代码是一个依赖注入容器方法。 您将在这里看到,IServiceCollection正在向服务描述符集合添加不同种类的服务。
我们将注入静态方法,AddApplication,在Startup文件的 Web API 项目,尤其是Startup文件,不需要声明任何对第三方库的依赖性等 AutoMapper 因为他们已经宣布在这个文件中。
现在让我们转到Travel.Domain项目,为Mail服务添加一个settings文件。 在Travel.Domain项目中创建一个文件夹并将其命名为Settings。
在创建Settings目录之后,在其中创建一个 c#文件:
// MailSettings.cs
namespace Travel.Domain.Settings
{
public class MailSettings
{
public string EmailFrom { get; set; }
…
public string DisplayName { get; set; }
}
}
前面的代码用于我们稍后将创建的电子邮件服务的设置。
现在让我们向Travel.Application项目中再添加一个异常文件。 进入Travel.Application项目的Common目录,创建一个 c#文件:
// ApiException.cs
…
namespace Travel.Application.Common.Exceptions
{
public class ApiException : Exception
{
public ApiException() : base() { }
public ApiException(string message) : base(message) { }
public ApiException(string message, params object[]
args)
: base(String.Format(CultureInfo.CurrentCulture,
message, args)) { }
}
}
前面的代码是用于我们稍后将在电子邮件服务中使用的一个异常。
让我们进入Travel.Shared项目,为这个项目添加一些 NuGet 包:
dotnet add package MailKit
上述 CLI 命令安装。net 邮件客户端库MailKit。 下面的命令行安装一个包,该包在 ASP. net 中提供额外的配置.NET Core 应用:
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
下面的 CLI 命令安装Mimekit,一个用于创建和解析 S/MIME、MIME 和 PGP 消息的包:
dotnet add package MimeKit
安装 CSV 文件读写包CsvHelper:
dotnet add package CsvHelper
安装完 NuGet 包后,让我们在Travel.Shared项目的root目录下创建两个新文件夹Files和Services。
在Files文件夹中,我们创建一个 c#文件:
// CsvFileBuilder.cs
using CsvHelper;
using System.IO;
…
namespace Travel.Shared.Files
{
public class CsvFileBuilder : ICsvFileBuilder
{
public byte[] BuildTourPackagesFile
(IEnumerable<TourPackageRecord> records)
{
using var memoryStream = new MemoryStream();
…
return memoryStream.ToArray();
}
}
}
前面的代码是CsvFileBuilder的实现。
现在让我们在Services文件夹中创建两个 c#文件:
// EmailService.cs
using MimeKit;
using MailKit.Net.Smtp;
…
namespace Travel.Shared.Services
{
public class EmailService : IEmailService
{
…
public async Task SendAsync(EmailDto request)
{
try
{ var email = new MimeMessage { Sender =
MailboxAddress.Parse(request.From ??
MailSettings.EmailFrom) };
email.To.Add(MailboxAddress.Parse(request.To));
…
await smtp.DisconnectAsync(true); }
catch (System.Exception ex)
{ Logger.LogError(ex.Message, ex);
throw new ApiException(ex.Message); }
}
}
}
前面的代码是IEmailService的实现。
// DateTimeService.cs
…
namespace Travel.Shared.Services
{
public class DateTimeService : IDateTime
{
public DateTime NowUtc => DateTime.UtcNow;
}
}
前面的代码是IDateTime的实现。 它只返回DateTime.UtcNow。
对于Travel.Shared项目的依赖注入,让我们在Travel.Shared项目的root目录下创建一个 c#文件:
// DependencyInjection.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
…
namespace Travel.Shared
{
public static class DependencyInjection
{
public static IServiceCollection
AddInfrastructureShared(this IServiceCollection
services, IConfiguration config)
{
services.Configure<MailSettings>
(config.GetSection("MailSettings"));
services.AddTransient<IDateTime, DateTimeService>();
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<ICsvFileBuilder,
CsvFileBuilder>();
return services;
}
}
}
前面的代码是另一个依赖注入容器方法,我们稍后将在Startup文件中添加该方法。
现在是最后一步了。 转到Travel.WebApi项目并更新appSettings.json文件:
// appSettings.json
{
"Logging": {
"LogLevel": {
…
}
},
"MailSettings": {
"EmailFrom": "",
…
"DisplayName": ""
},
"AllowedHosts": "*"
}
该代码在appsettings.json中添加了MailSettings。
接下来,我们在Travel.WebApi项目的Controllers文件夹中创建一个 c#文件:
// ApiController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace Travel.WebApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public abstract class ApiController : ControllerBase
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??=
HttpContext.RequestServices.GetService<IMediator>();
}
}
前面的代码是允许ApiContoller使用 Mediator 的属性注入。 与构造函数注入相比,我更喜欢这种方法,因为它简单。 属性注入使不再需要使用构造函数注入来维护所有控制器的参数和签名。
接下来,我们更新Travel.WebApi项目中的TourPackagesController和TourListController文件:
// TourPackagesController.cs
…
namespace Travel.WebApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TourPackagesController : ApiController
{
[HttpPost]
public async Task<ActionResult<int>>
Create(CreateTourPackageCommand command)
{ return await Mediator.Send(command); }
[HttpPut("{id}")]
public async Task<ActionResult> Update(int id,
UpdateTourPackageCommand command)
{
…
await Mediator.Send(command);
}
[HttpPut("[action]")]
public async Task<ActionResult> UpdateItemDetails(int
id, UpdateTourPackageDetailCommand command)
{
…
await Mediator.Send(command);
}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
await Mediator.Send(new DeleteTourPackageCommand { Id
= id });
}
}
}
这个更新的TourPackagesController的代码使用 Mediator 发送命令,并从ApiController派生而来。
// TourListsController.cs
…
namespace Travel.WebApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TourListsController : ApiController
{
[HttpGet]
public async Task<ActionResult<ToursVm>> Get()
{
return await Mediator.Send(new GetToursQuery());
}
[HttpGet("{id}")]
public async Task<FileResult> Get(int id)
{
var vm = await Mediator.Send(new ExportToursQuery {
ListId = id });
return File(vm.Content, vm.ContentType, vm.FileName);
}
[HttpPost]
public async Task<ActionResult<int>>
Create(CreateTourListCommand command)
{
return await Mediator.Send(command);
}
[HttpPut("{id}")]
public async Task<ActionResult> Update(int id,
UpdateTourListCommand command)
{
await Mediator.Send(command);
}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
await Mediator.Send(new DeleteTourListCommand { Id =
id });
}
}
}
此更新的TourListsController代码使用 Mediator 发送命令,并派生自ApiController。
在更新TourListsController之后,我们将为 API 添加一个过滤器,方法是在Travel.WebApi项目的root目录中创建一个新文件夹,并将其命名为Filter。 然后,在新创建的文件夹中创建一个 c#文件ApiExceptionFilter.cs,并为ApiExceptionFilter添加以下代码:
using System;
…
using Microsoft.AspNetCore.Mvc.Filters;
using Travel.Application.Common.Exceptions;
namespace Travel.WebApi.Filters
{
public class ApiExceptionFilter :
ExceptionFilterAttribute
{
private readonly IDictionary<Type,
Action<ExceptionContext>> _exceptionHandlers;
public ApiExceptionFilter()
{
…
}
public override void OnException(ExceptionContext
context)
{
HandleException(context);
base.OnException(context);
}
private void HandleException(ExceptionContext
context)
{
…
}
private void HandleUnknownException
(ExceptionContext context)
{
…
}
private void HandleValidationException
(ExceptionContext context)
{
…
}
private void HandleNotFoundException
(ExceptionContext context)
{
…
}
}
}
前一个类的任何一个方法都没有什么特别之处。 请到前面代码的 GitHub 库查看这些方法的实现。 我们创建的 API 异常过滤器将处理 Web API 通过捕获错误并以一致的方式处理它们而生成的任何NotFoundException、ValidationException和UnknownException。
最后要做的是更新 Web API 项目的Startup.cs文件:
// Startup.cs
using Microsoft.Extensions.DependencyInjection;
using Travel.Application;
using Travel.Data;
using Travel.Shared;
using Microsoft.AspNetCore.Mvc;
using Travel.WebApi.Filters;
…
namespace Travel.WebApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{ Configuration = configuration; }
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection
services)
{
services.AddApplication();
services.AddInfrastructureData();
services.AddInfrastructureShared(Configuration);
services.AddHttpContextAccessor();
services.AddControllersWithViews(options =>
options.Filters.Add(new ApiExceptionFilter()));
services.Configure<ApiBehaviorOptions>(options =>
options.SuppressModelStateInvalidFilter = true
);
…
}
// This method gets called by the runtime. Use this
method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{ … }
}
}
Startup.cs文件的更新代码通过依赖注入注册应用、数据和共享项目的服务。 更新后的代码是在启动项目中保持容器引导的一种优雅的方式,而不会违反面向对象的设计原则。
现在我们可以检查控制器是否连接正确。 运行应用,然后转到 Swagger UI。 尝试TourLists控制器的GET请求:

图 7.1 -使用 Swagger UI 测试 TourLists 控制器
测试此处TourLists的GET请求的响应可以在图 7.1 中看到。 可以看到控制器响应200与TourList对象。
现在让我们总结一下所学的一切来结束这一章。
总结
这里的全部内容相当于整整一章。 让我们总结一下重要的部分。
您终于看到了如何应用CQRS、MediatR和Pipeline Behavior。 MediatR包使得 CQRS 模式在 ASP 中很容易实现。 净的核心。 Pipeline Behavior包允许您在处理程序处理命令之前和之后在命令中运行许多方法,例如验证或日志记录。
您学习了如何使用FluentValidation包,这是一个用于验证模型的强大库。
您还学习了如何使用AutoMapper包,这个库允许您通过编写几行代码将一个对象映射到另一个对象。
最后,您看到了如何使用IServiceCollection在Startup.cs文件中创建一个干净的依赖项注入。
在此基础上,我们制作了 ASP.NET Core 5 应用更具可测试性和可扩展性。 在下一章中,我们将使用 Serilog 来登录 ASP.NET Core 5,我们还将实现 API 版本控制。
八、API 的版本控制和 ASP.NET Core 的登录
欢迎来到另一章。 本章将教你如何记录 API 请求,这是任何应用的基本部分,因为记录会给开发人员和业务人员带来好处。 您还将了解 API 版本控制,这有时是创建可维护 API 所必需的,但如果不正确地执行,可能会出现问题。
我们将涵盖以下议题:
- API versioning
- 日志在 ASP.NET Core
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019, Visual Studio for Mac,或 Rider
- SQLite 浏览器或 SQLiteStudio
已完成存储库的链接可以在这里找到:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter08。
API 版本控制
所以我们在这一节将要学习的是关于 ASP 版本控制的一些事情.NET Core Web API。 我们将首先讨论什么是 API 版本控制以及 API 中的一些版本控制策略,然后我们将深入一些代码,并将 API 版本控制与 OpenAPI 集成,以便您可以在 Swagger UI 中看到版本控制的 API。
现在让我们从定义 API 版本控制开始。
什么是 API 版本控制?
简而言之,API 版本就是您希望处理 API 随时间变化的方式。 我们大多数后端开发人员专注于构建和部署 web 服务,因为这是我们的工作。 它们通常是我们必须在一个典型项目中实现的书面任务。 但有时我们会忘记,我们必须在未来 5 到 10 年里支持 web 服务,或者取代我们的开发人员会这样做。
一旦部署了 web 服务,并且开发人员已经根据 API 进行了编码,就会有用户依赖于我们构建的 API。 例如,具有特定版本的移动应用将期望 web 服务的端点永远不变。 有一个处理不变 api 的计划和策略是至关重要的,因为您的需求和应用的特性将继续发展。 您需要一种方法在不破坏现有客户端应用的情况下更改这些 api。
这就是 API 版本控制的核心思想。
那么我们如何对 api 进行版本化呢? 让我们看下一部分。
API 版本策略
有很多方法可以做到这一点,但我们只讨论最常用的三个 API 版本控制策略或方案。
你应该选择哪一个? 这取决于谁使用您的 API,您的 API 将如何发展,以及您需要什么样的技术需求。 找到适合您用例的东西。
那么我们如何弃用一些 api 呢?
URI 路径版本控制
URI 路径策略很流行,因为实现起来很简单。 在 URI 路径的某处,您在根附近插入一个版本指示符,例如v1或v2,然后路径的其余部分跟随其后。 这里有一个例子:
https://traveltour.xyz/api/v1/customers
前面的例子是 API 的版本 1,版本 2 如下所示:
https://traveltour.xyz/api/v2/customers
请注意
当切换 API 版本时,缓存使用 URI 作为键来使其失效,以便缓存中的内容本身与返回的 API 的特定版本匹配。
URI 路径版本控制在其他框架(例如 PHP 中的 Laravel、Java 中的 Spring Boot 和 Ruby on Rails)中也比基于查询的版本控制或基于 header 的版本控制更常见。
稍后我们将在应用中应用 URI 版本控制。 现在让我们继续下一个流行的 API 方案或策略。
消息头版本控制
使用头进行版本控制时,您可以使用一个动词,并且使用头值来指示开发人员正在寻找的版本。
这里是你可以指定版本的头的样本内容:
GET /api/customers HTTP/1.1
Host: localhost:5001
Content-Type: application/json
x-api-version: 2
前面的头内容显示没有向 URI 添加过滤器。
这种策略很有用,因为它不会破坏 uri。 然而,在客户端使用这些类型的 api 将需要一些复杂的步骤来将正确的请求和头发送到 web 服务。
查询字符串版本控制
查询字符串版本化就是使用查询字符串指定 API 的版本。 它允许 api 的使用者在需要时更改版本。 他们可以选择旧版本或新版本的 api。
记住,如果请求中没有查询字符串,那么您应该拥有 api 的隐式默认版本。 下面是一个查询字符串版本控制的例子:
https://traveltour.xyz/api/customers?api-version=2
如您所见,?api-version=2意味着此请求是针对客户路由和控制器的版本 2 的。
好的,那么有没有办法让开发人员知道某个 API 是否不再推荐使用呢? 是的,它是一个已弃用的 API,您将在下一节中看到。
弃用 API
已弃用的 API 是指不再推荐开发人员使用的 API。 一旦某个版本的 API 在几个月内没有用户,就可以将该版本从应用中删除。
因此,将一个 API 标记为 deprecated 是非常简单的。 方法如下:
[ApiVersion("1.0", Deprecated = true)]
这是一个来自Microsoft.AspNetCore.Mvc命名空间的注释,告诉开发人员 API 版本 1 现在已弃用。
在下一节中,让我们看看如何实现版本控制、API 的弃用,以及如何将它们与 OpenAPI 集成,以便在 Swagger UI 中实现可视化表示。
API 版本集成与 OpenAPI
现在我们到了部分,我们将实现 API 版本控制,弃用 API,并将其与 OpenAPI 集成。 所有的变化都将发生在Travel.WebAPI项目内部:
-
Go to
Travel.WebAPIand install two packages. The following NuGet library adds an API versioning service for ASP.NET Core:Microsoft.AspNetCore.Mvc.Versioning下面的 NuGet 库增加了发现 api 版本的控制器和动作的元数据的功能,以及 url 和允许的 HTTP 方法:
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer -
Next, let's create two new folders inside the
Controllerdirectory and name themv1andv2. Then move all the following controllers insidev1:ApiControllers.csTourListsControllers.csTourPackagesControllers.csWeatherForecastController.cs将每个控制器的命名空间更新为
namespace Travel.WebApi.Controllers.v1,并从ApiController派生WeatherForecastController。 -
Next, we update the annotations on top of the
ApiContollerabstract class inApiController.cs. The code should be as follows:namespace Travel.WebApi.Controllers.v1 { [ApiVersion("1.0")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public abstract class ApiController : ControllerBase { private IMediator _mediator; protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService <IMediator>(); } }我们添加了的
apiVersion注释和指定的版本。 我们还用api和controller之间的api动态版本更新了Route注释。 这两个注释还将自动应用于从ApiController类派生的任何控制器。 -
Now let's deprecate the
WeatherForecastendpoint by updatingWeatherForecastControllerinside theControllerdirectory:namespace Travel.WebApi.Controllers.v1 { [ApiVersion("1.0", Deprecated = true)] public class WeatherForecastController : ApiController { … } }该注释意味着我们显式地将
WeatherForecastController标记为 deprecated。 -
After deprecating, let's create a new version of the
WeatherForecastendpoint by creating a new C# file inside thev2directory of the controller and name itWeatherForecast.cs.在
v2文件夹中创建一个新的WeatherForecast controller文件后,用下面的代码更新代码:namespace Travel.WebApi.Controllers.v2 { [ApiVersion("2.0")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public class WeatherForecastController : ControllerBase { … [HttpPost] public IEnumerable<WeatherForecast> Post(string city) { var rng = new Random(); return Enumerable .Range(1, 5) .Select(index => new WeatherForecast { … City = city }) .ToArray(); } } }如果你注意到,
v1 WeatherForecast和v2 WeatherForecast之间的区别是HTTP方法。 在版本 1 中,您必须发送一个GET请求来获取日期和温度数据,而在版本 2 中,您必须发送一个带有city查询参数的POST请求。 这是 API 中破坏性更改的一个极好的例子。 因此,API 必须具有的版本控制,以避免破坏正在使用WeatherForecastAPI 的第一个版本的应用。POST 请求与
query并不是一个最佳实践,因为它的非幂等性,更适合使用在一个请求,而【显示】,,【病人】和删除 idempodent 请求。 但是,带有query参数的 POST 请求是打破WeatherForecast端点的最简单方法。 所以现在请耐心听我说。****现在,对于 OpenAPI,让我们在
Travel.WebApi的根目录下创建一个新文件夹,并将其命名为Helpers。 创建文件夹后,在文件夹中创建两个 c#文件ConfigureSwaggerOptions.cs和SwaggerDefaultValues.cs:// ConfigureSwaggerOptions.cs
*using System; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; namespace Travel.WebApi.OpenApi { public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions> { … public void Configure(SwaggerGenOptions options) { … } private static OpenApiInfo CreateInfoForApiVersion (ApiVersionDescription description) { … } } }*这里我们需要写两个方法。 让我们将第一个方法命名为
Configure(),将命名为第二个方法OpenApiInfo()。 以下是Configure()方法的代码块:*public void Configure(SwaggerGenOptions options) { foreach (var description in _provider.ApiVersionDescriptions) options.SwaggerDoc (description.GroupName, CreateInfoForApiVersion (description)); }*Configure()方法所做的就是为每一个发现的 API 版本添加一个 Swagger 文档。 下面是OpenApiInfo()方法的代码块:* private static OpenApiInfo CreateInfoForApiVersion (ApiVersionDescription description) { var info = new OpenApiInfo { Title = "Travel Tour", Version = description.ApiVersion.ToString(), Description = "Web Service for Travel Tour.", Contact = new OpenApiContact { Name = "IT Department", Email = "developer@traveltour.xyz", Url = new Uri ("https://traveltour.xyz/support") } }; if (description.IsDeprecated) info.Description += " <strong>This API version of Travel Tour has been deprecated.</strong>"; return info; }*基本上,这个代码是针对应用的
title、version、description、contact name、contact email和URL等 Swagger 信息:// SwaggerDefaultValues.cs
*using System.Linq; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; namespace Travel.WebApi.OpenApi { public class SwaggerDefaultValues : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { … } } }*我们将使用此代码来替换我们在
Startup.cs中的services.AddSwaggerGen()方法中编写的配置。 下面是Apply()方法的代码块:*public void Apply(OpenApiOperation operation, OperationFilterContext context) { var apiDescription = context.ApiDescription; operation.Deprecated |= apiDescription.IsDeprecated(); if (operation.Parameters == null) return; foreach (var parameter in operation.Parameters) { var description = apiDescription. ParameterDescriptions.First( pd => pd.Name == parameter.Name); parameter.Description ??= description.ModelMetadata.Description; if (parameter.Schema.Default == null && description.DefaultValue != null) parameter.Schema.Default = new OpenApiString (description.DefaultValue.ToString()); parameter.Required |= description.IsRequired; } }*Apply()方法允许 Swagger 生成器添加 API 浏览器的所有相关元数据。
** Now let's update the Startup.cs file. Find the AddSwaggerGen() method inside ConfigureServices and update it with the following code:
```
services.AddSwaggerGen(c =>
{
c.OperationFilter<SwaggerDefaultValues>();
});
```
前面的代码允许 Swagger 生成器在最初使用过滤器(我们在前面创建的`SwaggerDefaultValues`)生成操作之后修改操作。
接下来,为我们之前创建的`ConfigureSwaggerOptions()`方法添加一个服务生命周期:
```
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
```
将`AddTransient()`方法放在`AddSwaggerGen()`方法块下。
现在我们可以添加我们安装的`Microsoft.AspNetCore.Mvc.Versioning`中的`ApiVersioning`配置。 把它放在`AddTransient()`方法下面:
```
services.AddApiVersioning(config =>
{
config.DefaultApiVersion = new
ApiVersion(1, 0);
config.
AssumeDefaultVersionWhenUnspecified
= true;
config.ReportApiVersions = true;
});
```
上述代码在服务集合中添加了`ApiVersioning`。 `ApiVersioning`声明 API 的默认版本,*声明 API 支持的版本*在 API 响应的头文件中。
接下来,我们从安装的`Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer`NuGet 包中添加`API Explorer`。 把它放在`AddApiVersioning()`方法下面:
```
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
```
这段代码添加了一个 API 浏览器,它可以理解应用中的 API 版本。 它添加了这样的格式:`"'v'major[.minor][-status]"`。
现在在`Configure()`方法中添加一个参数。 命名为`provider`,类型为`IApiVersionDescriptionProvider`,如下所示:
```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
…
}
```
前面的代码发现并描述了应用中 API 版本的信息。
让我们用下面的代码来更新`Configure()`方法中的`UseSwaggerUI()`方法:
```
app.UseSwaggerUI(c =>
{
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"/swagger/
{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant
());
}
});
```
这段代码将通过循环提供者为每个发现的 API 版本构建一个 Swagger 端点。
* Now let's run the application and see the results of the code we've added to our application. Let's look at Swagger UI and test the version 1 `WeatherForecast` API and the version 2 `WeatherForecast` API to see if they are working correctly if we send a request.
从下面的截图*图 8.1*中可以看到,你可以选择你想要检查的 api 的版本:*
*
图 8.1 -版本选择器
图 8.1 显示了一个下拉菜单,让您选择要查看的 api 版本。
现在让我们看看版本 1 中的WeatherForecast控制器。 你会注意到在下一张图片图 8.2中,版本 1WeatherForecast在 Swagger UI 如何呈现端点细节方面与版本 2 不同:

图 8.2 -弃用 API
图 8.2 中中的默认样式,带有褪色的颜色和一个插入的端点名称,告诉开发人员该端点已经被标记为 deprecated。
现在,让我们尝试WeatherForecast端点的版本 2,这是一个 POST 请求,需要添加一个query参数的关键和城市的价值正如它的名字你想用你的请求的数据:
**
图 8.3 -天气预报第二版
在 Swagger UI 的帮助下,我们可以方便地在输入框中插入查询参数。 让我们在文本框中键入Oslo并执行它。
让我们看看点击Execute按钮后端点的响应:

图 8.4 -天气预报版本二的回应
您将注意到,响应现在将作为数组中WeatherForecast对象的city属性包含您的输入。 现在可以看到,新端点正在正常工作。
以上就是 API 版本控制的内容,现在我们可以开始登录应用,这也是一个令人兴奋的主题。
登录 ASP.NET Core
有时我们假设一个理想的世界,在那里我们的后端应用能够成功运行。 然而,在现实世界中,总有一些事件或错误是可以预期的。 例如,我们到 SQL Server 的连接可能会因为任何原因而中断。 为了做好我们的工作,作为最佳实践,我们应该预见到错误并适当地处理它们。
日志是在应用运行时打印输出行的地方,这些输出行给您一些关于应用的使用、性能、错误和诊断的信息。 简而言之,输出告诉我们应用内部发生了什么。
我们如何在 ASP 中做到这一点.NET Core?
登录 ASP.NET Core
如果你有曾经用 ASP 做过任何工作.NET Core,你可能见过ILogger接口。 也许,您甚至知道 ASP.NET Core 默认内置了登录功能。 但你知道我们可以配置或替换它吗? 是的,我们可以使用第三方库,如Serilog或NLog,稍后您将看到。 但是让我们讨论一下日志记录的基础知识,也就是日志记录的级别。
以下是定义的ILogger接口的日志级别:
- Trace:记录中最详细的消息,如敏感的应用数据。 但是,在缺省情况下,消息是禁用的,并且不能在生产环境中启用。 输出目标是一个跟踪侦听器。
- Debug:开发过程中用于交互调查的日志。 在几乎所有情况下,这都应该是您的日志级别。
- Information:监控应用一般流程的日志,在生产环境中需要这些日志来查看应用的运行状态。
- Warning:应用中出现异常、意外或异常事件的日志。 这些事件不会停止应用。
- Error:当执行流由于失败而停止时,突出显示的日志。 哪些错误发生得最多?
- Critical:显示系统崩溃或不可恢复的应用或灾难的日志。
现在我们知道了日志的级别。 是它吗? 不,还没有。 下面是一些日志输出的例子:
Microsoft.Hosting.Lifetime: Information: Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
前面的日志的例子告诉我们,端口上运行的计算机,但是成堆的这些文本日志将给我们很难弄清楚发生了什么在我们的应用。更不用说,这是费力而耗费时间来得到你想要的信息,极大的日志数据。
那么,如果有一种简单的方法可以从日志中查询特定的数据呢? 这是结构化的记录。
什么是结构化日志?
结构化日志为全结构化 JSON 对象的日志。 例如,我们可以使用 Kibana、Elmah、Seq、Stackify 等工具来过滤和分析日志,而不需要编写正则表达式。 这种类型的日志是很好的,因为突然之间,找到关于趋势和人们如何使用我们销售的产品的各种信息变得微不足道了。
所以,现在的问题是我们如何在 ASP 中进行结构化日志记录.NET Core?
Serilog versus NLog
Serilog和NLog是。net 中两个可以使用的流行的日志框架,它们都有很多文档。 因此,您可以很容易地在互联网上找到使用 Serilog 或 NLog 在应用中需要做什么。
这两个日志记录器有相似之处,比如基于 c#的配置和结构化日志记录。 然而,根据我的经验,我发现在 Serilog 中设置结构化日志比在 NLog 中更容易。 类似地,Serilog 使用 Fluent API 进行基于 c#的配置。
Serilog 和 NLog 在日志级别上有一些相似之处。 日志级别在Serilog致命的,错误,警告、【显示】信息、调试,【病人】详细。 虽然NLog致命的,【t16.1】错误,警告,信息、调试,和跟踪。
这些是您在其他编程语言和框架中也经常看到的日志级别。
因此,我们将在应用中使用 Serilog。 在我看来,Serilog 具有结构更好的日志支持,易于配置。 Serilog 中的接口对开发人员友好,使用 Serilog 的开发人员比使用 NLog 的开发人员多,这增加了开发人员体验 Serilog 的机会。
给你 Serilog 名字的起源,它来自术语序列化日志。 好的,让我们开始将 Serilog 添加和配置到应用中。
配置 Serilog
现在我们需要添加一些 NuGet 包,以便正确地使用和配置 Serilog。 我们将把这些包添加到Travel.WebApi并更新Program.cs文件:
-
Here are the NuGet packages that we are adding. The following package allows you to use Serilog in ASP.NET Core:
Serilog.AspNetCore下面的包允许您使用
appsettings.json配置 Serilog:Serilog.Settings.Configuration下面的包允许您记录异常详细信息:
Serilog.Exceptions下面的包允许你以一种简单而紧凑的 json 格式记录事件:
Serilog.Formatting.Compact让我介绍一下 Serilog 中的丰富内容。 富集剂可以修改、添加、删除 Serilog 中的属性。 充实器通过实现
ILogEventEnricher实现此功能,该功能在日志记录期间应用,为日志事件提供额外的信息。 -
Here are some of the enrichers that we need. Let's also install them in the
Travel.WebApiproject.下面的包用
System.Environment发出的属性丰富了 Serilog 日志事件:Serilog.Enrichers.Environment下面的包为 Serilog 添加了一个进程丰富器:
Serilog.Enrichers.Process下面的包用当前线程发出的属性丰富了 Serilog 日志事件:
Serilog.Enrichers.Thread -
Now let me also introduce you to sinks in Serilog. Sinks allow you to direct your logs to storage in various formats. You can target different storages to log events. There are too many sink choices to fit on a single page so please go here to see all of them: https://github.com/serilog/serilog/wiki/Provided-Sinks.
下面的包允许您在文本文件中编写 Serilog 事件。 它可以是纯文本或 JSON 格式:
Serilog.Sinks.File下面的包允许你在一个 SQLite 数据库中编写你的 Serilog 事件:
Serilog.Sinks.SQLite现在我们可以进入下一节,在这里我们将编写一些代码。
-
After adding the packages, let's update the
Program.csfile of theTravel.WebApiproject.用以下代码更新
Main()方法:public static int Main(string[] args) { var name = Assembly.GetExecutingAssembly().GetName(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .CreateLogger(); // Wrap creating and running the host in a try-catch block /* CreateHostBuilder inside a try catch */ }更新后的代码块以一种简单的方式实现了 Serilog。 您可以在这里看到来自
Serilog名称空间的LoggerConfiguration构造。 您还将在这里看到将被传递到接收器的日志事件的MinimumLevel。 然后编写CreateLogger()方法,该方法使用最小级别、充实器和下沉器创建记录器。 -
Now let's add our enrichers and sinks by updating
LoggerConfiguration. Let's insert the enrichers and the sinks between theMinimumLevel()andCreateLogger()methods like so:Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .Enrich.FromLogContext() .Enrich.WithExceptionDetails() .Enrich.WithMachineName() .Enrich.WithProperty("Assembly", $"{name.Name}") .Enrich.WithProperty("Assembly", $"{name.Version}") .WriteTo.SQLite( Environment.CurrentDirectory + @"/Logs/log.db", // For Mac and Linux users // Environment.CurrentDirectory + @"\Logs\log.db", // For Windows users restrictedToMinimumLevel: LogEventLevel.Information, storeTimestampInUtc: true) .WriteTo.File( new CompactJsonFormatter(), Environment.CurrentDirectory + @"/Logs/log.json", // For Mac and Linux users // Environment.CurrentDirectory + @"\Logs\log.json", // For Windows users rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Information) .WriteTo.Console() .CreateLogger();现在我们正在使用从 NuGet 包管理器中安装的所有充实器和接收器。 用您各自的操作系统替换 SQLite 的路径,因为获取目录或文件的路径取决于您所使用的操作系统。
-
Let's now write
CreateHostBuilderwrapped with atry catch, like so:try { Log.Information("Starting host"); CreateHostBuilder(args).Build().Run(); return 0; } catch (Exception ex) { Log.Fatal(ex, "Host terminated unexpectedly"); return 1; } finally { Log.CloseAndFlush(); }很多 Serilog 同步会异步地将这些事件发送到不同的目的地,当你的应用出于任何原因退出时,你真的想要那些闪光。 返回
0表示程序成功; 否则,程序将带着错误或异常退出。 -
The last thing to do before we test our Serilog is to include the
CreateHostBuilderdefinition, like so:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });我们在这里将 Serilog 设置为默认日志提供程序。 这是可能的,因为
ILogger接口允许覆盖它。 -
Now let's run the application again to see the log events in the JSON file and SQLite database file; both files will be generated automatically.
应用再次运行后,使用 Swagger UI 向
WeatherForecast v2端点或TourList v1端点发送请求。 在图 8.5中,您将看到自动生成的 SQLite 文件和出现在新Logs目录中的 JSON 文件。 这些文件包含 Serilog 产生的日志事件:

图 8.5 -日志目录中的 SQLite 文件和 JSON 文件
让我们使用 SQLite 浏览器或 SQLiteStudio 来查看 SQLite 文件。 在图 8.6中,你会注意到RenderedMessage和Properties是 JSON 格式的:

图 8.6 -在 SQLite 文件中记录事件
格式将能够帮助任何数据可视化工具在 UI 图中呈现数据。 它还有助于任何数据可视化工具查询您想从日志事件中提取的特定细节。
现在让我们检查 JSON 文件。 你还会在图 8.7中看到日志事件以 JSON 格式存储:

图 8.7 -以 JSON 格式记录事件
您将注意到日志事件中有@ signs。 符号前缀@是一个操作符,它告诉 Serilog 序列化传入的对象,而不是使用ToString()。
请注意
数据库 URL 或 URL 路径必须根据应用环境(如测试、验收和生产环境)而变化。 我在教程中使用了 c#,这样我们就可以悬停鼠标,阅读 NuGet 包接口的细节。 它还为我们在编写 c#代码时提供了智能感知。 但是,您应该在appsettings.json中配置 Serilog。
好吧。 让我们快速回顾一下你在这里学到的东西。
总结
至此,您已经了解到 API 版本控制在 API 开发之初至关重要,它可以使 API 灵活应对未来的任何更改。
您已经学习了如何使用 API 版本控制策略,即 URI 路径版本控制、头版本控制和查询字符串版本控制。 URI 路径是所有方案中最常见的。 API 版本控制的一部分是弃用 API 并将其与 OpenAPI 集成,这有助于开发人员在 Swagger UI 中查看所有 API 的详细信息。
您还学习了在应用中记录日志的重要性,以及在 ASP 中使用哪个库。 净的核心。 您现在还知道了什么是结构化日志记录,以及如何将其保存到数据库中,以便可以轻松地查询希望在应用中研究的内容。
在下一章中,我们将为我们的 ASP 增加安全性.NET Core 应用使用 JWT,或 JSON web 令牌。***
九、ASP.NET Core 的安全防护
正确保护您的 api 是必要的。 在本章中,我们将学习如何保护 ASP.NET Core 5 Web API 应用,因为这也是开发人员的责任,他们构建后端应用来保护 API。 . net 中有两个可用的框架可以用来保护应用,还有云身份提供商。
我们将解决以下问题:
- 了解 ASP.NET Core 身份
- 引入 IdentityServer4
- 客户身份与访问管理(CIAM)
- 验证使用 JWT
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019, Visual Studio for Mac,或 Rider
- 邮递员
- JWT 调试器:https://jwt.io
这里是本章已完成的回购的链接:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter09。
【课文讲解】 NET Core 身份
NET Core Identity是。NET 中的一个开源身份框架。 它为。net 应用提供了一种实现认证的方法,以确定某人的身份和授权,从而详细说明对用户访问的限制。 它还允许。net 应用管理用户帐户。 建议在应用中使用 Identity,以便您的应用遵循安全最佳实践和注意事项。 ASP.NET Core 还节省了开发时间,因此您可以只专注于业务逻辑。
NET Core 标识特性
ASP. net 的主要特点 NET Core 身份提供如下:
- 用户帐号和密码管理。
- 多因素认证(MFA)和双因素认证(2FA)。
- 登录和注销功能。
- 外部认证提供者,如 Facebook、谷歌、Twitter 和 Microsoft 帐户。
- 根据需要定制身份框架。
开发人员需要了解认证在 ASP 中是如何流动的.NET Core 身份及其工作原理。 ASP.NET Core 身份使用基于声明的安全方案,该方案早在。NET 版本 4.5 中就引入了。
声明是身份的属性,它包含一个键值对。 一个身份可以有一个或多个与之相关的声明。 例如,学校应用中的学生用户可以有多个声明来定义他们是谁。 这些声明可以包括names、schoolEmails或dateOfBirths。 每一项要求都必须有其独特的价值。
ClaimsIdentity类代表一个身份及其所有要求。 ASP 中的用户.NET Core 应用可以通过ClaimsPrincipal类的帮助拥有多个标识。 ClaimsPrincipal包含一个或多个ClaimsIdentity类; 因此,它可以有一个或多个类 ims。
为了更好地概述ClaimsIdentity和ClaimsPrincipal,让我给你看一个图表,图 9.1如下:

图 9.1 -识别离子的两种形式
为了说明这一点,请参见前面的图 9.1,其中学生可能有两种形式的身份证明:他们的学生证和他们的驾驶证。 学生证上有学生号和学校名称。 他们的驾照上有身份证和出生日期。 每个标识都足够灵活,允许访问不同的资源。 ClaimsPrincipal从相关的身份继承索赔列表。 在这个场景中,驾照索赔和学生证索赔识别学生用户。
Cookie 认证是 ASP 中最常见的认证方式.NET Core 应用。 很高兴知道 ASP.NET Core 内置了中间件,使应用能够利用基于 cookie 的认证。
另外,为了更好地概述 cookie 认证,让我向您展示一个图表图 9.2,如下图:

图 9.2 - ASP.NET Core cookie middleware
前面的图概述了一个简单的 ASP.NET Core cookie 中间件工作。 下面的步骤是在引擎盖下发生的:
ClaimsPrincipal及其继承的所有索赔在用户成功登录后被序列化到 cookie 中。- 浏览器从服务器保存加密的 cookie。
- cookie 通过请求头发送,并在每个下一个请求上由服务器验证。
- 在成功验证了
ClaimsPrincipal之后,将使用该 cookie 重新创建它。 然后主体将 att 发送到 HTTP 上下文用户属性。 - 最后,应用代码可以访问链接到主体的所有用户信息和索赔。
这是对 ASP 的快速介绍.NET Core 身份; 现在,让我们继续访问 IdentityServer4。
引入 IdentityServer4
安全令牌服务(STS)是基于令牌认证的关键组件。 您可能遇到的其他术语是身份提供者或授权服务器。 它是一个软件,用于生成并向客户端应用发出安全令牌。 客户端应用将用户验证重定向到 STS 来处理它。 对令牌进行了加密和签名,以确保令牌免受任何篡改。
应用中的加密使用令牌服务保存的私钥,而用于解密令牌的公钥则与客户端应用共享。 通过这种方式,客户端应用相信令牌来自正确的令牌服务。 用于提供身份信息的标准,如 OpenID Connect,由令牌服务使用。
当谈到消费令牌服务时,有不同的选项。 您可以使用预构建的服务(如活动目录联合服务(ADFS)来构建使用 IdentityServer 框架的自定义服务。
IdentityServer4是一个用于令牌认证的开源框架。 在编写本文时,IdentityServer4 是 ASP 的最新推荐版本.NET Core 5 应用:

图 9.3 -安全系统的基本结构
前面的图解释了安全系统的基本结构。 客户机是一个从 IdentityServer4 请求令牌的应用。 例如,它可以是一个移动应用或网页应用。 IdentityServer4 必须通过注册或将它们包含在实体或存储集合中来了解允许使用它的客户机应用。 该集合可以持久化或保存在数据库或内存中。
接下来是使用客户端应用或与客户端应用交互的最终用户。 用户还必须通过 IdentityServer 注册。 唯一标识符以及用户名和密码凭证是定义用户的身份数据的示例。 用户还可以拥有一个或多个关联的索赔。 身份资源和 API 资源是需要保护的东西。 身份资源是关于身份的详细信息,例如用户声明。 另一方面,API 资源表示受保护的功能,比如 Web API。 资源也必须注册; t 它们需要在标识服务器存储中注册。 两种类型的令牌和两种资源类型一起生成。
当 IdentityServer4 对用户进行认证时,信息与身份令牌一起发送。 客户端将访问令牌转发给 API,当请求访问 API 资源时,API 允许或授予对受保护功能或数据的访问,并且成功发出访问令牌。
IdentityServer4 是一个中间件; 它使用行业标准JSON Web 令牌(jwt),并实现了两个标准协议 OAuth 2 和 OpenID Connect。 OAuth 2为授权的开放标准; 它确保用户具有访问受保护资源的权限。 另一方面,OpenID Connect是一个认证协议,是建立在 OAuth 2 协议之上的扩展。 使用 OAuth 2,客户端可以从 STS 请求访问令牌,然后与 api 通信。
IdentityServer4 可以通过其框架和 OAuth 2 和 OpenID Connect 的实现来管理或处理认证和授权需求。
以上就是对 IdentityServer4 的简要介绍; 让我们进入CIAM。
客户身份与访问管理(CIAM)
安全性是复杂的,您必须正确对待它。 所以今天,开发人员相信认证提供者可以帮助保护他们的应用。 Auth0,AAD B2C,Okta、【显示】AWS 隐身,和GCP 身份平台是很受欢迎的认证提供者,也称为【病人】身份作为服务(IDaaS)。 那么,为什么要考虑使用认证提供者?
注意:
我现在是 Auth0 大使。 不,我不是公司的雇员,也得不到任何金钱补偿。 尽管我偶尔也会在会议上发言并提及这些东西时得到一些好处和其他很酷的待遇。 但是我成为 Auth0 大使,正是因为我一直在使用和推荐它。
首先,安全性是复杂的,而且很容易出错。 这就像你在一个巨大的谜题中获得了所有正确的选择。 通过选择专门从事安全的云身份认证提供商,您可以相信认证提供商的安全工程师知道他们在做什么,并且可以把时间集中在开发应用上。 类似地,您可以根据自己的需要定制提供者。 因此,它们可以很好地与您的应用集成。 那么,定价呢? 嗯,价格各不相同,但一些认证提供商提供了一个免费层,适用于许多类型的应用。
我建议您使用 Auth0,因为它很容易使用,是最流行的,并且有优秀的在线文档。 它还为创建单页应用提供了优秀的库和 sdk,而且价格合理,因为它提供了慷慨的免费层。 Auth0 的免费定价支持多达 7000 名活跃用户。 如果你想知道付费版有什么功能,可以去他们的网站看看,但免费版对大多数应用来说已经足够了。
术语iaas和云身份提供商正在被缩写 CIAM 所取代。
好的,那么什么是CIAM,或者客户身份和访问管理?
CIAM 处于客户体验、安全性和分析的交叉点。 为用户提供无障碍的登陆方式对于建立用户忠诚度和推动转化率至关重要。 采取措施防止数据泄露和保护敏感数据免受恶意入侵是 CIAM 遵循数据隐私法律和安全政策的重点。 更不用说将用户数据编译成一个或单个的真相源对于理解您的客户是至关重要的。
许多公司选择使用第三方 IDaaS,而不是在内部从头构建解决方案。
公司需要身份和访问管理(IAM)解决方案,以满足不同类型的终端用户:客户、员工和企业客户。 但是每种类型的用户对用户体验(用户体验)和安全性的要求不同。 因此,CIAM 解决方案提供了一组出色的特性,有别于劳动力标识解决方案或 B2B。
以下是组成现代 CIAM 解决方案的四个特性。 没有两个 CIAM 解决方案提供完全相同的功能,但如果您正在注册或购买一个 CIAM 平台,它应该具有以下标准:
- 可扩展性:CIAM 必须将从数千用户扩展到数百万甚至数十亿用户。
- 单点登录:允许用户登录或登录到一个应用,然后自动登录到一组不同的应用。
- 多因素认证:MFA 是一种更安全的验证用户身份的方法,而不是普通的用户名和密码组合。
- 集中式用户管理:您对终端用户或客户的洞察力可以成为一个极好的竞争优势,但前提是数据是可访问的有组织且准确。
以上就是对 CIAM 的介绍。 现在,让我们在下一节讨论应用中的安全性实现。
使用 JWT 的认证实现
JWT或JSON Web 令牌是一种类型的令牌,用于在机器之间携带身份数据。 它由不同的编程语言、行业标准支持,并且可以很容易地传递。 JWT 是自包含的,并且它在自身内保存所需的标识信息,如下图所示:

图 9.4 - JWT 的部分
前面的图 9.4显示了 JWT 的三个部分:头、负载和签名。 头文件有两个属性。 一个是alg,它是算法的缩写,它确定用于编码此令牌的算法。 typ等于JWT。 我们不需要担心这个头,因为这只是一个标准。
对我们来说重要的是第二部分,即有效载荷。 所以在这里,我们有一个 JSON 对象,它有三个属性:sub(通常是一个用户 ID)、name和iat(这是生成令牌的时间)。 这里您需要知道的是,有效负载包含关于用户的公共属性,比如您的护照上如何拥有关于您自己的一些属性,比如姓名、出生日期和出生地。 我们在 JWT 上有相同的概念。 我们可以包含一些关于用户的基本公共属性。 因此,每次我们从客户机向服务器发送令牌时,我们都可以轻松地从有效负载中提取用户 ID。 如果您需要知道用户的名称,您也可以简单地从有效负载中提取它,这意味着您不需要查询数据库,发送用户 ID 来获取用户对象,然后提取 name 属性。
JWT 的第三部分是数字签名。 数字签名是基于 JWT 的头和有效负载以及秘密或私钥创建的。 如前所述,密钥或私钥仅在服务器上可用。
为了更好的可读性,也请参考 GitHub 中本章的源代码。 如果恶意用户获取了 JWT 并修改了属性,数字签名将无效,因为 JWT 内容被修改了。 现在黑客需要一个新的数字签名。 然而,黑客无法生成数字签名,因为他们需要只有在服务器上可用的私钥。 因此,如果他们不能访问服务器,他们就不能创建有效的数字签名。 当他们将这个新的篡改的 JWT 发送到服务器时,服务器将拒绝它。 服务器会说“不是有效的 JWT”。
这就是 JWT 的工作方式。
实现基于令牌的认证
我们将在应用中实现使用 JWT 的基于令牌的认证。 这个实现生成了一个 JWT,但是我需要您从本章中学到的是,了解您需要什么样的体系结构,并规划在未来的应用中适合使用什么样的安全实现类型。 您可能需要 ASP.NET Core 标识、IdentityServer4 或 CIAM。
所以,让我们开始为我们的应用添加安全性:
-
Create a new .NET 5 class library inside the infrastructure directory and name it
Travel.Identity. After creating the library, delete the defaultClass1.csfile that you will find there.我们还需要创建引用。 创建一个从这个项目
Travel.Identity到Travel.Application项目的引用,从Travel.WebApi项目到这个项目的引用。然后在
Travel.Identity项目中安装以下两个 NuGet 包:Microsoft.AspNetCore.Authentication.JwtBearer这个 NuGet 包是一个中间件,它允许。net 应用接收 OpenID Connect 令牌。 下面的 NuGet 包是一个中间件,它允许。net 应用支持 OpenID 认证工作流:
Microsoft.AspNetCore.Authentication.OpenIdConnect上述 NuGet 包目前安装在
Travel.WebApi项目中,但我们需要在Travel.Identity项目中安装,而不在Travel.WebApi项目中。 也就是说,删除安装在Travel.WebApi项目中的JwtBearer和OpenIdConnect包。 -
Next, we go to the
Travel.Domainproject and create a new C# file with the nameUser.csinside theEntitiesfolder:namespace Travel.Domain.Entities { public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Username { get; set; } public string Password { get; set; } } }前面的代码块是一个简单的包含
Id、FirstName、LastName、Username和Password的User实体。 -
Now, let's go back to the
Travel.Identityproject and create a folder in its root directory. Name the folderHelpers. Create three C# files inside theHelpersfolder and name themAuthSettings.cs,JwtMiddleware.cs, andAuthorizeAttribute.cs, like so:namespace Travel.Identity.Helpers { public class AuthSettings { public string Secret { get; set; } } }前面的代码块用于稍后将在
appsettings.json文件中编写的秘密配置。以下来自
JwtMiddleware.cs的代码块是自定义中间件,用于检测和提取 HTTP 请求中的授权报头:… using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Travel.Application.Common.Interfaces; namespace Travel.Identity.Helpers { public class JwtMiddleware { private readonly RequestDelegate _next; private readonly AuthSettings _authSettings; public JwtMiddleware(RequestDelegate next, IOptions<AuthSettings> appSettings) { … } public async Task Invoke(HttpContext context, IUserService userService) { … } private void AttachUserToContext(HttpContext context, IUserService userService, string token) { … } catch { } } } }前面的代码块还验证了从 sender 应用的头中提取的令牌,我将其截断并移动到这里:
private void AttachUserToContext(HttpContext context, IUserService userService, string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); byte[] key = Encoding.ASCII.GetBytes (_authSettings.Secret); tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, ClockSkew = TimeSpan.Zero }, out var validatedToken); var jwtToken = (JwtSecurityToken)validatedToken; var userId = int.Parse (jwtToken.Claims.First(c => c.Type == "id").Value); context.Items["User"] = userService.GetById(userId); } catch { } }假设验证是正确的; 调用
userService.GetById来获取用户的数据。下面来自
AuthorizeAttribute.cs的代码块是一个自定义属性,允许你用Authorize注释类或方法:… namespace Travel.Identity.Helpers { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AuthorizeAttribute : Attribute, IAuthorizationFilter { public void OnAuthorization (AuthorizationFilterContext context) { var user = (User)context.HttpContext.Items["User"]; if (user == null) { context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; } } } }如果命中带注释路由或控制器的请求没有经过认证,控制器将发送一个带有
Unauthorized消息的 JSON 对象。 -
Now let's go to the
Travel.Applicationproject. Find theDtosdirectory, create a folder inside of that directory, and name itUser. Then create two C# files inside of theUserfolder and name themAuthenticateResponse.csandAuthenticateRequest.cs, like so:namespace Travel.Application.Dtos.User { public class AuthenticateResponse { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Username { get; set; } public string Token { get; set; } public AuthenticateResponse (Domain.Entities.User user, string token) { Id = user.Id; FirstName = user.FirstName; LastName = user.LastName; Username = user.Username; Token = token; } } }这个
User模型是经过验证的用户成功登录后响应的形状:using System.ComponentModel.DataAnnotations; namespace Travel.Application.Dtos.User { public class AuthenticateRequest { [Required] public string Username { get; set; } [Required] public string Password { get; set; } } }前面的代码块要求登录请求在请求体中具有
Username和Password属性或键。 -
Now let's create an interface for the user service we will create later. Look for the
Commondirectory inside of theTravel.Applicationproject. Now there's theInterfacesdirectory inside theCommondirectory; create an interface inside theInterfacesfolder and name itIUserService.cslike so:using Travel.Application.Dtos.User; using Travel.Domain.Entities; namespace Travel.Application.Common.Interfaces { public interface IUserService { AuthenticateResponse Authenticate (AuthenticateRequest model); User GetById(int id); } }在创建
IUserService.cs文件之后,让我们创建该接口的实现。 回到Travel.Identity项目并在项目根目录中创建一个Services文件夹。 然后创建一个新的 c#文件,命名为UserService.cs:… using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace Travel.Identity.Services { public class UserService : IUserService { private readonly List<User> _users = new List<User> { new User {…} }; … public AuthenticateResponse Authenticate (AuthenticateRequest model) { … } public User GetById(int id) => _users.FirstOrDefault(u => u.Id == id); private string GenerateJwtToken(User user) { … } } }前面的代码块是我们前面创建的
IUserService.cs的实现。 出于可读性的考虑,我截断了Authenticate和GeneratateJwtToken方法的业务逻辑。 下面是Authenticate方法和GenerateJwtToken方法的完整代码:public AuthenticateResponse Authenticate (AuthenticateRequest model) { var user = _users.SingleOrDefault(u => u.Username == model.Username && u.Password == model.Password); if (user == null) return null; var token = GenerateJwtToken(user); return new AuthenticateResponse(user, token); }此实现通过检查用户是否在数据库中存在来验证
User。 但是,为了简化实现,我们在这里对user对象进行了硬编码:private string GenerateJwtToken(User user) { byte[] key = Encoding.ASCII.GetBytes (_authSettings.Secret); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), Expires = DateTime.UtcNow.AddDays(1), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature ) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken (tokenDescriptor); return tokenHandler.WriteToken(token); }GenerateJwtToken方法使用appsettings.json文件中的Secret值生成一个 JWT。 它还使用JwtSecurityTokenHandler和SecurityTokenDescriptor,它们创建令牌的声明或有效负载。 您将注意到,在这里SecurityTokenDescriptor使用密钥和算法参数调用SigningCredentials来签名令牌。 -
Now let's create a
DependencyInjectionclass that uses a staticIServiceCollectionfor theTravel.Identityproject. Create a C# class in the root directory of theTravel.Identityproject like so:using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Travel.Application.Common.Interfaces; using Travel.Identity.Helpers; using Travel.Identity.Services; namespace Travel.Identity { public static class DependencyInjection { public static IServiceCollection AddInfrastructureIdentity (this IServiceCollection services, IConfiguration config) { services.Configure<AuthSettings> (config.GetSection(nameof(AuthSettings))); services.AddScoped<IUserService, UserService>(); return services; } } }下面是我们将要在
Startup.cs中注册的依赖注入(DI)容器中的已注册服务。 我们正在获取appsettings.json文件中的AuthSettings对象,并为UserService注册一个有作用域的生命周期。AuthSettings的Secret键是一个敏感字符串,必须存储在环境变量或 Azure key Vault 中以保护它。 -
Next, we create a C# file and name it
UsersControllerinside theTravel.WebApiproject, specifically inside thev1folder of theControllersdirectory, like so:using Microsoft.AspNetCore.Mvc; using Travel.Application.Common.Interfaces; using Travel.Application.Dtos.User; namespace Travel.WebApi.Controllers.v1 { [ApiVersion("1.0")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public class UsersController : ControllerBase { private readonly IUserService _userService; public UsersController(IUserService userService) => _userService = userService; [HttpPost("auth")] public IActionResult Authenticate([FromBody] AuthenticateRequest model) { var response = _userService.Authenticate(model); if (response == null) return BadRequest(new { message = "Username or password is incorrect" }); return Ok(response); } } }这个控制器不需要授权,因为它是用于登录的。 它还使用了
v1API 来保持一致性。 控制器对用户进行认证,并使用User的 JWT 进行响应。 -
Now, let's protect the rest of the controllers by adding the custom attribute in
ApiController. Let's updateApiController's code:namespace Travel.WebApi.Controllers.v1 { [Authorize] [ApiVersion("1.0")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public abstract class ApiController : ControllerBase { private IMediator _mediator; protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService <IMediator>(); } }我们用
Authorize属性注释ApiController,这是我们先前创建的自定义属性。 该属性保护ApiController的所有子类不受经过认证的消费者或用户的影响。 -
Now the last file to update is
Startup.cswith the following code:public void ConfigureServices(IServiceCollection services) { services.AddApplication(); services.AddInfrastructureData(); services.AddInfrastructureShared (Configuration); services.AddInfrastructureIdentity (Configuration); services.AddHttpContextAccessor(); services.AddControllers(); … }在
services.AddInfrastructureShared(Configuration);方法下面添加services.AddInfrastructureIdentity(Configuration);方法。 -
Let's also update the
SwaggerGenconfiguration. The code that we are adding will allow us to enter a JWT in the Swagger HTTP requests header through a dialog box:
```
services.AddSwaggerGen(c =>
{
c.OperationFilter
<SwaggerDefaultValues>();
c.AddSecurityDefinition("Bearer", new
OpenApiSecurityScheme
{
Description = "JWT Authorization header
using the Bearer scheme.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer"
});
c.AddSecurityRequirement(new
OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type =
ReferenceType.SecurityScheme,
Id = "Bearer"
}
}, new List<string>()
}
});
});
```
`AddSecurityDefinition`描述了 API 是如何被保护的,而`AddSecurityRequirement`在 API 中添加了全局安全需求。
另外,像这样更新中间件:
```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
…
app.UseRouting();
app.UseMiddleware<JwtMiddleware>();
app.UseAuthorization();
…
}
```
在`app.UserRouting()`和`app.UseAuthorization()`之间插入定制`app.UseMiddleware<JwtMiddleware>()`中间件。 这个代码块是我们之前创建的自定义中间件。
现在,`Travel.Identity`项目被集成到`Travel.WebApi`项目中。
- 运行应用,看看安全性是否在应用和 Swagger 中正常工作。
在运行应用之后,让我们检查 Swagger UI。
检查 Swagger UI
在设置了 JWT 认证并将其添加到 Swagger 后,让我们看看如何在 Swagger UI 中使用和测试受保护的路由:
-
Swagger UI has the Authorize button for adding credentials or JWT, as shown in the following figure, Figure 9.5:
![Figure 9.5 – OpenAPI with the security definition and requirement]()
图 9.5 - OpenAPI 的安全定义和需求
-
Now let's check out the
/api/v1/Users/authroute of theUserscontroller:![Figure 9.6 – Signing in using Swagger UI]()
图 9.6 -使用 Swagger UI 登录
我们可以通过发送一个以
username为yoursuperhero和password为Pass123!的POST请求来测试 auth API,如上图所示。 -
Now let's check out the response of the API after sending the
POSTrequest:![Figure 9.7 – JWT response after successful login]()
图 9.7 -登录成功后的 JWT 响应
前面的屏幕截图显示了来自 auth API 响应体的 JWT,这意味着我们在发送用户名和密码后进行了认证。 让我们复制 token 的值,因为我们将在 Swagger UI 的Authorize框中使用它。
-
Go to the https://jwt.io website and paste the JWT in the left box, as shown in the following screenshot:
![Figure 9.8 – Encoded and decoded JWT]()
图 9.8 -编码和解码的 JWT
我们看到从应用的 auth API 接收到的已编码的 JWT。 同时,右边是已解码的 JWT。 已解码的令牌显示头和有效负载。 报头显示签名或加密算法和令牌类型。 随后,解码的令牌还显示了令牌的有效负载,其中包含用户的 ID,在时间之前无效,过期时间,以及时间颁发的。
-
Now let's check out the Authorize button of Swagger UI by clicking it:
![Figure 9.9 – Including the JWT in every API testing]()
图 9.9 -在每个 API 测试中都包含 JWT
将标记值粘贴到对话框中,允许 Swagger UI 在每个请求中包含
Authorization头,如前面的截图所示。 您可以尝试使用 Swagger 向/api/v1.0/TourLists发送一个GET请求,也可以使用 Postman 中的 JWT。 -
打开 Postman 并在Auth选项卡下选择无记名令牌。 然后将标记粘贴到输入框中。 向
TourLists控制器发送GET请求,https://localhost:5001/api/v1.0/TourLists:

图 9.10 -使用 Postman 进行授权 API 测试
下面的截图显示了一个包含我们所期望数据的 200 状态码:

图 9.11 - 200 受保护 API 的 OK 响应
如果我们发送错误的 JWT 怎么办? 你认为申请表会寄回来什么? 删除标记中的一些字母,并将它们替换为您的名字。 然后按发送按钮发送请求:

图 9.12 -未授权请求
您将看到应用响应 401 状态码和一条Unauthorized消息,如上面的截图所示。
这就是在受保护的路由或端点中使用 Swagger UI 的方式。 现在让我们总结一下在这一章所学到的知识来结束这一章。
总结
以下是本章的摘要。 你已经学过 ASP.NET Core Identity 是。NET 中的一个开源身份框架,它为您提供了管理用户认证和帐户的能力。 它有内置的中间件,允许应用使用基于 cookie 的认证。
您已经了解到,IdentityServer4 框架消除了在您的 ASP 中实现 OAuth 2 和 OpenID Connect 的繁重工作.NET Core 应用。 IdentityServer4 的一个合理用例是,需要一个集中式认证服务器,使用基于令牌的认证对来自不同服务的请求进行认证。
您还了解到,CIAM 是一个基于云的身份提供商,它为公司提供分析、安全性和良好的客户体验。 它具有可伸缩性,具有集中式用户管理,并且易于设置单点登录和 MFA。
你已经学习了如何保护 ASP。 使用 JWT 的 NET Core 5 应用。 应用用一个令牌响应一个经过认证的请求,令牌包含一个用户信息的有效负载。
最后,您还学习了如何在 Swagger UI 中显示受保护的 api,以及如何在 Swagger 的每个请求中包含授权头。
在下一章中,我们将讨论使用 Redis 缓存来提高性能。
十、Redis 的性能增强
缓存是提高应用性能的常用技术。 通常,我们会在内容分发网络(CDN)、HTTP 缓存和数据库缓存中遇到缓存。 缓存通过最小化访问底层较慢的数据存储层的需求来提高数据检索性能。 在本章中我们将学习的缓存技术是内存缓存和分布式缓存。
本章将涵盖以下主题:
- ASP 中的内存缓存.NET Core
- 分布式缓存
- 设置和运行 Redis
- 在 ASP 中实现 Redis。 核心网 5
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019, Visual Studio for Mac,或 Rider
- Redis
AnotherRedisDeskTopManager,见https://www.electronjs.org/apps/anotherredisdesktopmanager
下面是该存储库的最终代码:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter10的链接。
ASP 的内存缓存 NET Core
使用内存缓存允许开发人员将数据存储在服务器的资源中,特别是内存中。 因此,它通过删除对外部数据源的不必要的 HTTP 请求来帮助开发人员提高性能。
在 ASP 中实现内存缓存.NET Core 非常简单。 但是,我们不会在我们的应用中应用它。 我们将选择一种可扩展的缓存方式,即分布式缓存。 我们将只看如何实现内存缓存的部分,以便您有一个想法。
在 ASP 中启用内存缓存 NET Core
重复一遍,我们不会将本节中的代码应用到我们的应用中。 无论如何,你可以在Startup.cs的ConfigureServices中启用内存缓存:
public void ConfigureServices(IServiceCollection services)
{
…
services.AddMemoryCache();
}
方法在。net 中添加了一个非分布式内存实现。 您可以开始使用内存缓存,而不安装任何 NuGet 包。 然后将IMemoryCache注入到需要缓存的控制器中:
[Route("api/[controller]")]
[ApiController]
public class CacheController : ControllerBase
{
private readonly IMemoryCache _memoryCache;
public CacheController(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
}
在从Microsoft.Extensions.Caching.Memory命名空间注入IMemoryCache之后,您就可以开始使用它了。 下面的代码块只检查缓存中是否存在蛋糕列表,如果是true则返回。 否则,它将使用服务并存储结果:
[HttpGet("{cakeName}")]
public async Task<List<string>> Get(string
cakeName)
{
var cacheKey = cakeName.ToLower();
if (!_memoryCache.TryGetValue(cacheKey, out
List<string> cakeList))
{
cakeList = await Service.GetCakeList(
cakeName);
var cacheExpirationOptions =
new MemoryCacheEntryOptions
{
AbsoluteExpiration =
DateTime.Now.AddHours(6),
Priority = CacheItemPriority.
Normal,
SlidingExpiration =
TimeSpan.FromMinutes(5)
};
_memoryCache.Set(cacheKey, cakeList,
cacheExpirationOptions);
}
return cakeList;
}
您还会注意到在代码块中缓存中存在过期。 AbsoluteExpiration表示一个确定的到期日期,而SlidingExpiration用于监视缓存的非活动状态,或者仅仅是将它最后一次使用的时间放置。
虽然内存缓存消耗了它的服务器的资源,但内存缓存比分布式缓存快,因为它是物理上连接到服务器上的,但对于大型和多 web 服务器并不理想。
这里有一条建议。 在运行一个解决方案的多个实例时,不建议使用内存缓存,因为数据将不一致。 在多台服务器上工作时,有一种更好的缓存方法,这将在下一节中讨论。
分布式缓存
分布式缓存或全局缓存是具有专用网络的单实例或一组缓存服务器。 当应用到达分布式缓存时,如果与应用请求相关的缓存数据不存在,请求将重定向到数据库来查询数据。 否则,分布式缓存将只响应应用所需的数据。
这是两个服务器共享同一个分布式缓存实例的图:

图 10.1 -分布式缓存
上图显示了来自两个服务器的请求在决定是否从数据库中查询之前首先访问 Redis 缓存。
如果您的服务中的一个崩溃会发生什么? 实际上什么都没有,因为每个人都将查询分布式缓存。 因为缓存是分布式的,它会维护数据的一致性。 我们可以把所有的信息和所有头疼的事情都转移到分布式缓存中,大部分时候是 Redis。 分布式缓存比内存中慢,但更准确。
需要分布式缓存的原因之一是为了获得更高的精度。 例如,如果服务器崩溃,它不会把它的数据带到坟墓里。 这种方式更有弹性。
另一个原因是你可以独立地扩展分布式缓存或 Redis 缓存。 你可以独立地扩展 Redis 实例,同时保持你的 web 服务正常运行,而不使用他们的资源缓存。
设置和运行 Redis
Redis 官方支持 Linux 和 macOS,但不支持 Windows,因为工程师编写的 Redis 使用 BSD Unix。 Windows 端口是由一些被称为Microsoft Open Tech 小组的志愿者开发人员编写的。
让我们在 Windows、macOS 和 Ubuntu 上安装 Redis。 以下步骤取决于您的操作系统。
Windows 用户
-
Go to https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504 to download the installer of Redis for Windows:
![Figure 10.2 – Redis MSI installer and ZIP file]()
图 10.2——Redis MSI 安装程序和 ZIP 文件
-
Download and extract the Redis ZIP. Double-click the
redis-serverfile. Allow the permission dialog box that will pop up by accepting Yes. The Redis instance will automatically start.检查安装是否完成,在终端中执行如下命令:
redis-cli pingredis-cli是 Redis 功能的 CLI。 您应该看到来自终端的pong响应。
下面是安装 Redis 的另一种方法,使用msi文件从下载链接。
下载并安装msi文件只需点击它。 允许通过接受Yes将弹出的权限对话框。 Redis 实例将自动启动。
检查安装是否完成,在终端中执行如下命令:
redis-cli ping
redis-cli是 Redis 功能的 CLI。 您应该看到来自终端的pong响应:

图 10.3 -解压后的文件和 Windows 终端上运行的 Redis 实例
这里是从 Windows 的 ZIP 文件和 CMD 中提取的文件,其中显示了点击redis-server文件后的 Redis 图像。
如果你正在考虑使用 Chocolatey 包管理器来安装 Redis,那么在写这篇文章的时候,这个 URL 是坏的。 我收到一个错误说404 没有找到。
就是这样。 Redis 现在已经安装在 Windows 10 系统上。
用于 macOS 用户
你可以快速安装 Redis 在 Mac 上使用brew:
-
首先,通过运行以下命令更新
brew: -
接下来,我们通过运行以下命令安装 Redis:
brew install redis -
然后,让我们运行命令启动已安装的 Redis:
brew services start redis -
Now run the following command to check whether Redis is running and reachable:
redis-cli pingredis-cli是 Redis 功能的 CLI。 您应该看到来自 Terminal 的pong响应。注意:
使用
brew的 Redis 安装工作在 macOS 大苏尔,这是自最初的 macOS 以来最大的变化。
就是这样。 Redis 现在已经安装在 macOS 上了。
适用于 Linux 或 Ubuntu 用户
在 Linux 下安装 Redis 很简单:
-
让我们首先通过运行以下命令来更新我们的资源:
-
然后执行以下命令安装 Redis。
sudo apt install redis-server -
现在执行以下命令检查 Redis 是否运行且可达:
redis-cli ping
redis-cli是 Redis 功能的 CLI。 您应该看到来自 Terminal 的pong响应。 就是这样。 Redis 现在已经安装在你的 Linux 机器上了。
所以,这就是在 Windows、macOS 和 Linux 机器上安装 Redis 服务器。 现在让我们在 ASP 中使用 Redis.NET Core 5。
在 ASP 中实现 Redis.NET Core
所以,让我们使用我们刚刚安装在机器上的 Redis 通过将其与我们现有的 ASP 集成.NET Core 5 解决方案。 以下是步骤:
-
Go to the
Travel.Applicationproject and install these NuGet packages. The following NuGet package is a distributed cache implementation of theMicrosoft.Extensions.Caching.StackExchangeRedisnamespace using Redis:Microsoft.Extensions.Caching.StackExchangeRedis下面的 NuGet 包帮助我们检索
appsettings.json中的配置:Microsoft.Extensions.Configuration下面的 NuGet 包是。net 的 JSON 框架:
Newtonsoft.Json -
Next, we update the
DependencyInjection.csfile of theTravel.Applicationproject with the following code:namespace Travel.Application { public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration config) { services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly. GetExecutingAssembly()); services.AddMediatR(Assembly.GetExecutingAssembly()); services.AddStackExchangeRedisCache(options => { options.Configuration = config.GetConnectionString("RedisConnection"); var assemblyName = Assembly. GetExecutingAssembly().GetName(); options.InstanceName = assemblyName. Name; }); … return services; } } }前面的
Travel.Application的依赖注入实现现在需要一个IConfiguration参数。 我们将 Redis 分布式缓存服务添加到依赖注入容器中。 连接字符串的名称是RedisConnection,我们将在下一步中对其进行设置。 -
接下来是转到
Travel.WebApi项目并使用以下代码更新appsettings.json:{ "AuthSettings": { "Secret": "ReplaceThsWithYour0wnSecretKeyAnd StoreItInAzureKeyVault!" }, "ConnectionStrings": { "DefaultConnection": "Data Source= TravelTourDatabase.sqlite3", "RedisConnection": "localhost:6379" }, "Logging": { … }, "MailSettings": { … }, "AllowedHosts": "*" } -
We are adding connection strings for Redis and SQLite3 in this code. Consequently, we are also going to update
DependencyInjection.csofTravel.Data. So, let's update that with the following code:namespace Travel.Data { public static class DependencyInjection { public static IServiceCollection AddInfrastructureData(this IServiceCollection services, IConfiguration config) { services.AddDbContext<ApplicationDbContext>(options => options .UseSqlite(config.GetConnectionString("DefaultConnecti on"))); … } } }Travel.Data的依赖注入文件现在在appsettings.json中定义了DefaultConnection配置。 -
Another thing to do here is to update the
Startup.csfile ofTravel.WebApi. Go to that file and update it with the following code:public void ConfigureServices(IServiceCollection services) { services.AddApplication(Configuration); … services.AddHttpContextAccessor(); services.AddControllers(); … }我们现在在
AddApplication扩展方法中传递IConfiguration Configuration。 这样,Travel.Application可以访问appsettings.json中的RedisConnection。 -
Now let's use Redis to cache the response of the
localhost:5001/api/v1.0/TourListsendpoint to its consumers sending aGETrequest. To do this, we will update the handler ofapi/v1.0/TourListsfor theGETrequest, which isGetToursQuery.GetToursQuery可在Travel.Application/TourLists/Queries/GetTours/GetTours/GetToursQuery.cs中找到。 用以下代码更新GetToursQuery.cs:… using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; … namespace Travel.Application.TourLists.Queries.GetTours { public class GetToursQuery : IRequest<ToursVm> { } public class GetToursQueryHandler : IRequestHandler<GetToursQuery, ToursVm> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDistributedCache _distributedCache; public GetToursQueryHandler( IApplicationDbContext context, IMapper mapper, IDistributedCache distributedCache) { _context = context; _mapper = mapper; _distributedCache = distributedCache; } public async Task<ToursVm> Handle( GetToursQuery request, CancellationToken cancellationToken) { … } } }我们将
Microsoft.Extensions.Caching.Distributed命名空间中的IDistributedCache注入GetToursQueryHandler的构造函数中。 我们将在Handle方法的逻辑中使用distributedCache,出于可读性的考虑,我将其截断。下面的代码是
Handle方法更新后的业务逻辑:public async Task<ToursVm> Handle(GetToursQuery request, CancellationToken cancellationToken) { const string cacheKey = "GetTours"; ToursVm tourLists; string serializedTourList; var redisTourLists = await _distributedCache.GetAsync(cacheKey, cancellationToken); if (redisTourLists == null) { tourLists = new ToursVm { Lists = await _context.TourLists .ProjectTo<TourListDto>(_mapper. ConfigurationProvider) .OrderBy(t => t.City). ToListAsync( cancellationToken) }; serializedTourList = JsonConvert.SerializeObject(tourLists); redisTourLists = Encoding.UTF8.GetBytes(serializedTourList); var options = new DistributedCacheEntryOptions() .SetAbsoluteExpiration(DateTime.Now.AddMinutes(5)) .SetSlidingExpiration(TimeSpan.FromMinutes(1)); await _distributedCache.SetAsync( cacheKey,redisTourLists, options, cancellationToken); return tourLists; } serializedTourList = Encoding.UTF8.GetString( redisTourLists); tourLists = JsonConvert .DeserializeObject<ToursVm>(serializedTourList); return tourLists; }前面的代码块是
GetToursQuery处理程序的更新逻辑。 我们有"GetTours"作为cacheKey,我们将使用它从缓存中检索数据并从缓存中保存数据。cacheKey将用于搜索特定缓存时的查找。我们还通过
_distributedCache.GetAsync检查是否存在现有缓存。 如果没有数据,则序列化tourLists对象并将其保存在缓存_distributedCache.SetAsync中,然后返回tourLists。 我们缓存的数据在 Redis,但我们把过期。SetAbsoluteExpiration设置绝对过期时间,而SetSlidingExpiration设置条目可以不活动多长时间。如果有数据,则返回一个反序列化的
tourLists。现在,在我们继续 Vue.js 下一章,第 11 章,Vue.js 基本面 Todo 应用【显示】,
Startup.cs让我们清理文件,因为它开始变得混乱。我们要做的是将 Swagger 配置移动到它的目录和文件中,然后安排所有服务并删除所有不必要的
using语句。 -
So, go to
Travel.WebApiand create a folder namedExtensionsin therootdirectory of the project. Create two C# files namedAppExtension.csandServices.Extensions.cs. We are moving the Swagger code fromStartup.csto these two files like so:// AppExtension.cs
… namespace Travel.WebApi.Extensions { public static class AppExtensions { public static void UseSwaggerExtension(this IApplicationBuilder app, IApiVersionDescriptionProvider provider) { app.UseSwagger(); app.UseSwaggerUI(c => { ... }); } } }这里,我们将两个中间件从
Configure方法,即app.UserSwagger()和app.UseSwaggerUI()迁移到AppExtension.cs文件中。// ServicesExtensions.cs
… namespace Travel.WebApi.Extensions { public static class ServicesExtensions { public static void AddApiVersioningExtension( this IServiceCollection services) { services.AddApiVersioning(config => { ... }); } public static void AddVersionedApiExplorerExtension(this IServiceCollection services) { services.AddVersionedApiExplorer(options => { ... }); } public static void AddSwaggerGenExtension(this IServiceCollection services) { services.AddSwaggerGen(c => { ... }); } } }在这里,我们将
ConfigureServices方法中的services.AddApiVersion()、services.AddVersionedApiExplorer()和services.AddSwaggerGen()服务迁移到ServicesExtensions.cs。 -
After moving the code to the
Extensionsdirectory, let's refactorStartup.csby calling theextensionmethods that we created like so:public void ConfigureServices(IServiceCollection services) { services.AddApplication(Configuration); services.AddInfrastructureData(Configuration); services.AddInfrastructureShared(Configuration); services.AddInfrastructureIdentity(Configuration); services.AddHttpContextAccessor(); services.AddControllers(); services.AddApiVersioningExtension(); services.AddVersionedApiExplorerExtension(); services.AddSwaggerGenExtension(); services.AddTransient<IConfigureOptions<SwaggerGenOpti ons>, ConfigureSwaggerOptions>(); }现在,让我们看看应用的中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwaggerExtension(provider); } app.UseHttpsRedirection(); app.UseRouting(); app.UseMiddleware<JwtMiddleware>(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }上述代码为
Startup.cs中间件的重构块。 中间件现在比以前更干净了。同样,删除您将在
Startup.cs文件中找到的未使用的using语句。让我们运行应用,看看 Redis 是否正常工作:
-
Send a
GETrequest to/api/v1.0/TourListsusing Postman. Don't forget to include your JWT. The following screenshot shows the response time of the first request to the ASP.NET Core 5 application, which is more than 2 seconds:![Figure 10.4 – API response without Redis cache]()
图 10.4 -没有 Redis 缓存的 API 响应
-
Let's send the same request to the same API to see whether the response time will be shorter:

图 10.5 - Redis 缓存的 API 响应
前面的屏幕截图显示了第二个`GET`请求的较短的响应时间,33 毫秒,这是由于在对相同 API 的第一个`GET`请求期间存储了缓存。
- 要在中查看缓存,你可以使用 Redis 管理器工具。 这是一个免费的复述,马钎子工具,你可以下载并安装,https://www.electronjs.org/apps/anotherredisdesktopmanager,【T7 和付费版本发现 https://rdm.dev/】【显示】。 RDM 是为 Windows 和 macOS 用户提供的付费应用,但不适用于 Linux 用户。
- After running the Redis manager tool, send a new request to the
/api/v1.0/TourListsAPI and check your Redis manager tool.
让我们检查一下 Windows 10 v20H2、macOS Pro Big Sur 和 Ubuntu v20.10 Groovy Gorilla 中的缓存。 这些操作系统是撰写本书时的最新版本。
下面的截图显示`AnotherRedisDeskTopManager`在 Windows 上运行:

图 10.6 - Windows 上的另一个 Redis 桌面管理器
下面的截图显示了在 macOS 上运行的AnotherRedisDeskTopManager:

图 10.7 -另一个在 macOS 上的 Redis 桌面管理器
下面的截图显示了在 Ubuntu 上运行的 Redis GUI:

图 10.8 - Ubuntu 上的 Redis GUI
如果你不喜欢任何仪表盘或 GUI 的 Redis,你也可以使用一个 CLI 命令来调试或监控每一个命令处理你的 Redis 服务器。 运行命令开启监控:
redis-cli monitor
下面的屏幕截图显示了在运行redis-cli monitor后,您的请求在命令行中的样子:

图 10.9 - redis-cli 监视器
代码更新
接下来,我们更新应用中的一些代码,并更改命名约定,这对于前端准备来说非常简单。
以下是包含需要更新的项目、目录和文件的路径。 所以,去这一章的 GitHub repo,写下你的文件中缺少的东西,或者你可以从 GitHub 复制代码并粘贴到你的代码中。
同样,这些是命名约定、新属性和类中的简单更改。
Travel.Domain/Entities/TourList.cs:
public TourList()
{
TourPackages = new List<TourPackage>();
}
public IList<TourPackage> TourPackages { get; set; }
前面的代码是更新TourList类中的Tours。
Travel.Domain/Entities/User.cs:
public string Email { get; set; }
前面的代码正在更新User类中的Username。
Travel.Application/Dtos/Tour/TourPackageDto.cs:
…
public string WhatToExpect { get; set; }
public float Price { get; set; }
public string MapLocation { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<TourPackage,
TourPackageDto>()
.ForMember(tpDto =>
tpDto.Currency, opt =>
opt.MapFrom(tp =>
(int)tp.Currency));
}
前面的代码正在更新TourPackageDtoc 类。
Travel.Application/Dtos/Tour/TourListDto.cs:
public TourListDto()
{
TourPackages = new List<TourPackageDto>();
}
public IList<TourPackageDto> TourPackages { get; set; }
public string Country { get; set; }
前面的代码正在更新TourListDto类。
Travel.Application/Dtos/User/AuthenticateRequest.cs:
public string Email { get; set; }
前面的代码正在更新AuthenticateRequest类中的Username。
Travel.Application/Dtos/User/AuthenticateResponse.cs:
public string Email { get; set; }
…
Email = user.Email;
前面的代码正在AuthenticateResponse中更新Username。
Travel.Application/TourLists/Commands/CreateTourList/CreateTourListCommand.cs:
var entity = new TourList { City = request.City, Country =
request.Country, About = request.About };
前面的代码是在CreateTourListCommand中添加properties。
Travel.Application/TourLists/Commands/UpdateTourList/UpdateTourListCommand.cs:
entity.Country = request.Country;
entity.About = request.About;
前面的代码是在UpdateTourListCommand中添加属性。
在TourPackages目录中创建一个新的文件夹,并将其命名为Queries。 在查询中,创建两个新的 c#文件,并将其命名为GetTourPackagesQuery.cs和GetTourPackagesValidator.cs.
Travel/Application/TourPackages/Queries/GetTourPackagesQueryValidator.cs:
using System.Collections.Generic;
… // for brevity, please see the code in the Github
using Travel.Application.Dtos.Tour;
namespace Travel.Application.TourPackages.Queries
{
public class GetTourPackagesQuery : IRequest
<List<TourPackageDto>>
{
public int ListId { get; set; }
}
public class GetTourPackagesQueryHandler :
IRequestHandler<GetTourPackagesQuery, List<
TourPackageDto>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
public GetTourPackagesQueryHandler(
IApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public Task<List<TourPackageDto>>
Handle(GetTourPackagesQuery request,
CancellationToken cancellationToken)
{
var tourPackages = _context.TourPackages
.Where(tp => tp.ListId == request.ListId)
.OrderBy(tp => tp.Name)
.ProjectTo<TourPackageDto>(_mapper.
ConfigurationProvider)
.ToListAsync(cancellationToken);
return tourPackages;
}
}
}
在前面的代码中没有 ing new。 新文件简单地添加了一个获得旅行团的查询。
Travel.Application/TourPackages/Queries/GetTourPackagesQueryValidator.cs:
using FluentValidation;
namespace Travel.Application.TourPackages.Queries
{
public class GetTourPackagesQueryValidator :
AbstractValidator<GetTourPackagesQuery>
{
public GetTourPackagesQueryValidator()
{
RuleFor(x => x.ListId)
.NotNull()
.NotEmpty().WithMessage("ListId is
required.");
}
}
}
前面的代码行在查询旅行包之前添加了一个新的验证器。
Travel.Identity/Services/UserService.cs:
Email = "yoursuperhero@gmail.com",
var user = _users.SingleOrDefault(u => u.Email ==
model.Email &&
…
u.Password == model.Password);
…
Subject = new ClaimsIdentity(new[] { new Claim("sub",
user.Id.ToString()), new Claim("email", user.Email) }),
前面的代码正在更新UserService类。
Travel.Identity/Helpers/JwtMiddleware.cs:
var userId = int.Parse(jwtToken.Claims.First(c => c.Type ==
"sub").Value);
前面的代码是更新JwtMiddleware类的AttachUserToContext方法。
Travel.WebApi/Controllers/v1/TourPackagesController.cs:
[HttpGet]
public async Task<ActionResult<List<TourPackageDto>>>
GetTourPackages([FromQuery] GetTourPackagesQuery query)
{
return await Mediator.Send(query);
}
上述代码是TourPackagesController的一个新的Action方法。
现在,在您的存储库中更新了 code 之后,是时候挑战自己了。
运动/练习时间:
为了加强您在这里的学习,并在继续到前端部分之前,我希望您创建一个 ASP.NET Core 5 应用。 应用应该使用你在这本书中学到的所有东西,比如干净的架构、CQRS、API 版本控制、OpenAPI 和分布式缓存,无需认证或使用认证,或者使用像 Auth0 这样的身份作为服务来节省你的时间。 我现在能想到的一个应用是电子游戏的在线商店。 实体可以是Developer、Game、GameReviews、Genre、Publisher等。 这个练习很简单,你可以在一周内完成。 我知道你能做到。 好运!
好的,让我们总结一下你在这一章所学到的东西。
总结
你终于读完了这一章,你学到了很多东西。 您已经了解到内存缓存比分布式缓存更快,因为它更靠近服务器。 但是,它不适用于同一服务器的多个实例。
您已经了解了分布式缓存解决了多个实例中的内存缓存问题,因为它为所有服务器实例提供了缓存数据的单一真实来源。
你已经学习了如何安装和运行 Redis 在 pc, macOS 和 Linux 机器,以及如何整合 Redis 到一个 ASP.NET Core Web API 来提高应用的性能,给最终用户带来更好的用户体验。
在下一章中,您将使用 Vue.js 3 构建您的第一个单页面应用。
十一、Todo 应用中的 Vue.js 基础
本章主要介绍 Vue.js、Node.js npm 和 Vue CLI。 这些工具帮助开发人员根据用户的选项为 Vue.js 项目提供不同的配置。 本章还描述了 Vue 组件的特性以及使用它们可以做什么。 不仅如此,您还将了解前端 web 框架的实际结构。 我们会使用 TypeScript 来完成我刚才在 Todo 应用中提到的所有事情。
在本章中,我们将涵盖以下主题:
- 使用 Vue CLI 启动项目
- Vue CLI 生成的文件和文件夹
- 从一个 Vue 组件开始
- Vue 组件中的常见特性
技术要求
以下是你完成本章所需要的东西:
- Visual Studio Code:https://code.visualstudio.com/
- npm:Node Package Manager fromhttps://nodejs.org/en/
- Vue CLI:https://cli.vuejs.org/
本章已完成的知识库可以在以下链接中找到:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter11/vue3-typescript-todo
使用 Vue CLI 启动项目
Vue CLI是标准的开发工具,用于启动 Vue.js 项目。 CLI 允许你在一个项目中添加不同的支持,比如对 Babel、ESLint、TypeScript、Progressive Web Apps(PWAs)、PostCSS、单元测试和端到端测试的支持。
确保你有我们在第二章,设置开发环境中安装的 npm。 如果你有忘记安装 npm,你可以去https://nodejs.org/en/【显示】和安装最新的长期支持(【病人】LTS)版本的 node . js。
如果你不能在第二章,设置开发环境中安装 vae .js CLI,你现在可以运行以下命令:
npm install -g @vue/cli
该命令用于全局安装 Vue CLI。 npm命令的最后一部分是包名,而-g在全局上表示。
*安装 Vue CLI 后,让我们创建我们的第一个 Vue.js 应用,并构建一个简单的 Todo 应用来尝试 Vue 组件的常见功能:
-
Run the following command to create the directory of your Vue.js app:
vue create todo-appvue create命令还将触发 Vue CLI,打开 Vue 应用的一系列配置。 -
You should see the first list of options requesting the preset of the app, as per the following screenshot:
![Figure 11.1 – Vue CLI configuration options]()
图 11.1 - Vue CLI 配置选项
选择
Manually select features并按进入查看要配置的各种选项。 -
The
Choose Vue version,Babel, andLinter / Formatteroptions are enabled by the Vue CLI. Let's also turn onTypeScriptby pointing the cursor toTypeScript, as shown, and then press the spacebar:![Figure 11.2 – Adding TypeScript to the Vue.js CLI configuration]()
图 11.2 -在 Vue.js 的 CLI 配置中添加 TypeScript
按进入将
TypeScript支持添加到我们正在创建的 Vue.js 项目中。 -
Let's use version 3 of Vue.js in the project by selecting the
3.x (Preview)option, as shown in the following screenshot:![Figure 11.3 – Choosing Vue.js 3]()
图 11.3 -选择 Vue.js
2 .按进入选择 Vue.js。
-
下一个问题是我们是否愿意使用
class-style component syntax。 按进入拒绝。 在这个问题中默认选择的值是No,所以按Enter会对这个配置回答 no。 我们将不使用class-style component syntax。 -
另一个问题是我们是否愿意使用
Babel alongside TypeScript。 按输入接受。 Babel 是一个编译器,它将帮助你的 Vue.js 应用自动检测腻子,使 Vue.js 应用与大多数浏览器兼容。 -
As regards
linter / formatterwhile we are writing our code, let's chooseESLint, a static code analysis tool, plusPrettier, an opinionated code formatter, as shown in the following screenshot:![Figure 11.4 – Formatter configuration in the Vue CLI]()
图 11.4 - Vue CLI 中的 Formatter 配置
使用方向键指向
ESLint + Prettier配置后,按进入。 -
对于下一个选项,也就是附加的 lint 特性,选择
Lint on save。 -
然后,选择专用的配置文件,用于优先放置 Babel、ESLint 等的配置。
-
最后,暂时不要接受
saving the preset for future projects。
Vue CLI 将开始创建我们的项目,用 Git 初始化项目,安装 CLI 插件,并添加 JavaScript 包。
现在,启动 VS Code 文本编辑器并安装扩展,即Prettier - Code 格式化器、Vetur和vcode -icons,如下截图所示:

图 11.5 - VS Code 扩展
Vetur 是 Vue 语言服务器,而 vcode -icons 的设计目的是将图标带到 VS code 文本编辑器的文件和文件夹中。
现在你可以使用 Visual Studio Code 打开项目,查看 Vue CLI 为你生成的文件和文件夹:

图 11.6 - Vue CLI 生成的文件和文件夹
前面的屏幕截图显示了 Vue CLI 生成的文件和文件夹,我们将在下一节讨论这些内容。
Vue CLI 生成的文件和文件夹
Vue CLI 已经生成了开始 Vue.js 开发所需的文件和文件夹。 让我们依次来看看这些:
-
node_modules folder:该文件夹包含从 npm 下载的库。 -
public folder:此文件夹包含 HTML 文件和图标。 在公共文件夹中只会看到一个 HTML 文件,因此被称为“单页应用”。 -
src folder:这个文件夹是我们编写业务逻辑、创建 Vue 文件组件以及 JavaScript 或 TypeScript 文件的目录。 -
.browserlistrc:该文件是描述应用目标浏览器的工具。 -
.eslintrc.js:这个文件是 ESLint 的一个配置工具。 -
.gitignore:该文件用于 Git 中不提交目录或文件。 必须忽略的目录的一个很好的例子是node_modules。node_modules的文件大小很大,但是可以通过在根项目目录中运行npm install在项目中检索。 -
babel.config.js:该文件用于 Babel (JavaScript 编译器)的项目范围配置。 Babel 将较老的 JavaScript 版本(ES6+)转换为较低的 JavaScript 版本(ES5 及以下),以便较老的浏览器能够理解。 -
package-lock.json:该文件将项目的依赖关系锁定到特定版本的包。 -
package.json: This file holds the information of our project's dependencies and scripts, which we can use to run in the terminal.我希望大家记住
package.json的三个要素:scripts:scripts属性可以在其中声明一些自定义 CLI 命令。 Vue.js CLI 在我们的项目中有三个默认的npm脚本,即通过vue-cli-service serve命令运行 Vue 项目的服务器,用于构建用于部署的 Vue 项目的构建,以及用于运行 Vue 项目的检查器的 lint。 这里的思想是,我们只需在脚本的块中创建一个键和值对,就可以将长命令转换为短命令。dependencies:dependencies属性只是我们的应用运行所需的关键包列表。 我们通过npm install命令安装的任何库都将在依赖项中列出。devDependencies:devDependencies属性是一个帮助开发者编写应用的包列表。 我们可以通过添加-D标志来明确地告诉 npm 一个库必须包含在devDependencies中。 例如,npm install -D``prettier命令将把更漂亮的包添加到devDepenencies中。 此外,当你为应用运行npm build命令时,devDependencies不会被编译或包含。 -
tsconfig:该文件允许我们声明编译项目时所需的编译器选项。 它还表明该项目是一个 TypeScript 项目。
现在,我们对 Vue CLI 为我们构建的生成的文件和文件夹有了基本的了解。 让我们在 VS Code 的终端中运行下面的命令来运行应用来测试它是否会出现在浏览器中:
npm run serve
点击浏览器中的localhost:8080查看 Vue.js 应用的运行情况,如下图所示:

图 11.7 -启动 UI
启动 Vue.js 应用后,您将在浏览器上看到带有 Vue.js 徽标的欢迎消息。 默认 UI 有一些关于 Vue.js 中特性文档的外部链接。
在看到我们的 Vue.js 应用在浏览器中运行后,我们现在可以在下一节中讨论 Vue 组件及其组件。
开始使用 Vue 组件
Vue 组件是 Vue.js 应用的主要构建块。 组件由template语法、script和style组成。
让我们来看看在我们的应用的root组件App.vue中生成的代码:
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default defineComponent({
name: "App",
components: {
HelloWorld
}
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
下面是一些关于 Vue.js 组件部分的简单明了的解释:
语法部分是我们将应用的状态绑定到我们选择的 HTML DOM 的地方。 您还可以看到在模板中正在使用HelloWorld组件。 我们还将在本章的后半部分创建一些 Vue 组件。
在script部分,我们可以编写 JavaScript/TypeScript 代码,在应用中开发一些业务逻辑,导入文件或包,并定义应用的状态。 状态是一个对象,我们可以在其中存储属性值并在 UI 上呈现它们。
**您可以在脚本块中看到HelloWorld被导入并注册到 components 属性中,以允许App.vue组件在template部分呈现HelloWorld。
在style部分中,我们定义了要创建的组件的样式。 您可以在这里使用 CSS 预处理程序,如 SASS、LESS、Stylus 和 PostCSS,但这些都需要加载器插件才能工作。
现在,从模板和脚本部分删除HelloWorld组件,然后从组件目录中删除它。 在从应用中删除HelloWorld组件后,您应该只能在浏览器中看到 Vue.js 徽标。
让我们在下一节中编写第一个 Vue.js 组件。
编写一个 Vue 组件
让我们在组件目录中创建一个 Vue 组件(文件以.vue格式结束),并将其命名为TodoForm。 文件名应该像这样-TodoForm.vue。 然后编写以下代码:
<template>
<h1>TodoForm Works!</h1>
</template>
如果我们只是在 UI 中呈现一条消息,我们不需要包含任何 JavaScript/TypeScript 代码和 CSS 样式。 现在像这样将它导入到App.vue文件中:
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<TodoForm />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TodoForm from "./components/TodoForm.vue";
export default defineComponent({
name: "App",
components: {
TodoForm,
},
});
</script>
TodoForm在中导入的方式与HelloWorld在 UI 中导入和呈现的方式相同。 现在点击Save按钮,在浏览器中看到结果:

图 11.8 - Vue.js 组件
我们创建 Vue 组件的实现是一种快速而粗糙的方法,但是我们将在下一节中向它添加一些特性。
Vue 组件的常见特性
在本节中,我们将使用一些常见的特性在 Vue 组件构建我们的 Todo 应用。有许多指令(自定义的 HTML 属性),事件,和接口,我们可以使用在 Vue.js,但大部分都是很少使用,随着时间的推移你会倾向于忘记。 因此,在这个 Vue.js Todo 应用的快速演示中,我们将只使用 Vue 组件中最常见的事件、指令和 api。
让我们开始吧。
在 Vue 组件中写入本地状态
因为我们在这个项目中使用的是 TypeScript,所以我通常要做的第一件事就是编写应用中需要的模型。所以让我们在src目录下创建一个文件夹并命名为models。 然后,在models文件夹(src|models|todoModel.ts)中创建一个 TypeScript 文件,并添加以下代码:
export type TodoType = {
id: string;
done: boolean;
content: string;
};
// OR
export interface TodoModel {
id: string;
done: boolean;
content: string;
}
状态的形状或模型可以使用类型或接口来编写。 它们都是一样的; 区别在于您不能实现两个或多个接口,而类型允许我们这样做。 我们将在这里使用一个简单的模型,该模型具有id、done和content基本类型属性。
现在我们将需要一些第三方包。 它们是uuid用于生成应用的唯一标识符,@types/uuid用于生成 UUID 库的Type定义。 当我们使用uuid库时,我们的 Vue.js TypeScript 项目会给我们提供智能提示,最后是 Bootstrap 5 来样式化我们的组件。
因此,让我们运行以下命令开始安装包:
npm i uuid @types/uuid bootstrap@next
i是 install 的缩写,Bootstrap 的@next后缀明确表示我们想要这个库的 alpha 或 beta 版本,因为在撰写本文时 Bootstrap 的稳定版本是 v4。 然而,如果你看到 npm 网站上 Bootstrap 的稳定版本是版本 5,那么在bootstrap之后你就不需要@next了。
现在让我们在 Vue.js 项目中使用 Bootstrap 包,用下面的代码替换App.vue中生成的样式,导入 Bootstrap 的 CSS:
<style>
@import "../node_modules/bootstrap/dist/css/bootstrap.css";
</style>
在根组件中导入 Bootstrap 后,让我们使用 Bootstrap 包中的容器,Bootstrap 中最基本的布局元素之一。 我们可以在div标签中使用它,然后像这样包装 Vue 徽标和TodoForm组件:
<template>
<div class="container">
<img alt="Vue logo" src="./assets/logo.png" />
<TodoForm />
</div>
</template>
点击Save保存更新后的文件,并在浏览器中查看布局的变化。 然后,让我们通过更改 HTML 并向组件添加状态来更新 components 文件夹中的TodoForm.vue。
让我们用以下代码编辑TodoForm组件中的消息:
<template>
<div class="mb-4">
<h1>Vue 3</h1>
<h2>TypeScript Demo {{ version }}</h2>
</div>
</template>
新消息在TodoForm.vue现在 Vue 3 打印稿演示,但文本的胡子语法插值,{{ version }},这个词版本里面,不会显示在 UI 中,因为这是一种 UI 和国家之间的数据绑定。
现在让我们为TodoForm组件创建一个状态:
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "TodoForm",
setup() {
// local state
const version = ref("v1");
return { version }; // same as { version:version }
},
});
</script>
我们需要 Vue 库中的defineComponent和ref。 defineComponent组件是不言自明的。 ref用于创建和跟踪一个本地状态,该状态用于呈现响应性的任何更改,当状态值发生变化时,该状态将重新呈现 UI。 我们调用ref并传递字符串类型的an initial value v1,并将其存储在版本变量中。 然后我们需要将它作为一个匿名 JavaScript 对象的属性返回。
保存并在浏览器上查看 Vue 应用。 你应该看到TypeScript 演示消息后面跟着v1,如下所示:

图 11.9 -使用胡子的文本插值数据绑定
这就是执行数据绑定的简单程度。 现在让我们创建另外两个局部状态,一个具有显式类型,另一个具有隐式类型。
让我们首先导入我们创建的TodoType模型:
import { TodoType } from "@/models/todoModel";
路径中的@符号是tsconfig提供的一个解析别名,帮助我们轻松地直接访问导出的代码或库,而不需要编写太多的点和斜杠。 你可以检查tsconfig文件的属性来查看baseUrl的值和路径:
// implicit type safe
const version = ref("v1");
const newTodo = ref(""); // ref<string> is too verbose
// explicit type safe
const todos = ref<TodoType[]>([]);
用上面指出的新状态更新组件的setup功能。 状态的version和newTodo是用隐式类型编写的。 TypeScript 知道你传递的是什么类型,它可以推断出你从ref返回的是什么类型。 您可以将鼠标悬停在version和newTodo上,您将看到它们是字符串类型。
告诉 TypeScript 编译器初始值类型的显式方法是使用ref中的泛型。 在前面的代码中可以看到,todos是数组的TodoType类型。 这是为了可读性而编写状态静态类型的好方法,而不仅仅是为了智能感知。
您会注意到,我们没有显式地为 version 和newTodo添加类型,这两种类型都是字符串,因为如果值是原始类型,比如布尔类型、字符串类型和数字类型,那么很容易判断值的类型。
好了,我们已经完成了在TodoForm组件中写入状态并渲染它。 现在让我们向TodoForm组件添加功能。
在 Vue 组件中添加一个功能
在 Vue 组件中编写一个函数非常简单。 第一步涉及在TodoType下面导入uuid来为模型的 ID 生成一个 UUID:
import { v4 as uuidv4 } from "uuid";
我们正在导入v4并将其重命名为uuidv4,以提高可读性。
现在我们可以开始添加一个函数,在其中创建一个新的Todo对象:
// explicit type safe
const todos = ref<TodoType[]>([]);
function addNewTodo(): void {
if (!newTodo.value) return;
todos.value.push({
id: uuidv4(),
done: false,
content: newTodo.value,
});
console.log("newTodo:", newTodo.value);
newTodo.value = "";
}
返回void的addNewTodo函数检查将要连接到输入字段的newTodo.value是否为空。 这就是通过访问value属性来获得状态值的方法。 然后,如果值不为空,我们将把一个Todo对象推到 todos 中。 Todo对象将生成一个id,done设置为false,以及一个content属性,其值将来自newTodo。 该函数还记录了newTodo的字符串值,然后将其设置为一个空字符串,以删除用户的输入。
然后,让将addNewTodo函数连同todos和newTodo一起包含在 setup 的返回对象中,如下所示:
return { version, todos, newTodo, addNewTodo };
正如您在前面的代码中看到的,返回状态和函数将使它们可以在 Vue 组件的模板部分中使用。
现在把这个 HTML 表单放在div标签下面,它包装了 Vue3 的 TypeScript 演示:
<form @submit.prevent="addNewTodo">
<div class="mb-5">
<label for="newTodo" class="form-label">New Todo
</label>
<input
class="form-control"
id="newTodo"
placeholder="what's on your mind?"
v-model="newTodo"
name="newTodo"
/>
<div class="m-2">
<button type="submit" class="btn btn-primary">Add
New Todo</button>
</div>
</div>
</form>
@submit,其中是一个提交侦听器,在每次点击带有type="submit"属性的按钮时调用我们创建的addNewTodo函数。 newTodo输入被绑定到输入的v-model,这是一个指令,用于在输入和模型或状态之间创建双向数据绑定。 其余的细节是基本的 HTML 语法和 Bootstrap 样式。
保存文件并检查浏览器。 你会看到在TypeScript Demo v1下面简单地添加了 todo 表单:

图 11.10 -简单的表单
表单还没有准备好呈现我们在输入中添加的内容,但是我们可以在控制台中看到它是否工作。 现在打开你的 Chrome DevTools,进入控制台标签。 将check all emails写入输入字段,点击应用的Add New Todo按钮,可以看到它正在控制台注册:
**
图 11.11 -控制台日志
上面的图 11.11显示了Console标签和newTodo: check all email登录 DevTools。 这是一个快速概念验证(PoC),我们可以用它在我们的 Vue 组件中创建函数。
我们刚刚为 Vue 组件中的一个函数做了一个 PoC。 现在让我们在组件中呈现待办事项列表。
在 Vue 组件中的数组中循环
现在我们可以为TodoType数组添加 UI,这是一个集合,这意味着我们编写一个 todos 循环并在集合或列表中呈现每个项目。 我们可以通过使用v-for指令来实现。
在form标签下面写下以下内容:
<div>
<ul class="list-group">
<li class="list-group-item" v-for="todo in todos"
:key="todo.id">
<h3>
{{ todo.content }}
</h3>
</li>
</ul>
</div>
代码在待办事项中执行一个for each循环,并在列表中呈现每个待办事项的content属性。 你会注意到另一个 Vue.js 指令:key="todo.id",它的目的是帮助 Vue.js 运行时确定当todos状态改变时,列表中的哪个特定对象需要重新渲染。 反过来,DOM 的重新呈现在没有键的情况下性能更高。
key指令前的冒号是v-bind指令的简写,它用于一个或多个数据绑定属性。 key指令(key=" ")需要一个字符串值或数字值才能工作。 在 key 指令中传递什么东西的一个很好的例子就是 ID。 然而,key 指令不一定是 ID,重要的是它必须是唯一的。
在字段中输入check all emails,然后按输入,再次尝试该表单。 添加后,用户界面会看到检查所有邮件,如下截图所示:

图 11.12 -呈现待办事项列表
现在我们可以在控制台日志中呈现我们的待办事项,在浏览器的屏幕上呈现。
如果我们想在todos状态为空的情况下渲染某些内容,该怎么办? 如果我们想要将 todo 标记为 done,并将 todo 的样式从普通更改为带划线的呢? 这就是我们接下来要写的。
Vue 组件中的 If-else 条件
在本节中,我们有两个目标。 我们的第一个目标是,如果todos状态为空,用一个悲伤的表情符号显示一个空列表的消息。 你可以从以下链接获得:https://emojipedia.org/。
如果todos状态不为空,我们可以开始显示待办事项列表并隐藏Empty list以一个悲伤的面消息。
将空列表 UI 的代码放在表单标签和待办事项列表之间,如下所示:
</form>
<div v-if="todos.length === 0">
<h3>Empty list 🥺</h3>
</div>
<div v-else>
<ul class="list-group">
在这段代码中,我们使用v-if指令有条件地渲染一个块,而可选的v-else指令在条件中作为else。
检查浏览器并刷新页面,以查看新的 UI图 11.13。 当todos状态为空时,你应该看到一个悲伤的表情:

图 11.13 -带悲伤表情的空列表
当todos状态不再为空时,带悲伤表情的空列表立即消失。 简单的对吧?
本节的第二个目标是创建一个功能,其中可以更新Todo对象的done属性。 我们将通过在 todo 内容上标记一个删除线来让用户知道done是否已经变成true。 删除线是一种简单的 CSS 样式,我们也将写一些。
让我们开始。 编写一个名为toggleDone的新函数,它接受一个TodoType对象,如下所示:
function toggleDone(todo: TodoType): void {
todo.done = !todo.done;
}
toggleDone函数在每次调用该函数时将done属性更新为其相反的值。
现在让我们创建一个 CSS 类,把它放在一个style节中,像这样:
<style>
.mark {
text-decoration: line-through;
}
</style>
代码中的样式是一个贯行,将给 todos 内容一个划线,作为标记。
现在用以下新属性更新h3标记:
<h3
:class="{ mark: todo.done }"
style="cursor: pointer"
@click="toggleDone(todo)"
>
:class是v-bind:class的缩写,是一个类指令,它根据todo.done的布尔值动态切换类。 如果todo.done为true,则mark classstyle 被激活;如果todo.done为false,则mark classstyle 被关闭。 简单而有用的。
你还会注意到这里有@click。 @符号是v-on指令的简写,它监听 DOM 事件以运行或调用 JavaScript 函数。 当您开始在任何 HTML 标记中键入@符号时,您可以在编辑器的智能感知中看到所有事件。 你会得到mousehover,blur,keyup等等。
让我们检查一下和toggleDone函数。 toggleDone函数也被放置,并且我们将传递来自v-for循环指令的 todo 对象,您将在<li>元素中找到该指令。 现在我们可以测试应用的新功能了。
然后输入check all emails和yoga。 然后点击检查所有邮件,用划线标记,如下图所示:

图 11.14 -标记划线所做的事情
删除线意味着todo对象中的done属性被设置为true。
让我们创建更多的功能,例如标记列表中的所有项,删除列表中的所有项,或者只删除一个项。
编写一个带有number类型的removeTodo函数,如下所示:
function removeTodo(index: number): void {
todos.value.splice(index, 1);
}
我们将从待办事项列表中删除一个特定的todo对象。
在组件的setup函数的返回对象中包含removeTodo:
return {
…,
removeTodo,
};
removeTodo现在可以在 HTML 中使用了。
现在让我们更新一下template语法,特别是li标签:
<ul class="list-group">
<li
class="list-group-item d-flex flex-row justify-content-
between align-items-center"
v-for="(todo, index) in todos"
:key="todo.id">
<h3
我们正在更新li标签的类和v-for。 您将注意到在v-for中有第二个参数index。 这是我们从循环中得到的 todo 项的索引。 是的,我们可以接触到。
接下来,我们将以下代码添加到h3标签:
<button
type="button"
class="btn btn-warning"
@click="removeTodo(index)"
>
✔ Done & Remove
</button>
我们将索引传递给removeTodo函数,并将按钮命名为Done & Remove并使用一个检查表情符。
现在我们再写两个函数,命名为markAllDone和removeAllTodos,如下所示:
function markAllDone(): void {
todos.value.forEach((todo) => (todo.done = true));
}
function removeAllTodos(): void {
todos.value = [];
}
markAllDone将所有已完成的待办事项设置为true,而removeAllTodos将待办事项设置为空数组,删除列表中的所有项目。
然后,在return语句中包括markAllDone和removeAllTodos,就像这样:
return {
…,
markAllDone,
removeAllTodos,
};
markAllDone和removeAllTodos现在可以在TodoForm组件的template语法中使用。
现在,让我们在add new Todo按钮下面添加两个新的按钮:
<div class="m-2">
<button
type="button" class="btn btn-danger"
@click="removeAllTodos">
Remove All
</button>
</div>
<div class="m-2">
<button
type="button"
class="btn btn-success"
@click="markAllDone">
Mark All Done
</button>
</div>
这两个新按钮处理removeAlltodos和markAllDone功能。 运行应用并检查浏览器。 我们会得到以下截图:

图 11.15 -删除所有并标记所有完成
添加另一个 todo 标题order pizza,然后单击标记所有完成按钮。 您应该看到所有的待办事项都执行了,如图 11.15 中所示。 然后,点击Remove All按钮删除所有待办事项。
我相信您会发现 Vue.js 很容易使用,而且添加功能并不难。 现在,让我们在下一节中通过将状态从父组件向下传递到子组件来让它变得更复杂一点。
道具的创建和传递
在某些情况下,父组件必须将一个状态或函数传递给它的子组件,以呈现 UI 或创建功能,我们通过道具来实现。 Props 是一个在中也会用到的术语,其他 JavaScript 框架或库,如 Angular、React、苗条和 Ember。
首先,让我们在TodoForm组件中定义道具。 从 Vue 导入PropType以获得类型安全。 如果你的 Vue 项目使用的是 JavaScript,那么就不需要这个,但是如果你使用的是 TypeScript,那么就会很有用:
import { defineComponent, ref, PropType } from "vue";
导入PropType后,在defineComponent上面创建一个模型并命名为Props,如下所示:
import { v4 as uuidv4 } from "uuid";
type Props = {
title: string;
subTitle: string;
};
export default defineComponent({
您可以使用类型或接口来创建模型。 现在,让我们定义TodoForm的about props,并将其放在设置之前,如下所示:
name: "TodoForm",
props: {
about: {
type: Object as PropType<Props>,
required: true,
},
},
setup() {
我们的道具命名为about。 about是必需的道具,它是PropType形状为Props的物体。
接下来是用title字符串和 subTitle 字符串属性创建一个状态名,如下所示:
// Vue 3
setup() {
const about = ref({
title: "Vue 3",
subTitle: "TypeScript demo",
});
return {
about,
};
},
// Vue 2
/*
data:() => ({
about: {
title: "Vue 3",
subTitle: "TypeScript demo"
}
})
*/
导入ref并使用它来创建一个状态,将其存储在about中,然后返回about。 您将注意到,我在设置之上放置了一个 Vue 3 标签,在数据函数之上放置了一个 Vue 2 标签。 这是在 Vue.js 中编写组件的两种不同方式,当你开始在互联网上寻找一个示例 Vue 项目时,你会遇到这两种方式。 我想说,90%的在写这篇文章的时候,你会看到什么,直到未来几个月将写在 Vue.js 2 因为 Vue.js 3 刚刚发布 2020 年 9 月,大多数 Vue.js 社区和图书馆作者还没有采用最新 Vue.js 版本。
在通过App.vue组件的about之前,我们需要更新TodoForm的设置。 所以回到TodoForm.vue组件,像这样更新设置:
setup(props) {
我们可以在设置中使用可选的参数。 第一个参数是props,它是将传递给子组件的任何状态和函数的入口点。
让我们将props映射到TodoForm的template语法,替换 Vue 3 TypeScript 演示消息:
<template>
<div class="mb-4">
<h1>{{ about.title }}</h1>
<h2>{{ about.subTitle }} {{ version }}</h2>
</div>
其想法是,从现在开始,欢迎消息TodoForm将来自其父组件。
现在,让我们更新App.vue组件的template部分:
<TodoForm :about="about" />
这里我们将App.vue的about状态传递给TodoForm的about、:about和props。
你应该看到 UI 仍然在工作,如下面的截图所示:

图 11.16 -带有 about props 的 TodoForm
Vue3TypeScript 演示再次呈现。 如果你没有看到 Vue 3 的 TypeScript 演示,而你想要调试它怎么办? 我们可以在道具中使用一个控制台日志,我们将在下一部分中进行操作。
Vue 组件中的生命周期挂钩
生命周期钩子是在组件生命周期的每个特定点自动调用的函数或方法。 我们可以利用组件生命周期中的关键事件来编写应用中的业务逻辑。
让我们使用最常用的生命周期钩子,它们是 Vue 3 中的onMounted或 Vue 2 中的mounted。 onMounted在 DOM 被挂载时运行或触发:
import { defineComponent, ref, PropType, onMounted } from "vue";
我们正在从 Vue 进口onMounted
然后我们使用onMounted,并将其放在setup函数的return语句之前,如下所示:
onMounted(() => console.log(props.about.title));
return {
我们在这里记录about属性的title值,当你打开 Chrome DevTools 或 Firefox DevTools 时,你会看到,如下截图所示:

图 11.17 -控制台日志记录道具
您可以从App.vue组件的about the state传递的about道具中看到 Vue3 日志。 onMounted是当你在用户界面上自动呈现来自 web 服务的数据时,你将总是使用的。 我们将在下一个应用中对 HTTP 请求进行onMounted处理,这个实际的 Vue.js 应用使用 ASP。 净的核心。
就是这样。 您可以使用我们刚刚创建的 Todo 应用来玩。 现在让我们总结一下您在构建这个简单的 Todo 应用时学到的所有内容。
总结
我们在这一章做了很多事情。 您已经了解了 Vue CLI 是搭建项目的好工具,可以为开发人员节省大量时间。 您了解了 Vue 组件的各个部分,即模板块(它是 Vue 的 UI 部分)、用于编写业务逻辑的脚本部分和用于样式化组件的样式块。 您还学习了如何创建 Vue 组件以及如何使用 Vue 中的公共接口,例如用于循环的v-for、用于写入事件的v-if条件和@符号。
您还能够学习如何使用ref在 Vue 组件中编写状态,以及如何使用冒号前缀或双花括号进行数据绑定。
最后,您了解了什么是道具以及如何在两个 Vue 组件之间传递道具。 您还了解了什么时候在 Vue 组件中使用生命周期钩子来触发函数。
在下一章中,我们将开始开发现实世界中的企业 Vue.js 应用。
进一步阅读
我承认我的记忆力很差。 这就是为什么,每当我在软件开发中学习一项新技术时,我都需要备忘单来帮助我记住一项技术的关键方面。 以下链接是 Vue.js 中的备忘单:
- Vue Essentials 小抄:vuemastery.com/pdf/Vue-Essentials-Cheat-Sheet.pdf
- Vue.js 备忘单:devhints。 io/vue*****
十二、使用 UI 组件库和创建路由和导航
在本章中,您将学习如何使用由不同 Vue.js 社区构建的开源 UI 库。 您将能够使用 Vue.js 中流行的库之一,这将节省您花费大量时间构建组件。 然后,你将根据最佳实践来设置你的 Vue.js 3 应用的导航和路由。
我们将涵盖以下主题:
- 使用第三方 UI 组件库
- 使用其他第三方 UI 库
- 添加导航栏
- 编写页面组件
- 设置 Vue 路由器的延迟加载和快速加载
技术要求
你需要 Visual Studio Code 来完成本章。 本章已完成的项目可在此链接https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter12/找到。
使用第三方 UI 组件库
什么是UI 组件库? 它仅仅是一个提供一系列现成 UI 组件的库,比如日期选择器、表单、卡片、导航、按钮——你能想到的。 应用中需要的大多数 UI 组件都可以在成熟的 UI 组件库中找到。
使用第三方 UI 组件库的好处是什么,无论它是开源的、免费的还是付费的,并且得到 UI 组件库供应商的高级支持? 下面是 UI 组件库可以给你的好处:
- 节省时间:如果创建一个健壮的日期选择器与大量的配置,更少的错误,和跨浏览器支持将带你周完成,使用第三方的 UI 组件库是一个显而易见的——一个 UI 组件库,您可以导入和尝试一个日期选择的 UI 组件库在不到 4 分钟。
- 省钱:因为 UI 组件库为你节省了无数的时间,它为你的客户或公司节省了金钱。
- 可靠:顶级 UI 组件库得到了高度的维护,并且拥有非常大的社区来改进这些库。
- 上市时间:如果你能比你的竞争对手早几个月发布应用,那将会导致公司的成功。
这里的主要收获不是重新发明轮子和意识到你的客户,客户或公司的资源支出。 优秀的开发人员知道如何为节约资源做出贡献。
现在我们知道了为什么要使用 UI 组件库,让我们在 Vue.js 项目中包含一个。
设置 Vue.js 项目,安装 UI 组件库
让我们使用创建一个新的 Vue.js 项目,步骤如下:
-
Go to the
presentationdirectory of theTravelapplication, and we will run the Vue CLI commands again. Inside thepresentationfolder of thesrcdirectory, run the following command:vue create vue-app该命令将打开 Vue.js 项目的配置选项。 按空格选择
Manually,然后输入。 -
与上一章启用 TypeScript 不同,这次不要启用它,而是启用
Router和Vuex。 供参考,Vuex用于状态管理。 Vuex 版本 3 和 Vuex 版本 4 具有相同的接口。 -
Keep
BabelandLinterenabled. The new Vue.js project's preset should look like Figure 12.1:![Figure 12.1 – New Vue.js project's preset]()
图 12.1 -新的 Vue.js 项目的预设
-
After selecting the preset, choose
2.xfor the version of Vue.js as in Figure 12.2:![Figure 12.2 – Vue.js version]()
图 12.2 - Vue.js 版本
-
在选择
2.x版本后,Vue CLI 会询问你是否想要使用路由器的历史模式。 只需按键盘上的回车键,选择Y或 yes。 -
然后选择
ESLint + Prettier作为检查器和格式化器。 在Save选项上选择带有 lint 标签的配置选项。 -
最后,把 Babel 和 ESLint 的配置放在专用的配置文件中。
在使用 Vue CLI 创建一个项目之后,presentation目录应该类似于图 12.3:

图 12.3 -表示目录
Travel.WebApi和vue-app项目应该在presentation文件夹中。 然后运行新创建的项目:
npm run serve
这个命令将运行带有徽标和消息的 Vue.js 项目,该消息表示欢迎来到 Vue.js 应用。 为什么这次我们选择了 JavaScript 和 Vue.js 版本 2 呢?
Vue.js 版本
我们选择 Vue.js 版本 2,因为互联网上 95%的教程都在 Vue.js 2 中。 Vue.js 3 的大多数第三方库和插件还不能与该版本兼容。 遗憾的是,尽管 Vue.js 3 在 4 个月前就发布了,但大多数第三方库和插件的作者和贡献者仍然在将他们的代码迁移到最新的 Vue.js 版本。 向所有努力工作并将时间花在或大或小任务上的贡献者和作者喊话; 你们让 Vue.js 的开发体验如此之好。
这就是为什么我们在这个企业项目中使用 Vue.js 2 而不是 Vue.js 3。 您应该记住这一点,因为如果您喜欢的库或框架有一个新的主要版本,您就应该这样做。 为什么?
因为A,可能大多数外部库还不能与最新版本兼容。 最好创建一个小的概念证明,让典型的库在框架的最新版本上工作。 如果其他库与框架的最新版本不兼容,那么您应该等待几个月,直到最新版本成熟。
还有B,没有足够的文档。 通过回答以下问题来研究最新的工具:
- 有多少问题和答案来自最新版本?
- 有多少博客展示了使用框架的最新版本构建的完整功能的应用?
- 该框架的最新版本已经发布了多少 YouTube 教程?
如果您没有找到足够的文章或问答,这意味着该框架还不成熟,这是一个很好的迹象,不要在企业应用中为您的客户、客户或公司使用它。
现在让我们回到 Vue.js 项目并安装一个 UI 组件库。
在 Vue.js 应用中安装 Vuetify
什么是Vuetify? Vuetify 是一个针对 Vue.js 的材料设计框架。 它在 npm 网站上每周有 25 万次下载量。 如果你不熟悉材料设计,它是由谷歌创建的一个设计系统,他们在他们的移动和网络应用中使用它。 Material Design 非常流行,它也被移植到 Angular、React、slim、Ember.js 和其他 JavaScript 框架中。
你可以在网站上查看所有现成的组件,https://vuetifyjs.com/en/或你可以直接进入 https://vuetifyjs.com/en/components/alerts/看到 sample 组件。
在 Vue 应用中使用或添加 Vuetify 库非常简单。 我们将使用 Vue CLI,特别是 Vue CLI 的add命令,该命令将通过配置自动设置 Vuetify 库。 稍后您将了解更多关于我刚才提到的配置的内容。 现在,让我们开始:
-
Stop the running Vue.js app and run the following Vue CLI command:
vue add vuetify该命令将开始安装
vuetify插件。 -
Choose the default preset option as in Figure 12.4:
![Figure 12.4 – Preset options]()
图 12.4 -预设选项
默认预设为所有框架服务提供额外的基线值。
-
After adding Vuetify in the project using the Vue CLI, you will see some changes in the project's files. See Figure 12.5:
![Figure 12.5 – Changes in the project files]()
图 12.5 -项目文件中的变更
Vue【显示】CLI 文件创建了一个
vue.config.js,配置文件 transpiling Vuetify,vuetify.js文件,将 Vuetify 全球功能添加到应用。Vue CLI 还将更新package.json,index.html,App.vue,main.js,logo.svg和HelloWorld.vue文件。 -
重新运行 Vue.js 应用,看到欢迎消息和 UI 发生了变化,如图所示:

图 12.6 -欢迎访问 Vuetify
新的欢迎信息意味着 Vuetify 已经完全安装并在 Vue.js 应用中正常工作。
在我们继续构建导航栏之前,让我们在下一节中查看其他 UI 组件库选项。
其他第三方 UI 库
Vuetify 不是你可以在 Vue.js 中使用的唯一的 UI 组件库。 还有其他有用的 UI 组件库。 这里有一些你应该检查的库:
-
BootstrapVue: This is a popular library for Vue.js that lets you design components using the Bootstrap design system. It has 250,000 weekly downloads on the npm website. You can check it out at https://bootstrap-vue.org/.
图 12.7 显示了你可以在 Vue.js 应用中轻松使用的 BootstrapVue 按钮:

图 12.7 - BootstrapVue 的按钮组件
-
Buefy: This is also a popular library for Vue.js, which uses the design system of Bulma and has 45,000 weekly downloads on the npm website. You can check it out at https://buefy.org/.
图 12.8 显示了添加到 Vue.js 应用的 Buefy 按钮:

图 12.8 - Buefy 的按钮组件
-
PrimeVue: This is a UI component library based on PrimeFaces designs, which is also available in React, Angular, and Java web applications. You can check it out at http://primefaces.org/primevue/.
图 12.9 展示了你也可以在 Vue.js 应用中轻松使用的 PrimeVue 按钮:

图 12.9 - PrimeVue 的按钮组件
-
Quasar: This is technically not a library but a framework on top of Vue.js. It is also based on Material Design but has its own CLI and can be compiled to create desktop apps using Electron and iOS and Android apps using Cordova or Stencil. I would recommend this if you are planning to reuse your code base for cross-platform development.
类星体框架还提供了 SSR 或服务器端渲染,这有助于搜索引擎索引您的 Vue.js 应用的页面和内容。以下是类星体框架的链接:https://quasar.dev/。
图 12.10显示类星体按钮设计,你可以在类星体网站上看到:

图 12.10 -类星体的按钮组件
我想在这里提到的另一件好的事情是,新加入的 Vue.js 开发人员可能已经熟悉了我列出的 UI 组件库。 这将使入职过程更快,并将使新聘用的 Vue.js 开发人员在几天内具有生产力。
现在让我们回到 Vue.js 项目,为它构建顶部栏导航和侧边栏导航。
添加导航栏
在本节中,我们将构建一个顶部导航栏来导航到应用的不同主页面。我们还将在管理仪表板页面中创建一个侧边栏,以允许我们在管理仪表板中的其他页面之间导航。 让我们来看看这些步骤:
-
首先在
src目录中创建一个components文件夹。 -
After creating a
componentsfolder, create a Vue component file in thecomponentsfolder and name itNavigationBar. The path should look like this:|
components|NavigationBar.vue。 -
Now, in the
NavigationBar.vuefile, add the following code:<template> <v-app-bar app color="primary" dark> <div class="d-flex align-center"> // for brevity, please see the code in the // github repo </div> <v-spacer></v-spacer> <div> // for brevity, please see the code in the // github repo </div> </v-app-bar> </template> <script> export default { name: "NavigationBar"}; </script> <style scoped> .menu { color: white; text-decoration: none; } </style>前面的代码呈现了一个导航栏,我们将它放在 web 应用的顶部。 设计来自 Vuetify 的应用条; 我从 Vuetify 的文档中复制了代码,并将其粘贴到 VS code 中,但根据我们的需要进行了修改。 这里是应用栏设计的链接:https://vuetifyjs.com/en/components/app-bars/。 我建议看看 Vuetify 的页面,熟悉编写 Vuetify 组件。
例如,您将注意到代码中有
v-前缀-<v-app-bar>。 这意味着自定义 HTML 来自于 Vuetify 的组件。您还可以在代码中找到 Vue.js
router-link组件。 它用于导航,默认情况下将作为超链接标记呈现。 我们在router-link组件的to、attribute或props中设置目标路线。接下来,看一下
<v-btn>组件并检查:to,其中我们传递了一个带有path属性的对象来设置目标路由。这里您可能注意到的最后一件事是,我们没有从
Vue导入defineComponent,因为它只在 Vue.js 3 中工作。 我们可以简单地使用export default {}与其他组件共享NavigationBar.vue。 -
Now that we have our top navigation bar, let's update the
App.vuecomponent with the following code:<template> <v-app> <NavigationBar /> <v-main> <router-view /> </v-main> </v-app> </template> <script> import NavigationBar from "@/components/NavigationBar"; export default { name: "App", components: { NavigationBar, }, }; </script>我们正在用此代码替换
App.vue的代码,该代码导入并使用NavigationBar组件。 -
Now let's update the
About.vuecomponent with the following code:<template> <div class="about fill-height d-flex justify-center align-center"> <h1>About us 🙋🏿🙋🙋🏽</h1> </div> </template>这只是一条带有表情符号的信息。
-
Let's also update the
Home.vuecomponent with the following code:<template> <div class="home fill-height d-flex justify-center align-center"> <h1>Welcome to Travel Tours 🗺🧳 ✈️</h1> </div> </template> <script> export default { name: "Home", }; </script>我们正在用一个简单的欢迎消息更新
Home.vue组件。 -
运行 Vue.js 应用,查看 UI 的变化,如图 12.11:

图 12.11 -主页
屏幕截图显示了我们开发的当前 UI。 它有一个顶部导航栏和主页上的欢迎信息。 我们的大部分功能都在仪表板和登录表单上,所以让我们在下一节中添加更多的页面。
编写页面组件
现在让我们开始为我们的 Vue.js 应用特别是管理员添加页面:
-
In the
viewsdirectory, create a new folder and name itAdminDashboard. CreateDefaultContent.vue, the page forAdminDashboard. TheDefaultContentpage will be the default content of the application when a user goes to the/admin-dashboardpage. Here is the code forDefaultContent.vue:<template> <div> <div> <div class="text-h2 my-4">DefaultContent</div> </div> </div> </template> <script> export default { name: "DefaultContent", }; </script>代码足够简单,可以显示概念证明,我们可以使用带有浏览器屏幕上文本的
/admin-dashboard路径导航到此页面。 我们将在接下来的章节中进行更新。 -
Create another Vue component and name it
TourLists.vue. Write the following code in the TourLists page:<template> <div> <div> <div class="text-h2 my-4">TourLists</div> </div> </div> </template> <script> export default { name: "TourLists", }; </script>旅游列表页面为
/admin-dashboard/tour-lists路径。 -
Create another Vue component and name it
TourPackages.vue. The following code is for the TourPackages page:<template> <div> <div> <div class="text-h2 my-4">TourPackages</div> </div> </div> </template> <script> export default { name: "TourPackages", }; </script>TourPackages页面为
/admin-dashboard/tour-packages路径。 -
Next, create another Vue component and name it
WeatherForecast.vue. We are going to use the WeatherForecast API in our ASP.NET Core application here, but that will be in the next chapter. So for now, here is the code for the WeatherForecast page:<template> <div> <div> <div class="text-h2 my-4">WeatherForecast</div> </div> </div> </template> <script> export default { name: "WeatherForecast", }; </script>天气预报页面将有
/admin-dashboard/weather-forecast路径。 -
Then, create an
index.vuecomponent for theAdminDashboardfolder. This Vue component will contain the sidebar navigation for the dashboard. Here is the code for theindex.vuefile:<template> <v-sheet height="100vh" class="overflow-hidden" style="display: flex; flex-direction: row; justify-content: flex-start" > <v-navigation-drawer permanent expand-on-hover> // For brevity, please see the code in the // github repo of this chapter. Thank you. </v-navigation-drawer> <v-container> <router-view /> </v-container> </v-sheet> </template>您可以在代码中看到的来自 Vuetify 的
v-navigation-drawer组件是一个现成的组件,您可以从 https://vuetifyjs.com/en/components/navigation-drawers/复制和粘贴它。 虽然我复制粘贴了代码,但我仍然需要根据我们在应用中创建的页面进行调整。 您可以看到,代码中有一小部分用于配置文件 UI。 还有一些链接,一个到/admin-dashboard的链接,一个到/admin-dashboard/tour-lists的链接,一个到/admin-dashboard/tour-packages的链接,一个到/admin-dashboard/weather-forecast的链接,以及一个用于注销功能的链接。 -
接下来是
AdminDashboard/index.vue组件的组件名称和样式:<script> export default { name: "AdminDashboard", }; </script> <style scoped> .link { text-decoration: none; } </style>
我们这里只有一个link类来删除text-decoration,但是我们会在接下来的章节中更新它。
在完成本节之前,让我们在views目录中创建一个新文件夹,并将其命名为Main。 将About.vue组件和Home.vue组件移动到Main文件夹。 将About页和Home页移动到Main文件夹,我们可以得到更好的文件夹结构。 Main文件夹将包含仪表板之外的所有页面,而AdminDashboard文件夹将包含所有仪表板页面。
现在我们已经完成了创建仪表板页面,让我们在下一节中更新路由文件。
设置 Vue 路由器的延时加载和延时加载
为了将导航到我们创建的页面,必须在路由中注册一些指向各自组件的路径。 在这种情况下,让我们进入router/index.js文件来更新它:
-
我们将更新主页页和关于页的路径,因为我们将把它们移动到一个新文件夹
views/Main/:import Vue from "vue"; import VueRouter from "vue-router"; import Home from "@/views/Main/Home"; import TourLists from "@/views/AdminDashboard/TourLists"; import TourPackages from "@/views/AdminDashboard/TourPackages"; Vue.use(VueRouter); const routes = [ { path: "/", name: "Home", component: Home }, /* lazy loading through dynamic import() */ { path: "/about", name: "About", component: () => import("@/views/Main/About") }, { path: "/admin-dashboard", component: () => import("@/views/AdminDashboard"), children: [ { path: "", component: () => import("@/views/ AdminDashboard/DefaultContent") }, { path: "weather-forecast", component: () => import("@/views/ AdminDashboard/WeatherForecast") }, /* eager loading through static import statement */ { path: "tour-lists", component: TourLists }, { path: "tour-packages", component: TourPackages } ] }, { path: "*", redirect: "/" }, ]; const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes, }); export default router; -
Then we add the routes for the pages we created. The
vue-routerlibrary is responsible for creating routes in our Vue.js project. We are defining all the routes inside the array and passing the array inVueRouterwhile creating an instance of it.看看数组中对象内部组件的值。 您会注意到我正在展示导入组件的不同方法。 一种是动态导入,其中使用
import(),另一种是静态导入。 现在,为什么我们需要这样做? 这里使用动态导入,只在用户导航到特定页面时导入组件。 根据用户看到的 url 或 ui 按需导入组件或页面称为惰性加载。
Lazy loading是拆分代码的方式。 您还会听到术语“代码分割”而不是“惰性加载”。 当我们的 web 应用在浏览器中加载时,我们这样做是为了不因下载文件而导致网络拥塞。 因此,如果 web 应用在浏览器中同时有许多文件和大文件下载,就会发生网络瓶颈。 延迟加载提高了 web 应用的性能。
另一方面,预先加载是静态导入的作用。 当用户访问 web 应用时,任何页面的资源文件都会被立即下载。 如果由于网络瓶颈,加载应用的时间超过 5 秒,那么对于 web 访问者来说,这是一个糟糕的用户体验。
你可以在https://router.vuejs.org/查看 Vue 路由器的所有 api。
现在让我们再次运行我们的应用,并导航到仪表板内的不同页面:
-
Go to the dashboard by clicking the DASHBOARD menu. Open up your browser's DevTools, go to the Network tab, click All for the types, and clear out the logs as shown in Figure 12.12:
![Figure 12.12 – Dashboard default content and DevTools]()
图 12.12 -仪表盘默认内容和 DevTools
确保到删除日志,以便快速查看屏幕上是否出现一个或多个文件。 我们正在准备我们的 web 应用,以快速演示如何延迟加载工作。
-
Hover your mouse on the sidebar so you can see the navigation menus of the sidebar, like in Figure 12.13:
![Figure 12.13 – Navigation menus of the sidebar]()
图 12.13 -侧栏的导航菜单
-
现在转到Tour Lists页面和Tour Packages页面,同时查看 DevTools。 在 DevTools 的Network选项卡中没有下载的文件,因为这些页面是使用静态导入导入的。
-
转到天气预报页面,注意到脚本和样式表文件将登陆到 DevTools 上,如图 12.14:

图 12.14 -天气预报页面的延迟加载脚本文件和样式表文件
当我们导航到Weather Forecast页面时,就添加了这两个文件,使用动态导入来延迟加载文件。 假设在 Vue.js 应用的中有超过 50 页。 如果您延迟加载应用的所有 50 页,您的应用将在加载速度方面获得的改进。
还有一个建议:在 React、Angular、slim、Ember.js、Preact、Solid.js 或任何你将来会遇到的 JavaScript 框架中也应用代码拆分。
现在,在我们开始列出我们所学到的知识并结束这一章之前,我希望你们记住我们在开始一个项目时遵循的过程。 以下是我多年开发网页和手机应用后所想到的创建应用的步骤:
- 安装 UI 组件库或使用流行的 GitHub 存储库中的现成样板。
- 研究应用中需要的页面,知道有多少页面,以及是否需要一个仪表板管理员。 您通常会从公司的 UI 设计团队获得这些信息。
- 构建导航,编写惰性加载的路由,并使用简单的欢迎消息创建页面。 立即创建这些元素可以为您提供应用的总体框架。
所以这就是我在任何网页或手机开发的早期阶段所使用的通常模式。
现在让我们总结一下你在这一章所学到的东西。
总结
伟大的工作! 我们在这一章做了很多事情。 您已经了解了 UI 组件库可以通过不从头构建组件来帮助我们更快地开发应用。 您已经了解了 Vuetify、BootstrapVue、Buefy、PrimeVue 和 Quasar 是一些具有出色的视觉设计并为用户体验优化的 UI 组件库。
您还学习了如何在 Vue.js 中通过在路径中添加组件来创建页面,以及如何使用router-link组件从应用的一个点到达另一个点。
最后,您已经学习了如何延迟加载页面来提高 web 应用的加载速度。
在下一章中,我们将把 Vue.js 应用与 ASP 集成。 净的核心。
十三、在 ASP.NET Core 应用中集成 Vue.js
现在,是时候让 Vue.js 应用与 REST API 对话了。 在本章中,你将集成 Vue.js 前端应用和 ASP.NET Core 5 后端应用通过安装一个 NuGet 包和添加一些代码。 您还将在后端添加一个 CORS 策略,以允许来自其他域的 web 应用发送请求到您的后端。
我们将涵盖以下议题:
- ASP.NET Core Web API 和 Vue.js 应用一起作为一个单元
- 跨源资源共享或CORS
- 在 ASP 中启用 CORS 策略.NET Core
技术要求
以下是你完成本章所需要的东西:
- vs2019 IDE:https://visualstudio.microsoft.com/vs/
- :https://visualstudio.microsoft.com/vs/mac/
- Rider IDE:https://www.jetbrains.com/rider/
- Visual Studio Code:https://code.visualstudio.com/
- 邮差:https://www.postman.com/
完成的代码回购可以在这里找到:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter13/。
放 ASP.NET Core Web API 和 Vue.js 应用一起作为一个单元
我们在本节将要做的集成并不需要 Vue.js 向 ASP 发送请求.NET Core,但将给我们一些优势。 我们的目标是在一个应用项目中托管后端和前端项目。 作为回报,应用可以作为单个单元发布或构建。
有官方的 web 应用项目模板。 你可以在。net 5 中称它们为样板,用 ASP 构建 Angular 应用.NET Core Web API 和一个带有 ASP. NET 的 React 应用.NET CoreWeb API。 你可在以下连结查阅这两个项目模板:
- 使用 Angular 的项目模板和 ASP.NET Core:https://docs.microsoft.com/en-us/aspnet/core/client-side/spa/angular?view=aspnetcore-5.0&tabs=visual-studio
- 使用 React 项目模板.NET Core:https://docs.microsoft.com/en-us/aspnet/core/client-side/spa/react?view=aspnetcore-5.0&tabs=visual-studio
上面的样板是一个方便的 ASP 的起点.NET Core 应用使用 React 或 Angular。 该项目可以从 GitHub 或 Azure DevOps 这样的 Git 存储库中,作为一个单独的应用从本地机器快速发送到 Azure app Service。
不幸的是,没有官方的 Vue.js 与 ASP.NET Core Web API 样板。 然而,我们可以转换我们的 ASP。 通过在Startup.cs中添加一些额外的代码,更新Travel.WebApi.csproj,并安装一个 NuGet 项目,将 NET Core API 和 Vue.js 应用整合到一个项目中。 你兴奋吗? 让我们开始:
-
The first step is to install
VueCliMiddlewarein theTravel.WebApiproject. The NuGet package is only needed in thesrc|presentation|Travel.WebApiproject, so there's no need to install it in other projects.VueCliMiddleware将允许我们在 ASP 上构建 Vue.js SPA。 使用 Quasar CLI 或 Vue CLI 的 NET MVC 核心。 在此链接查看 nuGet 包及其 GitHub 存储库:https://www.nuget.org/packages/VueCliMiddleware和https://github.com/EEParker/aspnetcore-vueclimiddleware。 -
下一步是更新
Travel.WebApi.csproj文件。 我们将在文件中最后一个ItemGroup下面添加一些代码,它是:<ItemGroup> <Folder Include="Extensions\" /> </ItemGroup> -
We are going to add the following code to the
</ItemGroup>closing tag. So make some space, hitting the Enter key several times, and then write the following code:<!-- Below is for Single Page Application. --> <!-- Please wait for compilation to finish before going to https://localhost:5001 --> <PropertyGroup> <!-- Typescript/Javascript Client Configuration --> <SpaRoot>..\vue-app\</SpaRoot> <DefaultItemExcludes>$(DefaultItemExcludes); $(SpaRoot)node_modules\**</DefaultItemExcludes> <!-- Set this to true if you enable server-side prerendering --> <BuildServerSideRenderer>false </BuildServerSideRenderer> <IsPackable>false</IsPackable> </PropertyGroup>新添加的代码告诉应用 SPA 的目录在
vue-app中。还添加以下代码:
<ItemGroup> <!-- Don't publish the SPA source files, but do show them in the project files list --> <Content Remove="$(SpaRoot)**" /> <None Remove="$(SpaRoot)**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> </ItemGroup> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists( '$(SpaRoot)node_modules') "> <!-- Ensure Node.js is installed --> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(SpaRoot)" Command= "npm install" /> </Target>上述代码将
Spa Root文件夹(即vue-app)添加到项目中。 -
Then add the final block of code we need in
Travel.WebApi.csproj, like so:<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> <Exec WorkingDirectory="$(SpaRoot)" Command= "npm install" /> <Exec WorkingDirectory="$(SpaRoot)" Command= "npm run build" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" /> <DistFiles Include="$(SpaRoot) node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" /> <ResolvedFileToPublish Include="@(DistFiles ->'%(FullPath)')" Exclude=" @(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity) </RelativePath> <CopyToPublishDirectory>PreserveNewest </CopyToPublishDirectory> <ExcludeFromSingleFile>true </ExcludeFromSingleFile> </ResolvedFileToPublish> </ItemGroup> </Target>添加的代码是为了在 vae .js
root目录中运行npm run build,其中包括在发布输出中新建的文件。这就是所有的
csproj文件。 现在让我们进行下一步。 -
Now go to
Startup.csand update theConfigureServicesmethod. Add the following code at the end of the services:services.AddSpaStaticFiles(configuration => { configuration.RootPath = "../vue-app/dist"; });上述代码应在
services.AddTransient IConfigureOptions以下。 代码在应用中添加 SPA 静态文件服务。 -
Next, we update the middleware block, which is the
Configuremethod. Add the following code under theif (env.IsDeveloper())block:app.UseStaticFiles(); if (!env.IsDevelopment()) { app.UseSpaStaticFiles(); }我们正在中间件管道中添加
UseSpaStaticFiles。 -
Now let's add
UseSpa, another middleware, like so:app.UseSpa(spa => { spa.Options.SourcePath = "../vue-app"; if (env.IsDevelopment()) { spa.UseVueCli(npmScript: "serve"); } });该代码将源代码路径添加到
UseSpa,并在生产环境中运行Vue CLI serve。如果你的 ide 不能帮助你,你可能需要手动导入
VueCliMiddleware和SpaServices:using Microsoft.AspNetCore.SpaServices; using VueCliMiddleware;前面的代码是创建 Vue CLI 代理映射所需的名称空间。
Travel.WebApi、vue-app在presentation目录下,如下截图所示:![Figure 13.1 – The presentation directory]()
图 13.1 -演示目录
你在图 13.1中看到的两个文件夹也是你在 Visual Studio Code 中看到的:
![Figure 13.2 – VS Code folder arrangement]()
图 13.2 - VS Code 文件夹布局
图 13.2 中的 VS Code 文件夹排列也显示出
Travel.WebApi和vue-app在目录中处于同一级别。 然而,在 ide 中是不同的,如 JetBrains’Rider, Visual Studio for Mac, Visual Studio 2019,如下截图所示:![Figure 13.3 – Vue.js files and folders in the Travel.WebApi directory]()
图 13.3 - Travel 中的 Vue.js 文件和文件夹 WebApi 目录
ide 足够智能,知道 SPA 文件夹和文件,并将它们暴露在解决方案资源管理器的界面中。
-
Now let's run the application by hitting your IDE debug button or running the
dotnet runcommand inside theTravel.WebApiproject. Then go tohttps://localhost:5001to see the Vue.js app as shown in the following screenshot:![Figure 13.4 – Vue.js running on port 5001]()
图 13.4 - Vue.js 运行在端口 5001 上
接受来自浏览器的任何弹出窗口,该窗口告诉您本地主机的 SSL 证书。
localhost:5001代理 Vue.js CLI 的8080端口。 开始编辑你的代码库中的欢迎旅行消息,你会发现热模块替换仍然很快。 您可以在保存任何更改后立即看到更改。 -
现在看看 Swagger 的 UI,看看它也在同一个端口上运行:

图 13.5 - 5001 端口上的 Swagger UI
图 13.5 显示 Swagger UI 仍然在端口5001上工作。
现在我们可以将应用作为单个单元发送。 如果您有另一个位于另一个域的 SPA,并且希望向您的 ASP 发送请求,该怎么办? NET Core?
现在还不可能,这就是我们将在本章下一节讨论的问题。
介绍跨源资源共享或 CORS
在我们讨论 CORS 策略和跨源资源共享之前,尝试向WeatherForecast端点的版本 2 发送一个 POST 请求。 看看你是否仍然可以使用 Postman 检索WeatherForecast控制器的一些 JSON 响应,如下截图所示:

图 13.6 -使用邮差向天气预报发送 POST 请求
图 13.6表示端点仍然正常工作,但在另一个 SPA 中不起作用。
我创建了一个运行在端口3000上的 React 应用,看看它是否可以从WeatherForecast控制器获取 JSON 对象。 请求中不需要认证,但是 React 应用在控制台中记录错误; 见图 13.7

图 13.7 - CORS 策略阻止
图 13.7 中的错误表示从localhost:3000访问端点XMLHttpRequest已被 CORS 策略阻塞。 No Access-Control-Allow-Origin头出现在请求的资源上。 如果你想亲自看看,你可以运行从 GitHub repo 的第 13 章中获得的 React 应用。 通过运行npm install和npm run start来使用react-app文件夹,这将在localhost 3000上打开浏览器。
这里发生了什么? 我们如何解决这个问题?
CORS代表跨源资源共享。 这是一个很好的安全概念。 如果客户端应用和 api 都来自同一个服务器,就像传统 web 一样,发送 HTTP 请求就会成功。 服务器处理请求是因为请求试图访问同一服务器上的资源。
然而,有时甚至局部宿主也被认为是不同的起源。 如果源端不相同,以3000端口为例,React 从3000端口向服务器发送请求以获取资源,但浏览器会拒绝 React 的请求。
因为我们正在构建 RESTful api,所以我们希望允许这种访问,因为 Web api 需要被不同的浏览器应用使用。 我明确地说浏览器应用是因为 CORS 限制不应用于移动应用,所以移动应用不需要发送飞行前请求。 预飞行请求是浏览器发送给资源服务器的一个快速、小的请求。 飞行前请求检查 CORS 协议是否被理解,服务器是否有处理方法和头的策略。
现在我们知道了 CORS 是什么,让我们在下一节中更新后端以启用 CORS 策略配置。
在 ASP 中启用 CORS 策略.NET Core
本节的目标是使用带有 CORS 策略的接口,并配置以允许任何 spa 向我们的 ASP 发送请求.NET Core Web api。
因此,让我们进入Travel.WebApi项目的Startup.cs文件,通过在AddSpaStaticFiles方法下添加以下代码来更新ConfigureServices方法:
services.AddCors();
services.AddCors方法将是我们在ConfigureServices内部调用的最后一个方法。 此服务是用于设置 CORS 服务的扩展方法。
这里还没有结束; 我们仍然需要更新我们的中间件。 因此,转到Configure方法,并在app.UseSpaStaticFiles下添加以下代码:
app.UseCors(b =>
{
b.AllowAnyOrigin();
b.AllowAnyHeader();
b.AllowAnyMethod();
});
UseCors方法将 CORS 中间件添加到 web 应用的管道中。 从现在开始,这个中间件将允许在我们的应用中跨域请求。
现在让我们尝试一下,看看我们是否可以直接从不同的端口发送请求。
您可以通过在其根文件夹中运行npm run start来运行 React 应用。 你应该会看到如下截图所示的结果:

图 13.8 -从 3000 端口收到一个 200 OK 响应
图 13.8 显示了我们的 CORS 策略配置正在工作,并且它没有阻塞来自localhost:3000的 React 请求。
重要的
您可以在https://auth0.com/docs/applications/set-up-cors了解更多关于 CORS 政策的信息。
让我们来总结一下所学到的内容。
总结
你已经看完了这一章; 你已经学习了如何在 ASP 中包含 Vue.js 应用.NET Core Web API 项目来运行并将它们作为一个整体发布。 您也知道跨源资源共享或CORS的工作原理。 简而言之,它是浏览器的一个安全特性,可以阻止来自不同域或来源的请求。 您已经学习了如何在 ASP 中启用和配置 CORS 策略.NET Core 允许来自其他域或源的传入请求。
在下一章中,我们将开始向 ASP 发送 GET 请求.NET Core Web API,并使用 Vuex 来管理我们的 Vue.js 应用的状态。这将是一个重要的章节,所以先休息一下,40 到 60 分钟后再回来。
十四、使用 Vuex 和发送 GET HTTP 请求来简化状态管理
本章是关于发送 HTTP 请求和解决大型 web 应用中最常见的问题——同步一个组件与另一个组件的状态的问题。 在大型和复杂的应用中,您需要一个工具来集中应用的状态,并使数据流透明和可预测。
在本章中,我们将涵盖以下主题:
- 理解复杂的状态管理
- 发送 HTTP 请求
- 使用 Vuex 设置状态管理
技术要求
你需要 Visual Studio Code 来完成本章。
本章已完成的知识库可以在https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter14找到。
了解复杂状态管理
在开发大型复杂的 Angular、React 或 Vue web 应用时,你会遇到状态管理。 那么状态管理是什么意思呢?
应用状态管理是当你的应用从一个或几个视图开始成长。 您可能会开始遇到这样的问题:您希望在不同的嵌套组件之间共享应用的一些状态。 举个例子,你必须创建一个机制,让两个深度组件始终同步。 见图 14.1图:

图 14.1 -共享组件的本地状态
图 14.1显示组件 Y与组件 X状态相同。 有很多方法可以做到。 一种是绕过事件,从顶级组件传入属性。 尽管您可以传递道具和事件,但在多个嵌套组件中传递事件和道具会使应用难以维护,也会使代码难以推理。
了解全局状态
那么,对于由于多个嵌套组件之间的状态共享而导致的不可维护的代码,该如何解决呢? 应用范围状态或全局状态,也称为,即存储。 全局状态解决方案的思想是,每个视图和每个组件都可以只是一个中心状态的反映。 见图 14.2:

图 14.2 -无响应的全局状态
图 14.2 显示了一个活跃的全局状态,它可以被任何组件访问。 拥有一家商店是件好事。 我们只有一个真理的来源,而一切都只是它的反映。 全局状态或存储的活动性是确保用户在应用中看到的所有内容始终保持同步的好方法。
那么如何在 Vue.js 中建立一个商店呢? 我们稍后再做,但让我们在下一节中做一些不带存储的简单数据获取。 简单的数据获取将证明我们可以向我们的 ASP 发送一个 HTTP GET 请求.NET Core Web API。
在 Vue.js 中发送 HTTP 请求
如果您正在开发现代 web 应用,那么将 HTTP 请求发送到 RESTFul 服务是很简单的。 HTTP 客户端库有api-sauce、super-agent和axios。 你知道 JavaScript 本身有一个用于发送 HTTP 请求的本地 API 吗? 是的,这是 Fetch API。
Fetch API 可以在 JavaScript 和 TypeScript 中使用。 然而,它只在现代浏览器中工作,当您在旧浏览器中加载应用时将不起作用。 不仅如此,在我看来,Fetch API 在使用它的 API 发送请求时也可能过于冗长。 我更喜欢一个带有抽象的 HTTP 客户端库,而不是编写额外的代码,比如res.json()、headers或property方法。 在这种情况下,我们将使用 Axios 作为 HTTP 客户端库。
Axios 是一个基于承诺的 HTTP 客户端库,用于浏览器和服务器端 Node.js 应用。它使用简单,也支持旧浏览器。 我们将把 Axios 和 Day.js 一起安装,这是一个用于操作日期和时间的库。
因此,转到您的vue-app根目录并运行以下npm命令:
npm i axios dayjs
前面的npm命令将安装axios和dayjs。
接下来,我们在src目录中创建一个名为api的文件夹。
在api文件夹中创建一个 JavaScript 文件api-v1-config.js,并编写以下代码:
import axios from "axios";
const debug = process.env.NODE_ENV !== "production";
const baseURL = debug
? "https://localhost:5001/api/v1.0/"
: "https://traveltour.io/api/v1.0/";
let api = axios.create({ baseURL });
export default api;
前面的代码是针对 ASP 的 API 版本之一的 Axios 设置.NET Core Web API。 我们正在创建一个 Axios 实例,并将一个基 URL 传递给它使用。
让我们在api文件夹中创建另一个 JavaScript 文件api-v2-config.js,并编写如下代码:
import axios from "axios";
const debug = process.env.NODE_ENV !== "production";
const baseURL = debug
? "https://localhost:5001/api/v2.0/"
: "https://traveltour.io/api/v2.0/";
let api = axios.create({ baseURL });
export default api;
前面的代码是针对我们的 ASP 的 API 版本的一个 Axios 配置.NET Core Web API。 你会注意到这里唯一不同的是版本。
在api文件夹中创建另一个 JavaScript 文件weather-forecast-services.js,并编写以下代码:
import api from "@/api/api-v2-config";
export async function getWeatherForecastV2Axios(city) {
return await api.post(`WeatherForecast/?city=${city}`);
}
前面的代码是用于向WeatherForecast v2控制器发送 POST 请求的服务。 我们将 Axios 配置的默认导出命名为api,然后使用它调用WeatherForecast端点的POST方法。 我喜欢在函数服务中添加axios后缀,以便在阅读 IDE 或代码编辑器的智能感知时帮助我识别要导入的正确文件。
现在让我们更新views|AdminDashboard文件夹的WeatherForecast.vue组件。 用以下代码更新文件夹的内容:
<script>
import { getWeatherForecastV2Axios } from "@/api/weather-
forecast-services";
export default {
name: "WeatherForecast",
async mounted() {
await getWeatherForecastV2Axios("Oslo");
},
};
</script>
在前面的代码中,我们导入了getWeatherForecastV2Axios服务,并在mounted()生命周期钩子中使用它,以便在 DOM 呈现之前提前触发它。
通过在Travel.WebApi项目中运行以下命令来运行后端应用和前端应用:
dotnet run
运行前面的dotnet命令后等待几秒钟,然后进入浏览器,输入https://localhost:5001,检查 Chrome 的 DevTool,如下截图所示:

图 14.3 -向天气预报 API 发送 POST 请求后 Chrome DevTool 的状态
图14.3POST 请求返回一个【T6 状态】200 OK 代码,这意味着我们得到我们所要求的,那就是下面的 JSON 结果:****

图 14.4 -向天气预报 API 发送 POST 请求后的 JSON 响应
WeatherForecast控制器的 JSON 响应如图图 14.4所示。 响应表示我们可以发送请求并从后端获得响应。
现在我们知道浏览器中有数据可以用于用户界面,让我们为数据构建一个 UI。
让我们再次更新WeatherForecast.vue。
让我们从我们安装的dayjs库中导入relativeTime和dayjs:
import relativeTime from "dayjs/plugin/relativeTime";
import dayjs from "dayjs";
relativeTime将日期格式化为相对时间字符串,如an hour ago或in 2 days。
现在让我们更新挂载的生命周期钩子:
async mounted() {
this.loading = true;
dayjs.extend(relativeTime);
await this.fetchWeatherForecast(this.selectedCity);
this.loading = false;
},
我们使用库dayjs来操作日期。 您还会注意到,我们正在使用本地状态loading,将其设置为 true,然后在获取数据后将其设置为 false。 我们将使用本地状态loading在应用的屏幕上显示一个旋转器组件。
现在让我们定义我们的局部状态:
data() {
return {
weatherForecast: [],
cities: [],
selectedCity: "Oslo",
loading: false,
};
},
我们在本地状态中有一个weatherForecast数组、一个cities数组、一个selectedCity字符串和一个loading布尔值。 它们都是用默认值初始化的。
让我们在methods对象中添加一个异步方法,如下所示:
methods: {
async fetchWeatherForecast(city = "Oslo") {
this.loading = true;
try {
const { data } = await getWeatherForecastV2Axios(
city);
console.log(data);
this.weatherForecast = data?.map((w) => {
const formattedData = { ...w };
let date = w.date;
formattedData.date = dayjs(date).fromNow();
return formattedData;
});
} catch (e) {
alert("Something happened. Please try again.");
} finally {
this.loading = false;
}
},
},
我们在这个函数中将旋转器loading设置为true,使用getWeatherForecastV2Axios服务发送请求。 在获得数据之后,我们将使用 JavaScript 数组实用程序map,它就像一个循环,从 JSON 数组响应格式化每个对象的日期。
让我们还创建另一个方法来返回颜色编码的温度。 在 GitHub repo 中获得完整的函数,因为它太大了,无法在这里写入:
getColor(summary) {
switch (summary) {
case "Freezing":
return "indigo";
// get the rest from the github
default:
return "grey";
}
},
该方法是本质上只是一个助手,根据温度返回颜色。
现在来看一下template语法。 这里是:
<template>
<v-container>
<div class="text-h4 mb-10">
Two-week weather forecast of different cities
</div>
<div class="v-picker--full-width d-flex justify-center"
v-if="loading">
<v-progress-circular
:size="70"
:width="7"
color="purple"
indeterminate
></v-progress-circular>
</div>
<!-- Insert <v-simple-table> here -->
</v-container>
</template>
前面的代码是应用的title、container和WeatherForecast页面的微调器 UI。
现在将v-simple-table组件包含在WeatherForecastUI 的容器中。 将组件插入到你会找到<!-- Insert <v-simple-table> here -->注释的地方:
<v-simple-table>
<template v-slot:default>
<thead>
<tr>
<th class="text-left">Dates</th>
<th class="text-left">City</th>
<th class="text-left">℃</th>
<th class="text-left">℉</th>
<th class="text-left">Summary</th>
</tr>
</thead>
<tbody>
<tr v-for="item in weatherForecast"
:key="item.date">
<td>{{ item.date }}</td>
<td>{{ item.city }}</td>
<td>{{ item.temperatureC }}</td>
<td>{{ item.temperatureF }}</td>
<td>
<v-chip :color="getColor(item.summary)" dark>{{
item.summary
}}</v-chip>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
其中的v-simple-table项为WeatherForecast表,其中显示了奥斯陆某一特定日期的温度:

图 14.5 -两周天气预报
以上预报为Oslo的两周预报。 在那里你可以看到日期、城市、摄氏度和华氏温度,以及天气的可视化总结。
在下一节中,我们将添加一个下拉选择器,我们可以在其中选择一个城市,我们可以选择的城市将来自我们的 ASP.NET Core Web API。
现在我们有了一个使用简单 HTTP 请求而不使用存储的WeatherForecast特性。 我们将在下一节中构建的特性将使用一个 Vuex 状态管理库。 我们将在另一个组件重用的功能,需要一个存储在下一章,第 15 章,发帖子,删除,把 HTTP 请求在 Vue.js Vuex【显示】。
使用 Vuex 设置状态管理
Vuex是 Vue.js 的官方状态管理库,广泛用于管理复杂组件。 Vuex 有一个被动的全球商店,设置起来相当容易。 在编写代码时,我将解释 Vuex 实现的各个部分。
但是在我们开始构建我们的存储之前,让我们先从 API 控制器中删除授权,这样我们在向api/v1.o/端点发送请求时就不需要认证令牌了。
为此,转到Travel.WebApi项目的Travel.WebApi.Controllers.v1名称空间中的ApiController.cs文件,并注释Authorize属性,如下所示:
// [Authorize]
在注释了Authorize属性之后,我们现在可以暂时使用api/v1.0/。
让我们从设置 Vuex 的更新部分开始。
第一步-写一个商店
在store文件夹中创建一个名为tour的文件夹。 它将像这样:src|store|tour。
第二步-编写模块
在tour文件夹中创建一个名为services.js的 JavaScript 文件,并编写如下代码:
import api from "@/api/api-v1-config";
export async function getTourListsAxios() {
return await api.get("TourLists");
}
我们正在创建一个向版本 1TourLists控制器或端点发送请求的服务。
第 3 步-如果我们使用 TypeScript,编写一个模块
创建一个名为types.js的 JavaScript 文件,仍然在tour文件夹中,然后编写以下代码:
export const LOADING_TOUR = "LOADING_TOUR";
export const GET_TOUR_LISTS = "GET_TOUR_LISTS";
前面的代码不是编写 Vuex 时的必要条件,而是在实现状态管理时成为设计的一部分。 前面的类型是,称为动作类型,我们访问这些类型是为了让存储知道它在接收到动作后应该在状态中采取什么样的动作。 蛇形的类型只是字符串,但是它们可以在写入操作时防止拼写错误,稍后您将看到。
第 4 步-编写 API 服务
在tour文件夹中创建一个名为actions.js的 JavaScript 文件,然后编写如下代码:
import * as types from "./types";
import { getTourListsAxios } from "@/store/tour/services";
// asynchronous action using Axios
export async function getTourListsAction({ commit }) {
commit(types.LOADING_TOUR, true);
try {
const { data } = await getTourListsAxios();
commit(types.GET_TOUR_LISTS, data.lists);
} catch (e) {
alert(e);
console.log(e);
}
commit(types.LOADING_TOUR, false);
}
该操作包含如何更新存储的全局状态的说明。 Action 是在其他 JavaScript 框架的状态管理库中也可以找到的术语。 因此,记住这一点很有价值。
此外,如果需要,操作可以包含异步操作。 我们定义了一个带有解构的commit参数的action函数来进行突变。
要使用提交,只需将操作类型作为第一个参数传递。 如果在特定的提交中需要一个有效载荷,我们可以使用第二个可选参数来传递有效载荷。 一个例子就是commit(types.LOADING_TOUR, true);。 在前面的句子中,这行代码是一个关于用布尔参数作为动作的payload加载tour的commit语句。
我们并不局限于在一个action函数中编写一个提交。 只要需要,我们可以编写一个或多个提交。
我们在actions.js的getTourListAction中所做的是启用loading tour。 我们的目标是在使用getTourListsAxios服务获取数据时创建一个微调器。 然后,我们将使用commit(types.GET_TOUR_LISTS, data.lists)中的响应将数据保持在全局状态。
然后通过false到loading tour停止纺纱器的旋转。
在后面的章节中,当我们写更多的动作时,你会看到更多。
第五步-写一个动作类型
在tour文件夹中创建一个名为state.js的 JavaScript 文件,然后编写如下代码:
const state = {
lists: [],
loading: false,
};
export default state;
state对象将成为应用的全局状态的一部分,也就是商店。 存储是一个具有默认值或初始值的属性的大对象。 您必须记住初始值或初始状态,因为它们也适用于不同 JavaScript 框架中的任何其他状态管理库。
第六步-写一个动作
在tour文件夹中创建另一个名为mutations.js的 JavaScript 文件,然后编写如下代码:
import * as types from "./types";
const mutations = {
[types.GET_TOUR_LISTS](state, lists) {
state.lists = lists;
},
[types.LOADING_TOUR](state, value) {
state.loading = value;
},
};
export default mutations;
突变是函数,根据被分派的动作的类型获得触发器。 它们执行实际的状态修改。 正如您在前面的代码中看到的,types.GET_TOUR_LISTS更改了state.lists的值。 参数state是第一个参数,它是全局状态的一部分。 它会自动被维克斯通过。
第二个参数是来自我们前面定义的操作的可选负载。
另外,突变的工作是直接更新全局状态的一部分。 这取决于是否要删除或更新现有状态。
步骤 7 -写一个状态
在tour文件夹中创建一个名为getters.js的 JavaScript 文件,然后编写如下代码:
const getters = {
lists: (state) => state.lists,
loading: (state) => state.loading,
};
export default getters;
getter是用于存储的计算属性或派生值。 getters的结果或输出基于其依赖项进行缓存,当其任何依赖项发生更改时将重新运行或重新计算。 我们将在组件中包含getters,以便将全局状态连接到 UI。
步骤 8 -写一个突变
在tour文件夹中创建一个名为index.js的 JavaScript 文件。 这个文件将在我们的tour模块的索引文件中。 文件创建后,我们编写以下代码:
import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import * as actions from "./actions";
export default {
namespaced: true,
getters,
mutations,
actions,
state,
};
我们导入tour模块或名称空间的index文件的state文件、getters文件、mutations文件和actions文件。 在导入必要的文件后,我们将导出它们和,其中包括一个namespaced属性设置为true。
模块或名称空间是存储中的分区。 每个模块或名称空间都有其actions、state、mutations、getters,甚至还有嵌套模块。
第 9 步-写一个 getter
用下面的代码更新存储的index.js文件。 代码是我们将要添加tour模块的地方:
import Vue from "vue";
import Vuex from "vuex";
import createLogger from "vuex/dist/logger";
import tourModule from "./tour";
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== "production";
const plugins = debug ? [createLogger({})] : [];
export default new Vuex.Store({
modules: {
tourModule,
},
plugins,
});
我们导入一个记录器,前面文件中的 tour 模块,然后删除以下属性:state、mutations和actions。 然后我们使用tourModule作为modules对象的属性。
modules对象是 Vue.js 应用中所有模块的位置。 这意味着当我们在 Vue.js 应用中添加新模块或名称空间时,我们将更新modules对象。
步骤 10 -通过插入模块更新存储
现在让我们更新WeatherForecast.vue页面。 同样,让我们从vuex中导入mapActions和mapGetters:
import { mapActions, mapGetters } from "vuex";
mapGetters是一个助手,它简单地将本地计算属性映射到存储getters。 类似地,mapActions是一个助手,它将存储分派映射到组件方法。 我们将在methods块和computed块中使用这些映射器。
插入mapActions到methods块的第一行中。 使用spread操作符,以mapActions为前缀的三个圆点如下:
methods: {
...mapActions("tourModule", ["getTourListsAction"]),
…
},
mapActions将自动创建的local方法this.getTourListsAction映射到this.$store.dispatch("getTourListsAction")。 我们现在可以使用this.getTourListsAction来触发突变的动作。
在使用getTourListsAction之前,我们还希望访问存储的lists状态。 要做到这一点,创建computed块,并使用扩展操作符插入mapGetters,如下所示:
computed: {
...mapGetters("tourModule", {
lists: "lists",
}),
},
mapGetters将自动创建的本地状态this.lists映射到this.$store.getters.lists。
第 11 步-使用 mapgetter 和 mapActions 更新组件
让我们用下面的代码来更新方法。 我们将添加两行新代码:
async mounted() {
…,
await this.getTourListsAction();
this.cities = this.lists.map((pl) => pl.city);
},
我们触发getTourListsAction,然后从列表中提取城市,并将它们存储在城市的本地状态中。
现在,对于WeatherForecast屏幕的下拉框或选择UI,在template语法区域的v-simple-table组件上添加v-select组件:
<v-select
@change="fetchWeatherForecast"
v-model="selectedCity"
:items="cities"
label="City"
persistent-hint
return-object
single-line
clearable
></v-select>
前面的组件将为WeatherForecast屏幕提供功能,我们可以在其中选择查看哪个城市的 14 天天气预报。
在运行 ASP.NET Core 和 Vue.js 应用,我想让你使用的 SQLite 数据库的Travel.WebApi项目。 我已经更新了其中的数据,以帮助我们了解我们在前端的设计方面正在做什么。 您将看到 JSON 相当于 SQLite 数据在 GitHub 回购https://github.com/PacktPublishing/ASP.NET-Core-5-and-Vue.js-3/tree/master/Chapter-14,让你知道日期目前在数据库中,它的形状。
删除项目中现有的TravelTourDatabase.sqlite3数据库文件,并将其替换为 GitHub 存储库中的文件。
通过在Travel.WebApi项目中运行以下命令重新运行应用:
dotnet run
等待几秒钟,然后检查浏览器。 你应该看到下拉菜单选择器如下图所示:

图 14.6 - Vuetify 选择组件
图 14.6 显示了选择器,默认的Oslo作为所选城市。 尝试点击它来查看其他的可用城市。
接下来,检查浏览器的 DevTools 上的控制台日志。 您还应该看到我们在商店设置中包含的 Vuex 记录器插件。 它看起来类似于图 14.7 所示:

Figure 14.7 – Vuex logger
前面截图中的 Vuex 记录器显示了 Vue.js 应用的 Vuex 状态管理中发生的事情的日志。 如果在使用 Vuex 商店的部件时应用中出现意外行为,我们可以使用它来调试应用。
在其他框架中使用 Vuex 和其他状态管理库需要大量代码来设置它们。 但是一旦你完成了所有移动部分的设置和你的商店的配置,添加新的动作和使用它们就变得很容易了,因为你只需要更新现有的代码。 您只需要编写一次设置,其余的都是无缝的。
尽管完成设置需要时间,但是不必在嵌套组件内外传递道具和事件的好处非常好,这将使您在开发嵌套组件之间的复杂状态同步时更加轻松。
我建议记住前面的主题流,无论何时在项目中实现 Vuex,都要有一个清单,说明要做什么。 现在让我们回顾一下你在本章所学的内容。
总结
由于状态管理的概念,本章可能是迄今为止最具挑战性的章节之一。 然而,学习如何进行状态管理是非常宝贵的。 您已经学习了如何使用 Axios 发送 HTTP 请求。 您已经在大型应用中发现了状态管理的思想。
您还学习了如何使用 Vuex 和 Vuex 的部分,例如存储、模块、操作、突变和 getter。
在下一章中,我们将构建在 Vue.js 中使用 Vuex 发送POST、DELETE、和PUT HTTP请求的功能。
十五、在 Vue.js 中发送 POST、DELETE 和 PUT HTTP 请求
在上一章中,我们讨论了状态管理,并在我们的 Vue.js 应用中设置了 Vuex。 然而,在我们的应用中使用 Vuex 的 CRUD 操作还没有完成。 在本章中,我们将编写 HTTP 请求和 Vuex 参与的 CRUD 操作的其余部分。
至此,我们将在本章中涵盖以下主题:
- 使用 Axios 和 Vuex 删除旅游列表
- 使用 Axios 和 Vuex 添加旅游列表
- 在 Vuex 中使用非异步操作
- 使用 Axios 和 Vuex 移除旅行团
- 使用 Axios 和 Vuex 添加旅行团
- 使用 Axios 和 Vuex 更新旅游套餐
技术要求
你需要 Visual Studio Code 来完成本章。
本章已完成的知识库可以在https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter15/找到。
使用 Axios 和 Vuex 移除旅游列表
如果你仍然渴望用 Axios 和 Vuex 写一些东西,这个非常适合你。 我们将发送一个请求到我们的 ASP.NET Core Web API 检索TourList集合,使用 Vuetify 组件在 UI 上呈现集合,然后能够删除任何TourList对象。 但在我们开始之前,让我们更新一下 ASP.NET Core Web API 项目。
用以下代码更新namespace Travel.Application.Dtos.Tour中的TourPackageDto.cs文件:
public float Price { get; set; }
public string MapLocation { get; set; }
我们正在添加Price和MapLocation属性。
接下来,我们更新后端服务。 转到有namespace of Travel.WebApi.Controllers.v1的TourPackagesController.cs文件,并使用以下代码添加一个新的异步操作方法:
[HttpPut("{id}")]
public async Task<ActionResult> Update(int id, UpdateTourPackageCommand command)
{
if (id != command.Id)
return BadRequest();
await Mediator.Send(command);
return NoContent();
}
前面的 c#代码是一个额外的控制器,用于 Web API 在应用中编辑现有的旅游包。
现在我们已经更新了我们的 ASP.NET Core Web API 项目,让我们专注于我们的 Vue.js 应用,并完成我们的 Travel Tour Web 应用的功能。
在我看来,创建一个delete功能是仅次于fetch功能的第二容易实现的事情。
因此,让我们更新AdminDashboard目录的DefaultContent.vue文件。 首先是将mapActions导入DefaultContent组件,如下所示:
import { mapActions } from "vuex";
我们将也会在DefaultContent组件中使用mapActions,就像前一章的WeatherForecast组件中使用的一样。
接下来,我们创建methods对象,并像这样使用mapActions:
methods: {
...mapActions("tourModule", ["getTourListsAction"]),
},
前面的代码为我们提供了一个getTourListsAction方法。
现在创建mounted方法,并在其中调用getTourListsAction:
mounted() { this.getTourListsAction(); },
前面的代码将在呈现 DOM 时触发getTourListsAction。
接下来,我们编写一个本地化的样式部分,并编写一个默认内容类:
<style scoped>
.default-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
</style>
前面的 CSS 将为我们将在下一段代码中编写的div标记提供样式。
现在让我们更新template语法部分:
<template>
<div>
<div class="text-h2 my-4">Welcome to Admin
Dashboard</div>
<div class="default-content"></div>
</div>
</template>
前面的代码是一个简单的欢迎消息,表示Welcome to Admin Dashboard。 在创建card组件之后,我们将回到这个组件。
转到components文件夹并创建一个Vue.js组件文件TourListsCard.vue。
在新创建的 Vue 组件中添加以下脚本:
<script>
import { mapActions, mapGetters } from "vuex";
export default {
name: "TourListsCard",
computed: {
...mapGetters("tourModule", {
lists: "lists", loading: "loading",
}),
},
};
</script>
我们从 Vuex 引进了mapActions和mapGetters。 然后我们使用mapGetters将全局状态的列表和加载带到局部状态的列表和加载。 请注意,全局状态和加载状态的任何变化也将反映在本地化列表和加载状态中。
让我们将下面的代码添加到新创建的TourListsCardVue 组件:
<template>
<v-skeleton-loader
v-if="loading" width="300" max-width="450"
height="100%" type="card" ></v-skeleton-loader>
<v-card v-else width="300" max-width="450" height="100%">
<v-toolbar color="light-blue" dark>
<v-toolbar-title>Tour Lists</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-list-item-group color="primary">
<v-list-item v-for="tourList in lists"
:key="tourList.id">
<v-list-item-content>
<v-list-item-title v-text="tourList.city"></v-
list-item-title>
<v-list-item-subtitle v-text="tourList.about">
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<div class="mr-2">
{{ tourList.tourPackages &&
tourList.tourPackages.length }}
</div>
<v-icon> mdi-delete-outline </v-icon>
</v-list-item-action>
</v-list-item>
</v-list-item-group>
</v-card>
</template>
前面的语法template呈现一个装入器和带有tourLists包的卡。 我们使用v-for指令从列表中提取对象,然后逐个渲染它们。 每个tourList对象将显示两个属性的两个值——UI 中的city属性的值和about属性的值。
现在让我们回到DefaultContent.vue并更新它。
从components目录导入TourListsCard组件,如下所示:
import TourListsCard from "@/components/TourListsCard";
您可以使用@/轻松导航到src目录下的文件夹。 现在使用下面的导入组件:
components: { TourListsCard, å},
我们已经将TourListsCard组分注册到DefaultContent组分中。
现在,在带有默认内容类的div元素中,添加以下代码:
<div style="margin-right: 4rem; margin-bottom: 4rem">
<TourListsCard />
</div>
我们在前面的代码中呈现了TourListsCard。 重新启动服务器,因为我们已经创建了一个新文件。
在Travel.WebApi项目中,执行如下命令:
dotnet run
前面的命令将运行整个应用,这意味着同时运行后端和前端。
下面的图 15.1显示了一个简单的仪表板,其中带有欢迎消息:

图 15.1 - Admin Dashboard 组件上的欢迎消息
现在我们准备在 Axios 中编写delete功能并将其存储。 您将发现在状态管理中编写额外的操作是多么容易,因为我们已经在上一章中设置了存储。 现在,让我们继续。
接下来,我们使用以下代码更新store/tour/目录的services.js:
export async function deleteTourListAxios(id) {
return await api.delete("TourLists/" + id);
}
前面的代码是一个删除tourList对象的服务:
我们也更新一下store/tour/的types.js:
export const REMOVE_TOUR_LIST = "REMOVE_TOUR_LIST";
我们正在向types.js文件添加一个新的操作类型。 动作类型是用于删除旅行列表,因此在蛇形外壳中命名为REMOVE_TOUR_LIST。
让我们也更新store/tour/目录的actions.js文件:
import { getTourListsAxios, deleteTourListAxios } from "@/store/tour/services";
在添加remove功能之前,我们首先引入deleteTourListAxios服务。
现在,让我们将removeTourListAction函数写成这样:
// asynchronous action using Axios
export async function removeTourListAction({ commit }, payload) {
commit(types.LOADING_TOUR, true);
try {
await deleteTourListAxios(payload);
commit(types.REMOVE_TOUR_LIST, payload);
} catch (e) {
alert(e);
console.log(e);
}
commit(types.LOADING_TOUR, false);
}
前面的函数是removeTourListAction,它接受一个有效负载。 触发removeTourListAction时需要使用的参数技术上只有一个,即有效负载,而有效负载是我们将要删除的tourList的 ID。
现在,您将注意到在removeTourListAction中,它通过提交types.LOADING_TOUR并在使用deleteTourListAxios服务发送删除请求之前传递true来启用加载。 该函数还创建一个提交,删除tourList并在 ASP 中删除它。 净的核心。
removeTourListAction也再次使用false的布尔值提交types.LOADING_TOUR,这将禁用loader组件,或者spinner组件,从旋转。
现在,更新store/tour/的mutations.js:
[types.REMOVE_TOUR_LIST](state, id) {
state.lists = state.lists.filter((tl) => tl.id !== id);
state.packagesOfSelectedCity = [];
},
types.REMOVE_TOUR_LIST操作类型有一个指令,通过只从参数中获取具有id且不等于id的对象来过滤掉所有对象。 然后,操作类型还将packagesOfSelectedCity全局状态设置为空数组。
现在,让我们通过添加一个带有mapActions的methods对象和一个额外的removeTourList函数来更新TourListsCard.vue组件,它采用listId如下所示:
methods: {
...mapActions("tourModule", ["removeTourListAction"]),
removeTourList(listId) {
const confirmed = confirm(
"You sure you want to delete this tour list? This
will also delete the packages permanently"
);
if (!confirmed) return;
this.removeTourListAction(listId);
},
},
您将注意到在删除或删除旅游列表之前有一个确认提示。
现在,让我们像这样更新TourListsCardVue 组件的template语法部分中的v-icon组件:
<v-icon @click="removeTourList(tourList.id)">
mdi-delete-outline
</v-icon>
在前面的代码中,我们将removeTourList函数绑定到@click指令,将提供给我们一个点击事件。
如果 ESLint 错误阻止 Vue.js 应用刷新,重新启动服务器。 现在重新运行 ASP.NET Core Web API 项目,运行dotnet run命令。
该命令将再次启动整个应用。 现在,打开你的浏览器,前往我们的应用的管理仪表板页面:

图 15.2 -带有漫游列表的卡片组件
图 15.2 之前的显示了重新运行应用后的卡片 UI 组件。 您现在可以看到带有delete功能的tourList的集合。 垃圾桶图标上方的数字表示可在旅游列表中使用的软件包。 0表示尚未添加包。
尝试删除Manila并仔细查看 UI。 用户界面应该在删除旅游列表后更新。
我们已经成功地在应用中添加了删除或删除旅行列表的功能。 在下一节中,我们将创建一个表单,在其中我们可以创建一个新的旅游列表。
使用 Axios 和 Vuex 添加旅游列表
在 CRUD 中,Create或Add功能是第三个最容易实现的功能。 获取和删除功能很容易,因为不需要form组件和一些输入字段。 为了创建添加或生成新数据的功能,我们必须构建一个表单。
在为 UI 构建表单之前,我们将首先在 Axios 和 Vuex 中开发创建旅游列表的功能。
首先,让我们通过添加以下代码来更新store/tour中的services.js:
export async function postTourListAxios(tourList) {
return await api.post("TourLists", tourList);
}
前面的代码是一个向后端发送POST请求以创建tourList数据条目的服务。
现在,让我们更新store/tour目录的types.js。
export const ADD_TOUR_LIST = "ADD_TOUR_LIST";
我们在前面的代码中添加了一个新的操作类型。 最新的动作类型是用于添加新的tourList。
接下来我们更新store/tour目录的actions.js:
import { getTourListsAxios, deleteTourListAxios,
postTourListAxios,} from "@/store/tour/services";
在添加新操作之前,让我们先导入postTourListAxios服务。
在导入新创建的服务之后,让我们向actions.js添加一个新操作:
// asynchronous action using Axios
export async function addTourListAction({ commit }, payload) {
commit(types.LOADING_TOUR, true);
try {
const { data } = await postTourListAxios(payload);
payload.id = data; // storing the id from the response
int of ASP.NET Core, which will be used in the UI.
payload.tourPackages = []; // initialize the
tourPackages of the newly created tourList
commit(types.ADD_TOUR_LIST, payload);
} catch (e) {
alert(e);
console.log(e);
}
commit(types.LOADING_TOUR, false);
}
前面的代码是另一个异步操作函数。 这意味着这是一个使用 Axios 发送 HTTP 请求的操作。 该操作以tourList作为负载。 它把它发送给 ASP.NET Core 使用服务。
作为响应,服务从后端返回新创建的旅行列表的id。 然后我们将响应存储在payload.id中,以便在更新 UI 时在突变中使用它。
现在,让我们通过添加以下代码来更新store/tour中的mutations.js:
[types.ADD_TOUR_LIST](state, tourList) {
state.lists.unshift(tourList);
},
前面的突变将新创建的tourList添加到数组的开头。 unshift 帮助我们很容易地看到在card组件中新添加的tourList。
现在我们将创建一个helper函数,并将其放在一个单独的文件夹中。
让我们在src目录中创建一个新文件夹,并将其命名为helpers。 然后,在helpers文件夹中创建一个名为collections.js的 JavaScript 文件。
collection.js文件有一个getCountryList函数,该函数返回一个国家数组,我们将在表单的搜索输入中需要这个数组。 我们将很快创建表单并绑定getCountryList辅助函数。
因此,从 GitHub 库中获取名为getCountryList的帮助函数:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter15。
现在为表格。 在components文件夹中创建一个名为AddTourListForm.vue的 Vue 组件,并添加以下脚本:
<script>
import { getCountryList } from "@/helpers/collections";
import { mapActions } from "vuex";
export default {
name: "AddTourListForm",
data: () => ({
bodyRequest: { city: "", country: "", about: "", },
dialog: false,
countryList: getCountryList(),
}),
methods: {
...mapActions("tourModule", ["addTourListAction"]),
},
};
</script>
我们从 Vuex 导入了getCountryList辅助函数和mapActions辅助函数。 我们声明一个局部状态bodyRequest对象、一个布尔对话框和countryList,后者是getCountryList返回的数组。
现在,下面的代码是针对的template语法的AddTourListView组件:
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-
width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn
style="margin-top: 1rem" rounded color="light-
blue"
dark v-bind="attrs" v-on="on">
<v-icon left>mdi-plus</v-icon>
add new tour list
</v-btn>
</template>
<!-- INSERT <v-card> BELOW -->
</v-dialog>
</v-row>
</template>
v-btn是来自 Vuetify 的一个按钮组件,它将使用v-slot指令激活器触发模板,以便稍后显示对话框窗体。 v-slot指令用于包装另一个组件,使其可重用。
你会注意到有一个注释说insert v-card。 我们将添加以下template语法。 因此,用更新AddTourListForm.vue如下代码:
<v-card>
<form @submit.prevent="
addTourListAction(bodyRequest);
bodyRequest = {};">
<v-card-title>
<span class="headline">Create New Tour
List</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6">
<v-text-field required label="City"
v-model="bodyRequest.city"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-autocomplete required
:items="countryList" label="Country"
v-model="bodyRequest.country"
></v-autocomplete>
</v-col>
<!-- Removed for brevity -->
<!-- Please see the full code in GitHub -->
<v-btn color="blue darken-1" text
@click="dialog = false" type="submit" >
Save
</v-btn>
</v-card-actions>
</form>
</v-card>
前面的代码是让我们创建新的tourList的表单。 表单有一个用于city的文本字段,一个用于country的带有自动补全的字段,以及一个用于about的文本区域。 我们可能会注意到v-btn组件具有submit类型。 类型触发运行addTourListAction的窗体的@submit。
现在,让我们通过更新form组件来将其包含在DefaultContent.vue文件中:
import AddTourListForm from "@/components/AddTourListForm";
我们从组件中引入了AddTourListForm组件。
接下来,我们将AddTourListForm注册到组件对象中,如下所示:
components: { TourListsCard, AddTourListForm, },
现在可以在Default组件的模板语法区域中使用AddTourListForm了。
在TourListsCard组件下面添加AddTourListForm组件:
<TourListsCard />
<AddTourListForm />
重新启动服务器,因为我们已经创建了一个文件。 运行dotnet run命令运行整个应用。
打开浏览器,进入应用的管理面板,在那里你会看到以下截图:

图 15.3 -添加新的旅行团列表按钮
图 15.3 前的显示了来自 Vuetify 的ADD NEW TOUR LIST按钮。 点击打开表单:

图 15.4 -添加旅游列表表单
图 15.4显示创建或添加新的旅游列表的表单。 尝试一下,看看创建和发送旅游列表的结果。
在下一节中,我们将为特定的旅游列表获取旅游套餐。
在 Vuex 中使用非异步操作
我们可以在 UI 中看到旅游列表的集合,但不能看到他们的旅游套餐。 本节将使用 Vuex 而不使用 Axios 从旅游列表中提取旅游包,因为我们没有发送 HTTP 请求。 没有 HTTP 请求意味着我们将使用非异步操作。 您将学习如何有效地使用 Vuex 和非异步操作。 让我们开始。
用以下代码更新store/tour文件夹中的types.js文件:
export const GET_PACKAGES_OF_SELECTED_CITY = "GET_PACKAGES_OF_SELECTED_CITY";
前面的代码是我们在types.js中添加的一个新的操作类型。
然后,用以下代码更新store/tour中的actions.js:
// non-asynchronous action
export function getPackagesOfSelectedCityAction({ commit }, payload) {
commit(types.GET_PACKAGES_OF_SELECTED_CITY, payload);
}
您将在前面的操作中注意到没有try和catch块,因为我们没有在这里使用 Axios。
接下来,我们更新store/tour文件夹的state.js:
packagesOfSelectedCity: [],
最后,对于来自特定城市的旅游套餐,我们有了一个州。
现在,更新store/tour文件夹的mutations.js:
[types.GET_PACKAGES_OF_SELECTED_CITY](state, packages) {
state.packagesOfSelectedCity = packages;
},
我们现在将有效负载作为包使用,然后将它们存储在packagesOfSelectedCity状态中,这是一个数组。
接下来,我们更新store/tour文件夹的getters.js:
packagesOfSelectedCity: (state) => state.packagesOfSelectedCity,
我们稍后会在 UI 中使用前面的 getter 函数。
现在是时候更新TourListsCard组件了。 转到TourListsCard.vue文件,用以下代码更新组件:
...mapActions("tourModule", [
"removeTourListAction",
"getPackagesOfSelectedCityAction",
]),
我们现在展示的是新的行动,即getPackagesOfSelectedAction。
让我们使用下面的代码块向methods添加一个新函数:
addToPackages(packages, listId) {
this.getPackagesOfSelectedCityAction(packages);
this.$emit("handleShowPackages", true, listId);
},
我们使用getPackagesOfSelectedCityAction和发射handleShowPackages作为支撑TourListsCard成分。
接下来,我们更新TourListsCard组件的v-list-item-content组件:
<v-list-item-content
@click="addToPackages(tourList.tourPackages,
tourList.id)">
点击事件将触发我们刚刚添加到methods块中的addToPackages函数。
现在,让我们用以下代码更新DefaultContent.vue组件文件:
methods: {
...mapActions("tourModule", ["getTourListsAction"]),
handleShowPackages(show, listId) {
this.showPackages = show;
this.tourListId = listId;
},
},
我们正在将handleShowPackages方法添加到methods块中。
现在,让我们也添加一些本地状态:
data: () => ({ showPackages: false, tourListId: 0, }),
我们有一个布尔类型showPackages和一个数字tourListId。
现在让我们使用DefaultContent.vue组件的mounted生命周期钩子:
mounted() {
this.getTourListsAction();
this.showPackages = false;
},
当我们进入管理仪表板页面时,调用getTourListsAction并将showPackages设置为false。
然后,更新template语法中的<TourListsCard />:
<TourListsCard @handleShowPackages="handleShowPackages" />
我们正在使用TourListsCard已发出的handleShowPackages道具,并分配DefaultComponent.vue的handleShowPackages函数。
让我们用下面的代码再次更新template语法:
<div style="margin-right: 4rem; margin-bottom: 4rem">
<TourListsCard @handleShowPackages
="handleShowPackages" />
<AddTourListForm />
</div>
<div v-if="showPackages">
<h2>Tour Packages Card Here</h2>
<h3>Add Tour Package Form with tour list Id
Here</h3>
</div>
我们正在将v-if指令添加到前面的模板区域代码中。
现在运行该应用并点击任何城市。
TourPackagesCard组件应该在运行应用并点击任何城市后出现在屏幕右侧。
我们在这里做了一个快速的概念证明,我们可以渲染TourPackagesCard组件,如下面的截图所示:

图 15.5 -显示占位符
图 15.5 显示了在选择任何Tour Lists卡片城市后,占位符消息会弹出。 现在我们可以有条件地显示一个 UI。 让我们为包创建卡片组件。
在components文件夹中创建TourPackagesCard.vue,并添加以下脚本:
<script>
import { mapActions, mapGetters } from "vuex";
export default {
name: "TourPackagesCard",
computed: {
...mapGetters("tourModule", {
packages: "packagesOfSelectedCity",
}),
},
};
</script>
前面的脚本正在导入并使用mapAction和mapGetters。
现在让我们加入TourPackagesCard的template语法部分。 添加以下代码块:
<template>
<v-container>
<v-card width="500" max-width="600">
<v-toolbar color="pink" dark>
<v-toolbar-title>Packages</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-list two-line>
<v-list-item-group active-class="pink--text">
<div v-if="packages && packages.length > 0">
<template v-for="tourPackage in packages">
<v-list-item :key="tourPackage.id">
<!-- <template> for <v-list-item-content>
here -->
</v-list-item>
</template>
<v-divider />
</div>
<div v-else>
<v-list-item>
<v-list-item-content>
<v-list-item-title
v-text="'No package added yet 😢'"
></v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
</v-list-item-group>
</v-list>
</v-card>
</v-container>
</template>
前面的代码是来自 Vuetify 的卡片组件,用于呈现所选城市的每个包。 card组件的外观和感觉将类似于旅游列表,唯一的区别是颜色。
现在让我们添加card组件的列表项内容。 用以下代码替换注释<!-- <template> for <v-list-item-content> here -->:
<template>
<v-list-item-content>
<v-list-item-title v-text="tourPackage.name"></v-list-
item-title>
<v-list-item-subtitle
class="text--primary"
v-text="tourPackage.whatToExpect"
></v-list-item-subtitle>
<div style="margin-top: 0.5rem; display: flex; flex-
direction: row">
<v-list-item-subtitle
class="text--secondary"
v-text="`Duration: ${tourPackage.duration}hrs`"
></v-list-item-subtitle>
<v-list-item-subtitle
class="text--secondary" v-text="
`${tourPackage.instantConfirmation ? 'Instant
Confirmation' : ''}`"
></v-list-item-subtitle>
</div>
</v-list-item-content>
<v-list-item-action>
<v-icon> mdi-delete-outline </v-icon>
</v-list-item-action>
</template>
前面的代码是每个包所在行的 UI。 其中行为旅游套餐的属性值,如name、whatToExpect、duration、instantConfirmation。
现在再次使用以下代码更新DefaultContent.vue组件:
import TourPackagesCard from "@/components/TourPackagesCard";
我们正在导入TourPackagesCard组件。
让我们像这样在components块中注册TourPackagesCard组件:
components: { TourListsCard, AddTourListForm,
TourPackagesCard, },
现在,可以使用TourPackagesCard组件。
用以下代码替换v-if="showPackages"和占位符消息:
<div v-if="showPackages">
<TourPackagesCard />
</div>
现在,在单击城市之后,我们的TourPackagesCard组件应该是可见的。
运行dotnet run命令重新启动整个应用。
然后点击Oslo,会显示TourPackagesCard组件,里面有一些包,如下图所示:

图 15.6 - TourPackagesCard 组件
图 15.6 显示的TourPackagesCard组件在右侧。
现在我们已经完成了应用中唯一的非异步操作,让我们回到在 Vuex 中编写异步操作。
使用 Axios 和 Vuex 移除旅行团
在这个节中,我们将实现在后台中删除旅行包和在 UI 中删除旅行包的功能。 所以让我们开始吧。
更新store/tour文件夹的services.js:
export async function deleteTourPackageAxios(id) {
return await api.delete("TourPackages/" + id);
}
我们正在添加一个新的服务,删除一个旅行团。
接下来,我们更新store/tour文件夹的types.js:
export const REMOVE_TOUR_PACKAGE = "REMOVE_TOUR_PACKAGE";
在前面的代码中,我们添加了一个新的操作类型来删除旅行包。
接下来,我们更新store/tour文件夹的actions.js:
import { getTourListsAxios, deleteTourListAxios,
postTourListAxios, deleteTourPackageAxios,
} from "@/store/tour/services";
我们在之前的代码中导入deleteTourPackageAxios。
让我们在actions.js中添加另一个动作:
export async function removeTourPackageAction({ commit }, payload) {
commit(types.LOADING_TOUR, true);
try {
await deleteTourPackageAxios(payload);
commit(types.REMOVE_TOUR_PACKAGE, payload);
} catch (e) {
alert(e);
console.log(e);
}
commit(types.LOADING_TOUR, false);
}
removeTourPackageAction函数使加载器旋转,使用 Axios 向后端发送一个DELETE请求,提交一个类型来指示突变删除 UI tour 包,然后禁止加载器停止旋转。
然后,更新store/tour文件夹中的mutations.js:
[types.REMOVE_TOUR_PACKAGE](state, id) {
state.packagesOfSelectedCity =
state.packagesOfSelectedCity.filter(
(tp) => tp.id !== id);
},
前面的突变使用 JavaScript 中内置的过滤器删除 UI 旅行包。
现在我们可以转到TourPackagesCard.vue组件,用以下代码更新它:
methods: {
...mapActions("tourModule", ["removeTourPackageAction"]),
removeTourPackage(packageId) {
const confirmed = confirm(
"You sure you want to permanently delete this tour
package?");
if (!confirmed) return;
// might need to wait for 1 min because of the cache
this.removeTourPackageAction(packageId);
},
},
我们添加了一个methods块并使用mapActions映射removeTourPackageAction。 然后,我们将创建另一个名为removeTourPackage的方法,它将触发一个确认对话框和removeTourPackageAction。
最后,更新v-icon组件,给它一个以tourPackage.id作为参数运行removeTourPackage函数的点击事件:
<v-icon @click="removeTourPackage(tourPackage.id)">
mdi-delete-outline
</v-icon>
v-icon组件可以使用了。 我们可以点击删除一个旅行团。 进入 Vue app,点击City。 点击城市后,删除任何旅游套餐。 您应该看到旅行团已从 UI 中删除。
现在我们可以进入下一个部分,它创建了一个可以添加新旅行团的功能。
添加 Axios 和 Vuex 套餐
这个部分将构建一个功能,使用 Axios 和 Vuex 将一个新的旅游套餐添加到现有的旅游列表中。
让我们更新store/tour文件夹的services.js:
export async function postTourPackageAxios(tourPackage) {
return await api.post("TourPackages", tourPackage);
}
在前面的代码中,我们添加了一个在后端创建新的tourPackage的新服务。
接下来,我们更新store/tour文件夹的types.js:
export const ADD_TOUR_PACKAGE = "ADD_TOUR_PACKAGE";
代码更新添加了用于添加旅行包的新操作类型。
现在,使用以下代码更新store/tour中的actions.js:
import { getTourListsAxios, deleteTourListAxios,
postTourListAxios, deleteTourPackageAxios,
postTourPackageAxios,
} from "@/store/tour/services";
我们首先导入服务postTourPackageAxios,正如您在前面的代码中看到的那样。 现在我们可以在中使用导入postTourPackageAxios,代码如下:
export async function addTourPackageAction({ commit }, payload) {
commit(types.LOADING_TOUR, true);
try {
const { data } = await postTourPackageAxios(payload);
payload.id = data; // storing the id from the response
int of ASP.NET Core, which will be used in the UI.
commit(types.ADD_TOUR_PACKAGE, payload);
} catch (e) {
alert(e);
console.log(e);
}
commit(types.LOADING_TOUR, false);
}
addTourPackageAction打开加载程序,向后端发送POST请求,通过调用提交将tourPackage添加到 UI 中,然后关闭加载程序。
接下来,我们更新store/tour文件夹的mutations.js:
[types.ADD_TOUR_PACKAGE](state, tourPackage) {
state.packagesOfSelectedCity.unshift(tourPackage);
},
之前的代码是一个突变,它将新的tourPackage添加到packagesOfSelectedCity数组的第一个索引。
然后,对于表单,我们可以在其中添加新的tourPackage,在组件中创建一个 Vue 组件AddTourPackageForm.vue,并使用以下脚本:
<script>
import { mapActions } from "vuex";
export default {
name: "AddTourPackageForm",
props: {
tourListId: { type: Number, },
},
data: () => ({
id: 0,
bodyRequest: {
listId: 0, name: "", whatToExpect: "",
mapLocation: "https://www.google.com/maps/place/...",
price: 10, duration: 1, instantConfirmation: true,
},
dialog: false,
currencies: ["USD", "NOK"],
currencyValues: [0, 1],
durations: [1, 2, 3, 4, 5, 6, 7, 8],
durationValue: 1,
}),
methods: {
...mapActions("tourModule", ["addTourPackageAction"]),
onSubmit() {
this.bodyRequest.listId = this.tourListId;
this.addTourPackageAction(this.bodyRequest); //
triggers the method of the container holding this
this.bodyRequest = {};
},
},
};
</script>
在前面的代码中,我们有id、bodyRequest、dialog、currencies、currencyValues、durations和durationValue局部状态。 我们在方法中也有mapActions传播,触发addTourPackageAction方法中有onSubmit传播。 最后,形式有一个tourListId道具,是tourList中的id。
为 UI 添加以下template语法:
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-
width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn
style="margin-top: 1rem" rounded color="pink"
dark v-bind="attrs" v-on="on">
<v-icon left> mdi-plus </v-icon>
add new tour package
</v-btn>
</template>
<!-- Insert <v-card> here -->
</v-dialog>
</v-row>
</template>
我们有一个打开对话框或模态形式的组件。 此表格类似于旅游清单中的表格。
现在,用下面的模板语法替换< !-- Insert <v-card> here -->:
<v-card>
<form @submit.prevent="onSubmit">
<v-card-title>
<span class="headline">Create New Tour
Package</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12">
<v-text-field required label="Name"
v-model="bodyRequest.name"
></v-text-field>
</v-col>
<!-- Removed for brevity -->
<!-- Please see the full code in GitHub -->
</v-row>
</v-container>
<small>*indicates required field</small>
</v-card-text>
<!-- Insert <v-card-actions> here -->
</form>
</v-card>
前面的表单允许用户添加一个新的旅游包。 表格中包含旅游套餐:name、whatToExpect、mapLocation、price、duration、instant``confirmation。
让我们也添加一些按钮:
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="dialog =
false"> Close </v-btn>
<v-btn color="blue darken-1" text @click="dialog =
false" type="submit">
Save
</v-btn>
</v-card-actions>
Close v-btn和Save v-btn都具有关闭对话框的功能,但Save v-btn是submit类型,这意味着它触发for的@submit。 然后从methods块中触发onSubmit方法。
接下来我们再次更新DefaultContent.vue组件:
import AddTourPackageForm from "@/components/AddTourPackageForm";
我们正在引入AddTourPackageForm。 然后让我们像这样在components块中注册AddTourPackageForm:
components: { TourListsCard, AddTourListForm,
TourPackagesCard, AddTourPackageForm, },
现在我们可以将插入AddTourPackageForm到template语法部分。
在TourPackagesCard组件下面插入AddTourPackageForm组件。 不要忘记在tourListId道具中通过tourListId:
<div v-if="showPackages">
<TourPackagesCard />
<AddTourPackageForm :tourListId="tourListId" />
</div>
AddTourPackageForm现在在template语法中,可以使用了。
如果 ESLint 错误阻止 Vue.js 应用刷新,重启服务器。
好的,让我们打开浏览器,看到ADD NEW TOUR PACKAGE按钮,如下截图所示:

图 15.7 - ADD NEW TOUR PACKAGE 按钮
现在,点击新增的按钮,查看是否会出现对话框表单,如下截图所示:

图 15.8 -创建新旅行团表单
图 15.8显示为创建一个新的旅游包的表单。 尝试填写表格,点击保存。
现在我们可以进入下一节了。
使用 Axios 和 Vuex 更新旅游套餐
我们在本章的最后一节,这一节是关于使用PUT HTTP请求和 Vuex 更新一个旅行团。 我们将创建另一个窗体来编辑旅行团。 让我们开始。
第一个任务是更新目录中的store/tour``services.js:
export async function putTourPackageAxios(tourList) {
return await api.put(`TourPackages/${tourList.id}`,
tourList);
}
我们正在添加一个新服务,它向后端服务器发送一个PUT请求。
接下来,我们再次更新store/tour文件夹的types.js:
export const UPDATE_TOUR_PACKAGE = "UPDATE_TOUR_PACKAGE";
前面的代码是用于更新旅行团的新操作类型。
接下来,我们还更新了store/tour文件夹中的actions.js:
import { getTourListsAxios, deleteTourListAxios,
postTourListAxios, deleteTourPackageAxios,
postTourPackageAxios, putTourPackageAxios,
} from "@/store/tour/services";
在前面的代码中,我们正在导入putTourPackageAxios服务。
下面的代码是一个新的异步 Vuex 操作。 该操作将是在actions.js文件中的最后一个 Vuex 操作:
export async function updateTourPackageAction({ commit }, payload) {
commit(types.LOADING_TOUR, true);
try {
await putTourPackageAxios(payload);
commit(types.UPDATE_TOUR_PACKAGE, payload);
} catch (e) {
alert(e);
console.log(e);
}
commit(types.LOADING_TOUR, false);
}
新添加的操作称为updateTourPackageAction使装载机,发送一个把请求的有效载荷后台,UPDATE_TOUR_PACKAGE和提交,通过 th e【显示】有效载荷也更新 UI,然后禁用的加载 UI。
然后我们也用以下代码更新store/tour中的mutations.js:
[types.UPDATE_TOUR_PACKAGE](state, payload) {
const packageIndex =
state.packagesOfSelectedCity.findIndex(
(pl) => pl.id === payload.id);
state.packagesOfSelectedCity[packageIndex] = payload;
const listIndex = state.lists.findIndex(
(l) => l.id === state.packagesOfSelectedCity.listId);
state.lists[listIndex] = state.packagesOfSelectedCity;
},
我们正在添加一个新的突变,它可以找到特定旅行团的索引。 找到索引后,我们通过索引定位特定的旅行团,然后用有效载荷(更新后的旅行团)对其进行突变。
我们也通过再次找到selectedCity的索引,然后编辑它来更新tourList。 上述逻辑使所有受影响的对象和数组保持同步。
现在,在components目录中创建一个名为UpdateTourPackageForm.vue的 Vue 组件,并将以下脚本添加到新的 Vue 组件:
<script>
import { mapActions } from "vuex";
export default {
name: "UpdateTourPackageForm",
props: {
bodyRequest: {
type: Object,
required: true,
default: { listId: 0, name: "missing name",
whatToExpect: "missing what to expect",
mapLocation: "missing map location",
price: 0, duration: 0, instantConfirmation: true,
},
},
},
methods: {
...mapActions("tourModule",
["updateTourPackageAction"]),
onSubmit() {
this.updateTourPackageAction(this.bodyRequest); //
fyi, you might not see the results right away because
of the cache.
},
},
data: () => ({
dialog: false,
currencies: ["USD", "NOK"],
currencyValues: [0, 1],
durations: [1, 2, 3, 4, 5, 6, 7, 8],
durationValue: 1,
}),
};
</script>
的前脚本提供了一个bodyRequest道具,使用mapActions,onSubmit方法,调用updateTourPackageAction,【病人】一组当地州——即dialog、currencies,currencyValues,durations、【显示】。
现在,让我们使用下面的代码添加template语法:
<template>
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-
width="600px">
<template v-slot:activator="{ on, attrs }">
<v-icon class="mr-3" v-bind="attrs" v-on="on">
mdi-clipboard-edit-outline
</v-icon>
</template>
<v-card>
<form @submit.prevent="onSubmit">
<v-card-title>
<span class="headline">Update Tour
Package</span>
</v-card-title>
<v-card-text>
<!-- Insert <v-container> Here -->
<small>*indicates required field</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text color="blue darken-1"
@click="dialog = false">
Close
</v-btn>
<v-btn text color="blue darken-1"
@click="dialog = false" type="submit">
Update
</v-btn>
</v-card-actions>
</form>
</v-card>
</v-dialog>
</v-row>
</template>
前面的模板语法块是UpdateTourPackageForm.vue的 UI。 该模板使用了v-card组件,它看起来很整洁,并且采用具有@submit指令事件来调用或触发onSubmit函数的形式。
接下来,我们用替换<!—Insert <v-container> Here -->以下模板语法:
<v-container>
<v-row>
<v-col cols="12">
<v-text-field required label="Name"
v-model="bodyRequest.name"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
label="What to expect"
v-model="bodyRequest.whatToExpect" required
></v-textarea>
</v-col>
<!-- Removed for brevity -->
<!-- Please see the full code in GitHub -->
</v-col>
</v-row>
</v-container>
前面的模板负责提供表单中所需的字段。 旅游套餐字段如下:name、whatToExpect、mapLocation、price、duration、instantConfirmation。 您将注意到字段正在使用bodyRequest道具,因为这些道具将是用户将要编辑的tourPackage。
现在,让我们用以下代码更新TourPackagesCard.vue:
import UpdateTourPackageForm from "@/components/UpdateTourPackageForm";
我们正将UpdateTourPackageForm导入TourPackagesCard.vue。 然后我们将UpdateTourPackageForm注册到components块中,如下所示:
components: { UpdateTourPackageForm },
现在我们可以在模板语法区域中使用UpdateTourPackageForm。
使用以下新代码更新template部分中的<v-list-item-action>组件:
<v-list-item-action>
<UpdateTourPackageForm :bodyRequest="tourPackage" />
<v-icon @click="removeTourPackage(tourPackage.id)">
mdi-delete-outline
</v-icon>
</v-list-item-action>
前面的模板代码块包含用于更新旅行团的 UI。
现在,在测试新表单之前,让我们更新一下 ASP.NET Core Web API。 转到Travel.Application.TourPackages.Commands.UpdateTourPackage命名空间的UpdateTourPackageCommandValidator.cs。 然后,像这样编辑Name的最大长度规则:
public UpdateTourPackageCommandValidator(IApplicationDbContext context)
{
_context = context;
RuleFor(v => v.Name)
.NotEmpty().WithMessage("Name is required.")
.MaximumLength(200).WithMessage("Name must not
exceed 200 characters.");
}
名称应该仅为200字符。 然而,这个还不够。 我们还将在前端创建一个验证,但这将在第 17 章,Input validation in Forms中介绍。
现在重新运行应用,看看应用是否仍然运行没有任何问题:

图 15.9 -铅笔板图标
图 15.9显示的是一个旅游套餐。 尝试通过单击铅笔板图标来更新旅行团,该图标将打开UpdateTourPackageForm组件。
现在,让我们总结一下您在这一长章中学到的关于 HTTP 请求、Vuex 状态管理和来自 Vuetify 的表单的内容。
总结
你已经完成了。 干得好! 现在让我们把你从这一章中学到的东西分解开来。 您已经学习了如何使用 Axios (HTTP 客户端库)进行 CRUD。 您已经学习了如何在 Vuex 中编写异步和非异步操作。 您还学习了如何在组件中使用mapActions和mapGetters。 最后,您已经学习了如何使用 Vuetify 来构建有吸引力的表单和按钮。
在下一章中,我们将为用户构建登录和注册表单。 我们还将在后端启用授权,以便只有经过认证的用户才能向我们的 ASP 发送 CRUD 操作.NET Core Web API。
十六、在 Vue.js 中添加认证
在本章中,你将学习如何通过构建注册表单和登录表单来使用 Vue.js 应用的认证过程。 您将创建一个服务认证和路由器 auth 警卫保护中的任何路由器或页面 Vue.js 应用。我们还将使用 JSON Web 标记(****JWT)当请求受保护的资源。 这一章是激动人心的一章,所以让我们开始吧!
我们将讨论以下议题:
- 设置 Vuex 进行认证
- 编写认证保护程序
- HTTP 拦截器
- 自动登录
技术要求
你需要 Visual Studio Code 来完成本章。
下面是本章的 GitHub 知识库的链接:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter16/。
设置 Vuex 认证
本章的第一节是关于在我们的 Vuex 状态管理中创建另一个模块,它将用于我们应用的 auth 模块。 本节还将帮助您学习如何向 Vuex 商店添加一个新模块。 这个模块将需要新的操作类型、新的操作、新的状态和新的突变。
在我们开始为我们的 auth 实现这个新模块之前,我们需要来自jsonwebtoken``npm库的帮助。 所以,让我们下载jsonwebtoken库。
执行以下命令npm:
npm i jsonwebtoken
前面的命令将在我们的应用中安装jsonwebtoken包。
然后,在src目录中创建一个新文件夹,并将其命名为auth。
现在,在auth目录中创建一个名为auth.service.js的 JavaScript 文件。 在auth.service.js文件中添加以下代码:
import api from "@/api/api-v1-config";
export async function loginUserAxios(login) {
return await api.post("Users/auth", login);
}
在前面的代码中,我们创建了一个服务,该服务接受一个登录对象并向/api/v1/Users/auth端点发送一个 POST 请求。
接下来,我们必须在store文件夹中创建一个文件夹,并将其命名为auth。
现在,在store/auth文件夹中创建一个名为types.js的 JavaScript 文件,并添加以下代码:
export const LOGIN_USER = "LOGIN_USER";
前面的代码是用于登录的操作类型。
接下来,我们必须在store/auth文件夹中创建另一个名为actions.js的 JavaScript 文件,并添加以下代码:
import * as types from "./types";
import { loginUserAxios } from "@/auth/auth.service";
export async function loginUserAction({ commit }, payload) {
try {
const { data } = await loginUserAxios(payload);
commit(types.LOGIN_USER, data.token);
} catch (e) {}
}
在这里,我们导入了auth操作类型和loginUserAxios服务,并在名为loginUserAction的新创建操作中使用它们。 loginUserAction操作需要一个有效载荷,该有效载荷将包含试图登录的用户的用户名和电子邮件。
接下来,我们必须在store/auth文件夹中创建另一个名为state.js的 JavaScript 文件,并添加以下代码:
const state = {
signInState: {
email: "",
exp: Date.now(),
sub: "",
token: null,
},
};
export default state;
我们的auth模块或名称空间的状态将有一个带有email、exp、sub和token属性的signInState模型。
现在,在store/auth文件夹中创建另一个名为mutations.js的 JavaScript 文件,并添加以下代码:
import * as types from "./types";
import * as jwt from "jsonwebtoken";
const mutations = {
[types.LOGIN_USER](state, token) {
state.signInState.token = token;
const loginClaim = jwt.decode(token);
claimToState(state, loginClaim);
localStorage.setItem("token", token);
},
};
export default mutations;
function claimToState(state, claim) {
state.signInState.sub = claim.sub;
state.signInState.email = claim.email;
state.signInState.exp = claim.exp;
}
这里,我们将导入类型和jsonwebtoken到mutations.js文件中。 然后在mutation函数中使用导入的类型和jsonwebtoken。 添加的突变接受一个标记,然后我们将其存储在signInState.token中。 在此之后,我们解码令牌并将解码后的令牌存储在loginClaim中。
您会注意到有一个名为claimToState的函数。 该函数将权利要求的sub、mail和exp映射到 signInState 的sub、mail和exp。
在signInState中存储声明的sub、email和exp属性之后,我们必须将令牌保存在浏览器的本地存储中。 存储的令牌值的键名是token。
接下来,我们必须在store/auth文件夹中创建另一个名为getters.js的 JavaScript 文件,然后添加以下代码:
const getters = {
email: (state) => {
return state.signInState.email;
},
isAuthenticated: (state) => {
return state.signInState.token;
},
};
export default getters;
在前面的代码中,我们为signInState.email创建了一个 getter,我们只是将其命名为email。
现在,为store/auth模块创建一个名为index.js的索引文件:
import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import * as actions from "./actions";
export default {
namespaced: true,
getters,
mutations,
actions,
state,
};
如果前面的代码看起来很熟悉,这是因为我们也在tour模块中编写了它。 导入和导出状态、getter、突变、动作和将namespaced属性设置为true是您在存储中创建新模块时要反复编写的内容。
设置为true的namespaced属性要求您在每次展开mapActions或mapGetters时写入模块的名称。 看一下下面的代码示例,它使用了一个foo模块:
...mapActions("fooModule", ["selectAction"]),
您会发现第一个参数是名称空间,然后第二个参数是动作。 前面代码中命名空间的目的是避免名称冲突,如果您有一个大型应用,并且使用许多具有相同名称的模块来执行操作。
现在,我们必须更新store文件夹的index.js文件:
import authModule from "./auth";
我们将authModule放入存储的index.js文件中,然后在modules对象中使用authModule:
export default new Vuex.Store({
modules: {
tourModule,
authModule,
},
plugins,
});
现在,authModule集成到我们的 Vuex 状态管理实现中。
好! 在下一节中,我们将学习如何保护 Vue 应用的页面。
编写认证保护程序
一个认证保护是我们路由器中的一个中间件。 我们可以编写函数并在路由中触发它们。 我们可以在路由器中放入一个函数的一个很好的例子是一个帮助进行认证的函数。 然后,该函数将检查应用的用户是否经过了认证。 然后,我们必须向经过认证的用户显示特定的页面。 让我们开始写 auth 守卫。
在auth文件夹中创建一个名为auth.guard.js的 JavaScript 文件,并添加以下代码:
import store from "@/store";
export const authGuard = (to, from, next) => {
console.log("authGuard");
const authRequired = to.matched.some((record) =>
record.meta.requiresAuth);
if (authRequired) {
if (store.getters["authModule/isAuthenticated"]) {
next();
return;
}
next("/login");
}
next();
};
在前面的代码中,我们定义了我们的authGuard。 这里,authGuard包含to、from和next属性。 我们也在检查一条路线是否被标记了true。 我们可以通过使用route对象中的authRequired元属性来实现这一点。
如果auth是必需的,我们必须检查isAuthenticatedgetter 是否返回true。 如果是这样,我们必须转向中间件的下一个管道。 如果没有,我们必须进入登录页面。
现在,让我们在router/index.js文件中更新我们的路由器,这样我们就可以使用刚才写的authGuard中间件:
import { authGuard } from "@/auth/auth.guard";
让我们把我们的authGuard带进来。
现在,写下面的代码,并把它放在路由器文件夹的index.js文件中的导出默认路由器代码行之前,像这样:
router.beforeEach((to, from, next) => {
console.log("router.beforeEach");
authGuard(to, from, next);
});
export default router;
router.beforeEach是一个全局前保护,当我们在应用中导航时,它就会触发。我们加入了console.log,这样你就可以看到beforeEach每次导航时都会运行。
现在,在auth/views文件夹中创建一个名为Login.vue的新的 Vue 组件,并添加以下脚本:
<script>
import { mapActions } from "vuex";
import router from "@/router";
export default {
name: "Login",
data: () => ({
dialog: true,
tab: null,
login: {
email: "",
password: "",
},
}),
methods: {
...mapActions("authModule", ["loginUserAction"]),
onSubmit() {
this.loginUserAction(this.login).then(() => {
this.$router.push({ path: "/admin-dashboard" });
});
},
navigateHome() {
router.push("/");
},
},
};
</script>
这里,我们在脚本中声明一些状态。 我们有一个dialog,一个tab和一个login对象。 然后导入mapActions和router并在methods块中使用它们。
接下来,我们创建了两个方法,即onSubmit和navigateHome。 onSubmit调用loginUserAction,如果loginUserAction成功,则必须导航到admin-dashboard路径。 同时,navigateHome会简单地把我们送到主页。
现在,让我们为Login.vue组件添加template语法。 然后,编写以下代码:
<template>
<!-- https://vuetifyjs.com/en/styles/colors/#material-
colors-->
<v-app>
<v-dialog
v-model="dialog" persistent
max-width="600px" min-width="360px"
@click:outside="navigateHome"
>
<div>
<v-tabs
show-arrows background-color="pink accent-4"
icons-and-text dark grow
>
<v-tabs-slider color="pink darken-4"></v-tabs-
slider>
<v-tab>
<v-icon large>mdi-login</v-icon>
<div>Login</div>
</v-tab>
<v-tab>
<v-icon large>mdi-account-box-outline</v-icon>
<div>Register</div>
</v-tab>
<!-- Login <v-tab-item> here -->
<!-- Register <v-tab-item> here -->
</v-tabs>
</div>
</v-dialog>
</v-app>
</template>
这里的template语法是一个dialog组件,它呈现两个选项卡——一个用于登录,另一个用于注册的选项卡。
现在,让我们用下面的模板语法来替换注释<!-- Login <v-tab-item> here -->:
<v-tab-item>
<v-card class="px-4">
<v-card-text>
<form @submit.prevent="onSubmit">
<v-row>
<v-col cols="12">
<v-text-field label="E-mail" v-
model="login.email"></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
type="password" label="Password"
hint="At least 8 characters"
v-model="login.password" counter
></v-text-field>
</v-col>
<v-col class="d-flex" cols="12" sm="6" xsm="12">
</v-col>
<v-spacer></v-spacer>
<v-col class="d-flex" cols="12" sm="3" xsm="12"
align-end>
<v-btn :disabled="false" color="primary"
type="submit"
>Login</v-btn>
</v-col>
</v-row>
</form>
</v-card-text>
</v-card>
</v-tab-item>
前一个login选项卡提供了一个text字段用于电子邮件,一个text字段用于密码。 字段用触发onSubmit方法的表单包装。
现在,让我们用下面的模板语法替换注释<!-- Register <v-tab-item> here -->:
<v-tab-item>
// … please go to the repo for the complete code
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
label="First Name"
maxlength="20" required
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
label="Last Name"
maxlength="20" required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field label="E-mail" required></v-text-
field>
</v-col>
<v-col cols="12">
<v-text-field
counter label="Password"
hint="At least 8 characters"
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
block counter
label="Confirm Password"
></v-text-field>
</v-col>
<v-spacer></v-spacer>
<v-col class="d-flex ml-auto" cols="12" sm="3"
xsm="12">
<v-btn :disabled="false"
color="primary">Register</v-btn>
</v-col>
</v-row>
…
</v-tab-item>
register选项卡应该为firstName、lastName、password和confirmPassword提供文本字段。 您可能还记得,我们没有在 ASP 中创建一个新的用户的 API。 净的核心。 我们将用户对象硬编码在 ASP 的UserService.cs文件中.NET Core Web API 项目:
new User
{
Id = 1,
FirstName = "Yourname",
LastName = "Yoursurname",
Email = "yoursuperhero@gmail.com",
Password = "Pass123!"
}
因此,我们可以使用的登录详细信息是我们在前面代码中提供的电子邮件和密码的值。
现在,让我们更新路由和login路径和about路径的元信息,如下所示:
{
path: "/login",
component: () => import("@/auth/views/Login"),
meta: {
requiresAuth: false,
},
},
上述代码将元信息添加到login的路由中。 现在,我们对about的路线做同样的处理:
{
path: "/about",
name: "About",
component: () => import("@/views/Main/About"),
meta: {
requiresAuth: false,
},
},
前面的代码将元信息添加到about的路由中。 requiresAuth将不同于admin-dashboard的路径,但是:
{
path: "/admin-dashboard",
component: () => import("@/views/AdminDashboard"),
meta: {
requiresAuth: true,
},
}
这里,我们将requiresAuth设置为true。 这意味着admin-dashboard和它的其他页面只能由经过认证的 web 用户访问。
接下来,我们必须更新components目录的NavigationBar.vue文件:
import { mapGetters } from "vuex";
让我们从 Vuex 导入mapGetter。
现在,让我们使用mapGetters和authModule键作为mapGetters中的命名空间,如下所示:
computed: {
...mapGetters("authModule", {
isAuthenticated: "isAuthenticated",
email: "email",
}),
},
现在,我们必须将isAuthenticated和email映射到自动为我们创建的本地状态。
现在,写一个来自 Vuetify 的v-btn组件,像这样:
<v-btn
v-if="isAuthenticated"
color="primary"
outlined
:to="{ path: '/admin-dashboard' }"
>
<span class="menu">Dashboard</span>
</v-btn>
<v-btn v-else color="primary" outlined :to="{ path:
'/login' }">
<span class="menu">Login</span>
</v-btn>
Dashboard菜单按钮仅对已通过认证的用户可见,而Login菜单按钮对未通过认证的用户可见。
在我们开始向后端发送请求之前,让我们将[Authorize]属性放在ApiController.cs内部,命名空间为Travel.WebApi.Controllers.v1:
[Authorize]
我们在这里注释掉了Authorize属性,以便 ASP 的资源.NET Core Web API 受到保护。
让我们重新运行服务器,并选择屏幕顶部的LOGIN菜单:

图 16.1 -登录表单
上面的截图显示了点击login菜单后的登录表单。 使用以下登录凭证进行认证:
- Email:
yoursuperhero@gmail.com - 密码:
Pass123!
您应该能够登录,但是将出现错误警告提示。 打开浏览器的开发工具,进入网络|预览选项卡:

图 16.2 - DevTools 的 Network 选项卡的 Preview 选项卡
前面的截图显示登录成功,但是发送 GET 请求获取旅游列表集合的方法失败。 获取请求失败,因为 ASP.NET Core Web API 在我们的请求头中没有找到任何 JWT 令牌。 因此,我们没有被授权。
我们将在下一节中修复头中的 JWT 令牌。 但在此之前,让我们检查一下浏览器的本地存储,看看 Vue 应用是否可以将 JWT 令牌保存在本地存储中。
成功的登录值以及令牌响应应该存储在本地存储中:

图 16.3 -在本地存储中保存的令牌
上面的屏幕截图显示令牌保存在浏览器的Local Storage区域。 我们将在请求中使用这个令牌来告诉 ASP.NET Core Web API,我们被授权在资源中读和写。 我们将在下一节中进行此操作。
HTTP 拦截器
什么是 HTTP 拦截器? HTTP 拦截器是一种拦截传入 HTTP 响应和传出 HTTP 请求的功能。 在将请求发送到 web 服务端点之前,我们可以自动更改或修改请求的头。 幸运的是,Axios 有 HTTP 拦截器接口可供开发人员使用。 那么,让我们开始吧!
我们必须做的第一件事是用以下代码更新src/auth文件夹的auth.service.js文件:
const key = "token";
令牌的键名就是key。
我们还将在auth.service.js文件中添加两个函数:
export function getToken() {
return localStorage.getItem(key);
}
export function logOut() {
localStorage.clear();
window.location = "/login";
}
getToken函数从本地存储中获取令牌的值,而logout函数清除本地存储中的任何存储值,然后将用户重定向到登录页面。
现在在api文件夹中创建一个新的 JavaScript 文件interceptors.js,并在其中写入以下代码:
import { getToken } from "@/auth/auth.service";
export function interceptorsInit(axiosInstance) {
axiosInstance.interceptors.request.use(function(options) {
const jwtToken = getToken();
if (jwtToken) {
options.headers["Authorization"] = `Bearer ${jwtToken}`;
}
return options;
});
axiosInstance.interceptors.response.use(
(response) => response,
(error) =>
Promise.reject(
(error.response && error.response.data) || "Something
went wrong"
)
);
return axiosInstance;
}
在这里,我们引入了getToken服务并创建了一个名为interceptorsInit的函数。 该函数接受一个 Axios 实例,然后使用它触发getToken。 如果令牌存在于本地存储中,则必须向带有Bearer空间令牌值的头部添加授权键。 请注意,令牌的值是令牌中Bearer字符串的插值值。
我们还为任何响应添加了拦截器。 如果愿意,您可以向响应添加任何修改。
现在,我们通过导入interceptorsInit来更新api文件夹的api-v1-config.js文件:
import { interceptorsInit } from "@/api/interceptors";
我们现在可以使用interceptorsInit。 让我们通过传递 Axios 的实例来使用interceptorsInit,如下所示:
api = interceptorsInit(api);
然后,interceptorsInit的return值被存储在同一个实例中,这将修改 Axios:
现在重新运行应用。 注销并再次登录,看看我们是否可以在登录后立即获取旅游名单。

图 16.4 - Tour Lists 状态码 200
从前面的截图可以看出,getTourListsAxios服务可以准确无误地获取旅游列表。 在这里,您可以看到auth端点的响应码为200,TourLists端点的响应码为200。
现在,让我们试着登出,看看令牌是否会从浏览器中删除。
打开浏览器的开发工具,进入应用选项卡,如下所示:

图 16.5 -空的本地存储
前面的屏幕截图显示注销会从浏览器中删除令牌。
此时,我们知道使用 Vuex 的认证正在工作,认证保护正在工作,应用的 HTTP 拦截器也在工作。 现在,如果用户关闭浏览器,然后在稍后返回到 web 应用呢?
在这里,我们必须创建一种机制,在这种机制中,如果本地存储中存在有效的令牌,则登录是自动的。 在下一节中,我们将构建这个特性,让用户不必输入凭据。
自动登录
为了让我们的网络用户在登录时获得一个对用户友好的 web 应用,我们将创建一个自动登录。 其思想是,每当一个令牌出现在本地存储中,我们将检查它是否是一个有效的令牌,然后使用它进行认证。 通过这样做,我们的网站访问者不需要重新输入他们的登录细节,这有时是他们的烦恼。
首先,让我们更新src/auth文件夹的auth.service.js文件,然后导入jsonwebtoken,如下所示:
import * as jwt from "jsonwebtoken";
稍后我们将使用 JWT loken 来解码令牌。
现在,在auth.service.js文件中添加两个新函数:
export function isTokenFromLocalStorageValid() {
const token = localStorage.getItem(key);
if (!token) {
return false;
}
const decoded = jwt.decode(token);
const expiresAt = decoded.exp * 1000;
const dateNow = Date.now();
return dateNow <= expiresAt;
}
export function getUserEmailFromToken() {
const token = localStorage.getItem(key);
if (!token) return false;
const decoded = jwt.decode(token);
return decoded.email;
}
isTokenFromLocalStorageValid获取令牌,解码它,然后检查它的过期时间,而getUserEmailFromToken从本地存储中获取令牌并解码后返回电子邮件。
让我们更新应用的路线:
import { isTokenFromLocalStorageValid } from "@/auth/auth.service";
这里,我们正在导入isTokenFromLocalStorageValid服务。
现在,我们将向登录路由添加一个名为beforeEnter的保护钩子。 beforeEnter保护我们放置它的路线。 我们将在应用读取login路由对象之前运行一些逻辑。 现在,更新login路由对象,如下所示:
{
path: "/login",
component: () => import("@/auth/views/Login"),
meta: {
requiresAuth: false,
},
beforeEnter: (to, from, next) => {
const valid = isTokenFromLocalStorageValid();
console.log("VALID::", valid);
if (valid) {
next("/continue-as");
} else {
next();
}
},
},
{
path: "/continue-as",
component: () => import("@/auth/views/ContinueAs"),
meta: {
requiresAuth: false,
},
},
在这里,我们将为我们的路线添加一个名为Continue As的新页面。 我们将使用Continue As页面让我们的网站访问者选择从令牌使用帐户或注销。 然后,如果本地存储中的令牌不再有效,我们将使用Continue As页重新路由用户。
现在,如果令牌仍然有效,我们将在全局状态中保存已解码的令牌。 更新store/auth文件夹的types.js文件,添加以下代码:
export const LOCAL_STORAGE_TOKEN_LOG_IN = "LOCAL_STORAGE_TOKEN_LOG_IN";
前面的代码是我们要添加到文件中的一个新的操作类型。 现在,让我们更新store/auth文件夹的actions.js文件,并添加以下代码:
import {
loginUserAxios,
isTokenFromLocalStorageValid,
getToken
} from "@/auth/auth.service";
这里,我们用引入isTokenFromLocalStorageValid服务和getToken服务。
现在,让我们为auth模块创建一个新的 Vuex 动作:
export function useLocalStorageTokenToSignInAction({ commit }) {
if (!isTokenFromLocalStorageValid()) {
return;
}
const token = getToken();
commit(types.LOCAL_STORAGE_TOKEN_LOG_IN, token);
}
这个新操作检查令牌的有效性,然后使用LOCAL_STORAGE_TOKEN_LOG_IN操作类型提交令牌。
现在,让我们更新store/auth文件夹的mutations.js文件:
[types.LOCAL_STORAGE_TOKEN_LOG_IN](state, token) {
state.signInState.token = token;
const loginClaim = jwt.decode(token);
claimToState(state, loginClaim);
},
新的突变将标记存储在signInState.token中,解码该标记,然后将解码后的标记的信息映射到signInState的其他属性。
接下来,我们必须用以下代码更新src/auth文件夹的auth.guard.js文件:
import { isTokenFromLocalStorageValid } from "./auth.service";
if (authRequired) {
if (store.getters["authModule/isAuthenticated"]) {
next();
return;
} else if (isTokenFromLocalStorageValid()) {
next();
return;
}
next("/login");
}
这里,我们引入了isTokenFromLocalStorageValid服务并使用它来验证authGuard中的令牌。
现在,我们可以创建ContinueAs.vue组件,这是我们最近添加到路由的页面。 为ContinueAs页编写以下脚本:
<script>
import { getUserEmailFromToken, logOut } from "@/auth/auth.service";
export default {
name: "ContinueAs",
data: () => ({
email: getUserEmailFromToken(),
}),
methods: {
handleLogOut() {
logOut();
},
onSubmit() {
this.$router.push({ path: "/admin-dashboard" });
},
},
};
</script>
ContinueAs页面有一个从getUserEmailFromToken函数获取其值的电子邮件状态。 ContinueAs页也有一个方法和onSubmit。 handleLogout方法将用户注销,而onSubmit方法将用户重定向到 Admin Dashboard 页面。
现在,对于ContinueAs页面的template语法,复制以下代码:
<template>
<div class="container" v-if="email">
<div class="text-h4 my-5">Do you like to continue as {{
email }}? 🔒</div>
<v-btn @click="onSubmit" color="primary" class="mr-
4">Yes</v-btn>
<v-btn @click="handleLogOut()" outlined color="primary">
No thanks, I'd like to log out</v-btn>
</div>
<div v-else class="text-h4 m-10">
Fancy meeting you here.
<v-btn @click="handleLogOut()" outlined color="primary"
>Go to Home Page</v-btn>
</div>
</template>
这个模板是一个简单的 UI,它让我们的网络用户可以选择是否继续使用他们的帐户。
现在,让我们用下面的代码更新views/AdminDashboard文件夹的index.vue文件script部分:
import { mapActions, mapGetters } from "vuex";
这里,我们再次导入了mapActions和mapGetters。 接下来,我们必须创建一个名为localstorageLogin的方法,它将调用useLocalStorageTokenToSignInAction:
methods: {
...mapActions("authModule",
["useLocalStorageTokenToSignInAction"]),
localstorageLogin() {
this.useLocalStorageTokenToSignInAction().then();
}
},
computed: {
...mapGetters("authModule", {
email: "email",
})
},
mounted() {
this.localstorageLogin();
},
我们还从 auth 模块中引入了emailgetter。 让我们用我们商店的状态来替换这里的硬编码文本邮件:
<v-list-item-content>
<v-list-item-subtitle>{{ email }}</v-list-item-subtitle>
</v-list-item-content>
侧边栏的电子邮件现在将根据用户的电子邮件地址变成动态的。
现在,重新运行应用,登录并关闭浏览器。 重新访问 Vue 应用的 URL。
你应该看到一个标题格式为的问题,请问你是否愿意继续使用,如下所示:

图 16.6 -自动登录,选择是否继续
前面的屏幕截图显示了ContinueAs页面,以及一个问题和用户的电子邮件地址。 Vue 应用将您重定向到ContinueAs页面,因为您的浏览器包含了从 ASP 获取的存储令牌.NET Core Web API 响应。
单击YES进入管理仪表板:

图 16.7 -侧边栏上的电子邮件地址
前面的屏幕截图显示了侧边栏上的yoursuperhero@gmail.com电子邮件地址,这意味着令牌的详细信息已经保存在全局状态存储中。
通过实现一些技术,我们完成了对 Vue.js 应用的保护。 仍然有一些缺失的参数,我们可以用来加强我们的应用的保护。
如果您正在构建一个复杂的 Vue 应用,并且只有很少的时间来完成您的项目,那么可以考虑使用身份即服务(Identity as a Service)来验证您的用户。 有几个选项,如 AWS Incognito、Azure AD B2C、CGP 身份平台、Okta 和 Auth0,我在我的几个项目中使用过它们,因为它们有一流的认证 SDK 和大量的文档。 我提到的身份即服务将显著加快您的开发速度,并帮助您确定应用中需要的安全措施。
所以,让我们总结一下,看看你在这一章中学到了什么。
总结
让我们快速总结一下你在本章中学到的东西。 首先,您学习了如何设置用于认证的 Vuex 商店,并练习了如何在商店中端到端地设置模块。 然后你学习了 auth 守卫,它保护路线。 您还学习了如何拦截传出的 HTTP 请求,这有助于自动修改请求的每个头。 最后,您学习了如何为您的用户创建一个简单的自动登录,以改善他们的用户体验。
在下一章中,我们将在表单中创建一些验证,以便在用户填写表单时提供更好的用户体验。
十七、表单中的输入验证
在前一章中,我们讨论了保护页面以及创建登录表单和注册表单。 但是,我们没有编写任何可以防止用户输入无效输入的验证。
本章将为表单添加验证,这将改善用户使用表单时的体验。 让我们开始。
我们将涵盖以下议题:
- 安装输入验证库
- 在表单中使用验证器
技术要求
你需要 Visual Studio Code 来完成本章。
以下是本章已完成存储库的链接:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter17/。
安装输入验证库
用户友好验证告知用户输入时字段无效的原因。 我们可以通过在表单的每个字段中编写一个验证器来从头构建它,并确保验证器是响应式的。 实现是可行的,但它需要大量的时间,并且代码的整洁程度将取决于开发人员。
那么,为什么不使用一个验证我们的库呢? 幸运的是,有几个 Vue.js 库用于验证,在本节中,我们将使用其中一个顶级库。
所以让我们安装一个输入验证库:
npm i vuelidate
vuelidate库是一个简单的基于模型的轻量级验证,我们可以将其用于我们的 Vue 应用。
现在,让我们在src/plugins文件夹中创建一个名为vuelidate.js的 JavaScript 文件,并应用以下代码:
import Vue from "vue";
import Vuelidate from "vuelidate";
Vue.use(Vuelidate);
前面的代码导入Vue和Vuelidate,然后将Vuelidate传递给Vue use方法。
接下来,我们用以下代码更新main.js:
import "./plugins/vuelidate";
让我们从刚才创建的文件中引入Vuelidate。 这是所有。
接下来,我们创建一个名为validators的文件夹,并在文件夹中创建一个名为index.js的 JavaScript 文件。 它应该像validators/index.js:
import { required, email, minLength } from "vuelidate/lib/validators";
export default {
login: {
email: { required, email },
password: { required, minLength: minLength(8) },
},
};
为了实现内聚性,我们在单独的文件中定义验证器。 我们有一个登录对象,它有一个电子邮件。 电子邮件是必需的,并且只能是电子邮件格式。 登录对象还包含一个密码对象,这也是必需的,至少 8 个字符。
简单易行,对吧? 现在让我们使用一些验证器。
在表单中使用验证器
在表单中使用验证器很容易,因为我们使用了一个npm库来验证表单,而且我们并不是在重复工作。
现在,让我们更新auth/views文件夹中的Login.vue页面:
import validators from "@/validators";
让我们导入已经创建的validators,然后在computed块中使用validators,如下所示:
computed: {
emailErrors() {
const errors = [];
if (!this.$v.login.email.$dirty) return errors;
!this.$v.login.email.email && errors.push("Must be
valid e-mail");
!this.$v.login.email.required && errors.push("E-mail
is required");
return errors;
},
passwordErrors() {
const errors = [];
if (!this.$v.login.password.$dirty) return errors;
!this.$v.login.password.required &&
errors.push("Password is required");
!this.$v.login.password.minLength &&
errors.push("Minimum characters is 8");
return errors;
},
},
validations: {
login: validators.login,
},
我们有一个针对电子邮件错误的computed方法。 初始化错误数组。 它检查email字段是否脏,意味着该字段的值发生了变化。 它还检查email字段是否无效。 emailErrors还检查邮件是否为空。
模式也与passwordErrors计算方法相同。 美元符号意味着属性对任何变化都是有反应的。
然后,我们声明一个带有login属性的对象的validations块,并将validators.login赋值给它。
下一步是用以下代码更新登录字段的v-text-field组件:
<v-text-field
label="E-mail"
v-model="login.email"
@input="$v.login.email.$touch()"
@blur="$v.login.email.$touch()"
:error-messages="emailErrors"
></v-text-field>
我们在前面的代码中添加了错误检查器。 错误检查器是反应性的; 我们一输入东西,它们就会运行,而我们的注意力就会分散。 :error-messages是来自 Vuetify 的一个指令,我们可以使用它来分配emailErrors的计算方法。 这很好,不是吗?
让我们也更新登录密码的v-text-field组件,如下所示:
<v-text-field
type="password"
label="Password"
hint="At least 8 characters"
counter
v-model="login.password"
@input="$v.login.password.$touch()"
@blur="$v.login.password.$touch()"
:error-messages="passwordErrors"
></v-text-field>
前面的代码将验证器附加到登录密码v-text-field。 我们使用的是带有值passwordErrors的:error-messages。
现在,运行应用,然后在登录表单中输入yoursuperhero,不完成电子邮件,然后在密码字段中添加两个字母:

图 17.1 -无效
图 17.1表明,没有@domain.com的yoursuperhero是无效的,同时两个应该是八个字母/数字的字母也是无效的。 字段从黑色变为红色,字段下面的红色文本表明验证器正在工作。
这很好。 现在我们有了一个概念证明,输入验证库正在工作。 让我们向其他表单添加更多的字段验证。
第一步是更新validators文件夹的index.js文件:
import {
required,
email,
minLength,
maxLength,
} from "vuelidate/lib/validators";
我们将maxLength添加到从vuelidate/lib/validators的现有导入中。
然后通过添加city、country和about来更新导出的对象:
export default {
login: {
email: { required, email },
password: { required, minLength: minLength(8) },
},
city: {
required,
maxLength: maxLength(90),
},
country: {
required,
},
about: {
required,
},
};
添加的大多数对象都是下面表单中的必需字段,我们将对其进行修正。
现在,用以下代码更新components文件夹的AddTourListForm.vue组件:
import validators from "@/validators";
让我们导入validators,然后添加一个带有错误检查方法的计算对象和验证对象,如下所示:
computed: {
cityErrors() {
const errors = [];
if (!this.$v.bodyRequest.city.$dirty) return errors;
!this.$v.bodyRequest.city.required &&
errors.push("City is required");
!this.$v.bodyRequest.city.maxLength &&
errors.push("Max length is 90");
return errors;
},
countryErrors() {
const errors = [];
if (!this.$v.bodyRequest.country.$dirty) return
errors;
!this.$v.bodyRequest.country.required &&
errors.push("Country is required");
// no need for max length because this is a dropdown
with options
return errors;
},
aboutErrors() {
const errors = [];
if (!this.$v.bodyRequest.about.$dirty) return errors;
!this.$v.bodyRequest.about.required &&
errors.push("About is required");
return errors;
},
},
validations: {
bodyRequest: {
// for brevity, please go to the github repo
},
},
我们有cityErrors计算方法,如果city字段为空且最大长度超过 90 个字符,则会增加一个错误。 虽然country字段只需要一个必需的验证,因为在下拉列表或自动补全组件中不需要最大长度,但是aboutErrors只检查字段是否为空。
同时,validations对象初始化一个bodyRequest对象,其属性来自validators/index.js文件。
现在,让我们通过编辑city输入的v-text-field来更新AddTourListForm.vue组件的template语法:
<v-text-field
label="City"
v-model="bodyRequest.city"
@input="$v.bodyRequest.city.$touch()"
@blur="$v.bodyRequest.city.$touch()"
:error-messages="cityErrors"
required
></v-text-field>
我们为:error-messages指令添加了cityErrors。 我们更新了@input和@blur事件。
我们还可以编辑template语法中的v-autocomplete组件,如下所示:
<v-autocomplete
:items="countryList"
label="Country"
v-model="bodyRequest.country"
@input="$v.bodyRequest.country.$touch()"
@blur="$v.bodyRequest.country.$touch()"
:error-messages="countryErrors"
required
></v-autocomplete>
前面的代码使用验证器和国家输入错误消息更新@input、@blur和:error-messages。
接下来,我们更新字段,实际上是文本区域,为about输入:
<v-textarea
label="About"
v-model="bodyRequest.about"
@input="$v.bodyRequest.about.$touch()"
@blur="$v.bodyRequest.about.$touch()"
:error-messages="aboutErrors"
required
></v-textarea>
在前面的代码中,让我们使用v-textarea组件中的计算错误和错误消息。 前面的代码还将验证附加到textarea。
现在,重新运行应用。
在City字段中写出 100 个字符的句子,选择一个国家,点击About文本区域,然后再点击City字段。 这是在我们的表单中创建一个示例条目,如下所示:

图 17.2 -新 Tour List 表单验证
图 17.2 显示Create New Tour List表单中的输入验证正在工作。 City字段中有错误,因为 City 的最大字符数为 90,而About字段为空。
活动
让我们通过完成 Register Form 的验证、AddTourPackageForm.vue的验证和UpdateTourPackageForm.vue的验证来测试您在 Vue.js 中的技能。
Vuelidate 的链接为https://vuelidate.js.org/。 还有另一个很好的输入验证库,您可能想要签出它。 该库名为VeeValidate(https://vee-validate.logaretm.com/v4)。 如果你打算使用 Vue.js 版本 3,我可以推荐 VeeValidate。 该库易于使用,并具有对开发人员友好的 api。 库还很好地集成了 Yup 验证,Yup 验证是用于验证的 JavaScript 模式构建器。
让我们总结一下你所学到的。
总结
让我们快速总结一下你在这一章中学到的东西来总结它。 您已经学习了如何安装 Vuelidate 并在 Vue 应用中设置它。 您还学习了如何使用 Vuelidate 在表单中创建输入验证,这为应用的表单提供了更好的可用性。
在下一章中,你会看到用 ASP 编写测试是多么重要,同时又是多么有趣.NET Core 应用。
十八、使用 xUnit 编写集成测试
在前一章中,我们在 web 应用的表单中编写了输入字段验证,以改善填写表单时的用户体验(UX)。 现在,我们将向 web 服务添加测试。
编写自动化测试是一个很大的主题——太大了,不可能在一本书中写出来,也不可能讨论您需要知道的所有细节。 然而,我们只会触及表面,并编写实际的测试。 我们将编写一些单元测试,然后在我们的 ASP 中编写集成测试.NET Core 应用。
本章将涵盖以下主题:
- 从自动化测试开始
- 在 Docker 容器中安装Microsoft SQL Server(MS SQL Server)
- 理解 xUnit
- 了解单元测试
- 了解集成测试
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019
- Visual Studio for Mac
- JetBrains Rider
- 一个码头工人客户
- 实体框架核心(EF 核心)命令行界面(CLI)
以下是本章已完成知识库的链接:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter18/
开始自动化测试
让我们首先定义什么是自动化测试。 自动化测试是编写代码来测试项目中的代码,然后以自动化的方式运行这些测试的过程——就是这样。
想象一下这样的场景:如果您想手动测试一个函数,那么您必须在浏览器中启动应用,填写要进行认证的表单,然后单击一些链接以访问您想要测试的函数。 然后,您必须填写表单中的所有字段,点击提交按钮,并在浏览器中查看该函数的反馈或结果。 假设您必须重复手动测试表单的方式,并尝试在不同的边界情况下测试表单。 这太疯狂了! 只是因为用户使用表单的方式不同,所以当您向表单添加更多字段时,这个值会呈指数增长。
如您所见,手动测试非常耗时。 手动测试您的功能的工作流可能每次都要花费几分钟,如果您的应用中有数百个功能,情况会更糟——想象一下吧。
那么,您将从自动化测试中得到什么呢?
自动化测试的好处
以下是你应该在应用中编写测试的原因:
-
频繁测试你的代码,用更少的时间:你可以经常用更少的时间测试你的应用代码。
-
在部署前捕获 bug:在部署应用之前捕获 bug。
-
有信心地部署:这非常重要,因为它允许您更有信心地部署应用。
-
Code that works after refactoring: The problem with refactoring code that has not been tested is that you have to manually test the code just to check that it is working again.
如果您要重构数百个方法或函数,那么这个过程将非常痛苦,因为 a)这非常耗时,b)随着应用的增长,您可能会忘记需要测试的部分。 使用自动化测试,每次重构代码时,都可以轻松地运行测试,以查看是否破坏了某些内容。
-
更多地关注质量:如果你更多地关注你正在编写的方法或函数的质量,确保每个方法在不同的环境下使用不同的输入,这会有所帮助。
这里有一个有趣的事实:上面提到的自动化测试的好处可以帮助我在晚上入睡。
回到我们的主题,为了设置我们的自动化测试,我们将用 MS SQL Server 代替 SQLite 来编写集成测试。 SQLite 有局限性,我们目前在 ASP 中使用它.NET Core 应用在建模、查询和迁移方面的限制。 在前面的章节中,我们使用 SQLite 来避免在 Windows、Linux 或 Mac OS X 中使用真正的数据库引擎来设置数据库,而是将重点放在应用的架构上。
首先,Windows 用户可以选择在 Windows 机器上或 Docker 容器中安装 MS SQL Server 实例。 相比之下,Linux 和 Mac OS X 用户只需要使用 Docker 来安装 MS SQL Server 实例。
SQL Server Management Studio(SSMS)适用于 Windows 系统:

图 18.1 - SSMS
这里有两个不同的连接字符串,一个用于 Windows,另一个用于 Docker。
对于 Windows 的 MS SQL Server,这里是连接字符串:
"DefaultConnectionUsingWindows": "Server=(localdb)\\mssqllocaldb;Database=TravelDb;Trusted_Connection=True;MultipleActiveResultSets=true"
将appsettings.json中数据库连接字符串的当前值替换为前面的值。
对于 Docker 中的 MS SQL Server,下面是连接字符串:
"DefaultConnectionUsingDocker": "Data Source=localhost,1433;Initial Catalog=TravelDb;User id=sa;Password=Pass123!;MultipleActiveResultSets=True"
将appsettings.json中数据库连接字符串的当前值替换为前面的值。
在下一节中,我们将在 Docker 中安装 MS SQL Server。
在 Docker 容器中安装 MS SQL Server
在使用Docker并安装 MS SQLServer 之前,让我们快速讨论一下 Docker。 如果您以前没有试过码头工人,码头工人是一个平台作为一种服务(PaaS)【显示】提供操作系统(OS【病人】)虚拟化程度的提供软件安装包名为【t16.1】容器。****
我建议在https://www.docker.com/resources/what-container阅读更多关于 Docker 和容器的内容,因为我们将在本章继续关注自动化测试的主题。
下面是安装 Docker 的链接:
- 安装 Docker Engine on Ubuntu:https://docs.docker.com/engine/install/ubuntu/
- 在 Mac 上安装 Docker Desktop:https://docs.docker.com/docker-for-mac/install/
- 安装 Docker Desktop on Windows:https://docs.docker.com/docker-for-windows/install/
在您的 Linux 机器、MacBook 或 Windows 上安装 Docker 后,您可以在 Docker 中安装 MS SQL Server。 以下是如何做到这一点的快速指南:https://devlinduldulao.pro/how-to-use-microsoft-sql-server-on-mac-for-development。
虽然这个指南只适用于 Mac 用户,但是相信我,在 Docker 中安装 MS SQL Server 也适用于 Ubuntu 和 Windows 用户。
下一个是 Azure Data Studio,这是一个跨平台的数据库工具,是 SSMS 的替代品。
上面的链接指向在 Mac 上使用 MS SQL Server 的指南,还提供了如何使用 Azure Data Studio 的详细信息。
接下来,在安装 Docker、MS SQL Server 和 Azure Data Studio 之后,让我们安装 EF SQL Server 提供商的 NuGet 包。
在Travel.Data项目中安装Microsoft.EntityFrameworkCore.SqlServerNuGet 包,然后在Travel.Data项目的DependencyInjection.cs文件中更新services.AddDbContext,代码如下:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(config
.GetConnectionString("DefaultConnection")));
前面的代码仅将UseSqlite替换为UseSqlServer。
接下来,我们删除Travel.Data的Migrations目录,并使用dotnet命令为 MS SQL Server 数据库创建一个新的迁移:
dotnet ef migrations add InitialCreate --startup-project ../../presentation/Travel.WebApi/
我们正在创建一个新的迁移,并将其与我们稍后将使用的 MS SQL Server 数据库同步。
接下来,我们通过运行以下命令来更新数据库:
dotnet ef database update --startup-project ../../presentation/Travel.WebApi/
在运行前面的dotnet efCore 命令后,MS SQL Server 数据库现在应该准备好了。
让我们通过在Travel.Data项目的Contexts目录中创建一个名为ApplicationDbContextSeed.cs的新 c#文件来创建DbContext,并添加以下代码:
namespace Travel.Data.Contexts
{
public static class ApplicationDbContextSeed
{
public static async Task
SeedSampleDataAsync(ApplicationDbContext context)
{
if (!context.TourLists.Any())
{
await context.TourLists.AddAsync(new TourList
{
City = "Oslo", About = "…",
Country = "Norway",
// Removed for brevity. See Github repo
});
await context.SaveChangesAsync();
}
}
}
}
你可以从本章完成的存储库中使用TourPackage对象获得样本TourList对象的全部细节。
现在,让我们通过更新代码来在Program.cs文件中创建一个数据库种子器:
public static async Task<int> Main(string[] args)
{
var name = Assembly.GetExecutingAssembly().GetName();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
// Removed for brevity. See Github repo
.WriteTo.Console().CreateLogger();
// creating a host builder here.
}
前面的代码用于记录器的配置。 我们将编写我们的数据库种子器与的以下代码:
try
{
Log.Information("Starting host");
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
// Removed for brevity. See Github repo
}
await host.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
await ApplicationDbContextSeed.SeedSampleDataAsync(context)将在应用启动后将样本数据播种到 MS SQL Server 数据库中。 构建并运行应用,以查看应用是否仍然运行而没有任何问题。
现在,我们准备进入下一节,这一节是关于 xUnit 和编写测试的。
了解 xUnit
xUnit是一个面向。net 的开源现代测试框架,由NUnit v2的作者编写,这是一个面向所有。net 语言的单元测试框架。 xUnit 是第一个兼容。net Core 的测试框架。
xUnit 是 Visual Studio 中。net Core 的默认测试工具,目前微软自己也在其现代项目中使用它。
xUnit 的特点
让我们来看看 xUnit 的特性,如下:
- 支持多平台:您可以使用它来测试。net Framework 应用、Xamarin 应用、。net Core 应用和 ASP.NET Core 应用。
- 支持并行测试执行:加速单元测试的执行。
- 支持数据驱动测试:您可以编写一个测试,然后通过不同的数据输入来查看不同的预期输出。
- 它被设计为可扩展:您可以添加更多的数据类型、属性和断言,还可以将 xUnit 与其他测试框架一起使用。
- 易于安装:您可以通过 NuGet 包进行安装。
好的,在学习了 xUnit 的基础知识之后,让我们开始使用它。
在 ASP 中使用 xUnit NET Core
我们的第一个任务是在根目录中创建一个文件夹并将其命名为tests。 进入新创建的tests目录,执行如下命令:
dotnet new xunit --name Application.IntegrationTests
上面的命令在tests目录中创建名为Application.IntegrationTests的xunit项目。
该命令会自动在xunit项目中添加以下包:
Microsoft.NET.Test.Sdk:这个包有 MSBuild 目标以及用于构建。net 测试项目的属性。xunit:该包由 xUnit 库组成,用于编写诸如xunit.core、xunit.assert和xunit.analyzers等测试。xunit.runner.visualstudio:这个包是 xUnit 测试框架的Test Explorer运行器。
现在,转到Application.IntegrationTests项目文件夹,像这样:
cd Application.IntegrationTests
然后为Travel.WebApi项目添加引用,如下所示:
dotnet add reference ../../src/presentation/Travel.WebApi/Travel.WebApi.csproj
现在,让我们将以下 NuGet 包添加到Application.IntegrationTests:
FluentAssertions:在我们的测试中编写断言的一种优雅且更好的方法Moq:最友好、最流行的。net 模仿库Respawn:将测试数据库重置为干净状态
安装完 NuGet 包后,再次进入“tests”目录,执行以下命令dotnet:
dotnet new xunit --name Application.UnitTests
前面的命令在tests目录中创建一个名为Application.UnitTests的新 xUnit 项目。
执行以下命令,进入Application.UnitTests:
cd Application.UnitTests
接下来,添加对Travel.Application项目的引用,如下所示:
dotnet add reference ../../src/core/Travel.Application/Travel.Application.csproj
现在让我们将添加一个 NuGet 包到Application.UnitTests。 我们将添加一个名为FluentAssertions的包。
现在,回到解决方案文件所在的Travel根目录,并运行以下dotnet命令:
dotnet sln add tests/Application.IntegrationTests/Application.IntegrationTests.csproj
前面的命令将Application.IntegrationTest项目添加到我们的 ASP 的解决方案文件中.NET Core 应用。
再将Application.UnitTests项目添加到我们的解决方案中,如下所示:
dotnet sln add tests/Application.UnitTests/Application.UnitTests.csproj
我们只对Travel.Application进行几个单元测试,其余的都是Travel.Application的集成测试。
应用的文件夹结构应该是这样的:

图 18.2 -添加测试项目后的文件夹结构
之前的屏幕截图显示了我们的集成开发环境(IDE)中的项目,其中Application.IntegrationTests和Application.UnitTests位于tests目录中。
现在,我们准备进入单元测试和集成测试部分。 但是,在继续之前,我强烈建议查看FluentAssertions网站(https://fluentassertions.com/)和阅读的文档,这样你有一个想法FluentAssertions可以做什么,当我们将使用这个来取代内置的维护应用编程接口(API【显示】)xUnit。****
**# 理解单元测试
单元测试测试应用的单元,而不需要外部依赖项,如消息队列、文件、数据库、web 服务等。 您可以在几秒钟内运行数百个单元测试,因为它们编写起来很便宜,而且执行起来很快,因此您可以很容易地验证应用中的每个构建块是否按预期工作。 因此,由于您没有使用类或组件的外部依赖性来测试它们,因此您不能对应用的可靠性有很大的信心。 这就是集成测试发挥作用的地方,我们将在后面讨论。
在我的日常工作中,我只为容易出错的运行时代码、复杂代码和算法逻辑的复杂代码编写测试,然后将所有精力投入集成测试。 无论如何,我对单元测试还有其他看法,但是稍后您将看到这些。 现在,让我们开始使用 xUnit 和FluentAssertions编写一些单元测试。
编写单元测试
在Application.UnitTests项目的根目录中创建一个名为Common的新文件夹。 然后,在新创建的Common文件夹中创建两个名为Exceptions和Mappings的文件夹。
测试异常
在新创建的Exceptions文件夹中创建一个名为ValidationExceptionTests.cs的 c#文件,并添加以下代码:
using System;
using System.Collections.Generic;
using FluentAssertions;
using FluentValidation.Results;
using Travel.Application.Common.Exceptions;
using Xunit;
namespace Application.UnitTests.Common.Exceptions
{
public class ValidationExceptionTests
{
[Fact]
public void
DefaultConstructorCreatesAnEmptyErrorDictionary()
{
var actual = new ValidationException().Errors;
actual.Keys.Should()
.BeEquivalentTo(Array.Empty<string>());
}
// another test method here.
}
}
在前面的代码中发生了什么? 属性用于编写没有方法参数的单元测试。 第一个单元测试检查ValidationException的默认构造函数并创建一个空数组字典。 类包含一个错误字典。 然后,我们可以使用FluentAssertions库对测试进行断言。
下面是另一种方法:
[Fact]
public void SingleValidationFailure
CreatesASingleElementErrorDictionary()
{
var failures = new List<ValidationFailure>
{
new ("Mobile", "Mobile is required.")
};
var actual = new ValidationException(failures)
.Errors;
actual.Keys.Should()
.BeEquivalentTo("Mobile");
actual["Mobile"].Should()
.BeEquivalentTo("Mobile is required.");
}
Should().BeEquivalentTo()是来自FluentAssertions库的扩展方法,用于将对象与传入BeEquivalentTo的值调用该方法。 然后,如果两者相等,断言将返回true。
在比较字符串以外的值时,首选FluentAssertions标准库。 Should().BeEquivalentTo()是一种扩展方法,可以用于不同的数据类型。
虽然我们还可以使用 xUnit 默认方法如Assert.True(),我们必须通过一个条件,返回true或false,我更喜欢在Assert.True()``FluentAssertions扩展方法因为它使两国的比较容易和可读性。
测试映射
在新创建的Mappings文件夹中创建一个名为MappingTests.cs的 c#文件,然后添加以下代码:
using System;
using AutoMapper;
using Travel.Application.Common.Mappings;
using Travel.Application.Dtos.Tour;
using Travel.Domain.Entities;
using Xunit;
namespace Application.UnitTests.Common.Mappings
{
public class MappingTests
{
// Removed for brevity. Go to this repo's Github
[Fact]
public void ShouldHaveValidConfiguration()
{
_configuration.AssertConfigurationIsValid();
}
[Theory]
[InlineData(typeof(TourList), typeof(TourListDto))]
[InlineData(typeof(TourPackage),
typeof(TourPackageDto))]
public void ShouldSupportMappingFromSourceTo
Destination(Type source, Type destination)
{
var instance = Activator.CreateInstance(source);
_mapper.Map(instance, source, destination);
}
}
}
那么,在前面的代码中您注意到了什么? 有Theory和InlineData属性。 Theory属性用于编写单元测试,通过该测试我们可以向方法传递一组参数,而InlineData属性用于向方法参数提供数据。
我们可以多次编写InlineData属性。 xUnit 将创建多个测试实例,并使用我们在InlineData中传递的测试用例参数填充它们。
最后一个使用InlineData的单元测试检查我们的mapper属性是否支持TourList到TourListDto和TourPackage到TourPackageDto的映射。
我们在代码中总共有 5 个单元测试。 最后一个单元测试乘以 2,因为我们使用了两个InlineData属性。
好的,现在您已经了解了如何使用 xUnit 和FluentAssertions编写单元测试。 没有那么复杂。 现在,我们可以继续下一节,这是关于集成测试的。
理解集成测试
什么是集成测试? 集成测试通过测试应用代码与文件、数据库等具体依赖项的集成来测试应用及其外部依赖项。
集成测试的执行时间较长,因为它们通常涉及读取或写入数据库,但反过来它们使我们对应用的健康状况更有信心。
如果单元测试的快速反馈循环会发送误导的结果,我不介意编写一个提供正确结果的缓慢集成测试。 复杂的反馈循环不值得这么快的速度。
我的观点已经说得够多了——让我们开始编写一些集成测试并测试 MediatR 实现的所有命令和查询。
编写集成测试
我们的第一个任务是在Application.IntegrationTests的根文件夹中创建一个新的appsettings.json文件。 复制Travel.WebApi的 appsettings.json文件的内容并将其粘贴到新创建的appsettings.json文件中,但是将连接字符串中的数据库名称从TravelDb更改为TravelTestDb。
接下来,我们将创建一个测试 fixture,以便我们可以共享多个测试的资源。
创建测试 fixture
在Application.IntegrationTests的根目录下创建一个名为DatabaseFixture.cs的 c#文件,并添加以下库和包:
using System;
using System.IO;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Respawn;
using Travel.Data.Contexts;
using Travel.WebApi;
现在,让我们定义一个实现IDisposable的DatabaseFixture类,如下所示:
namespace Application.IntegrationTests
{
public class DatabaseFixture : IDisposable
{
private static IConfigurationRoot _configuration;
private static IServiceScopeFactory _scopeFactory;
private static Checkpoint _checkpoint;
public DatabaseFixture()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.AddEnvironmentVariables();
_configuration = builder.Build();
var startup = new Startup(_configuration);
var services = new ServiceCollection();
services.AddSingleton(
Mock.Of<IWebHostEnvironment>(w =>
w.EnvironmentName == "Development" &&
w.ApplicationName == "Travel.WebApi"));
services.AddLogging();
startup.ConfigureServices(services);
_scopeFactory = services
.BuildServiceProvider()
.GetService<IServiceScopeFactory>();
_checkpoint = new Checkpoint
{
TablesToIgnore = new[] {
"__EFMigrationsHistory" }
};
EnsureDatabase();
} // static methods and IDispose are below ..
}
}
那么,我们在前面的代码中写了什么?
我们创建了一个名为ConfigurationBuilder的配置,以及名为ServiceCollection的服务。 我们使用Moq创建了一个 web 托管环境来运行Travel.WebApi项目中的应用。 我们还添加了一个检查点来重置数据库状态。
添加静态方法
添加静态方法,如下代码片段所示:
private static void EnsureDatabase()
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider
.GetService<ApplicationDbContext>();
context.Database.Migrate();
}
public static async Task ResetState()
{
await _checkpoint.Reset(_configuration
.GetConnectionString("DefaultConnection"));
}
public static async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
using var scope = _scopeFactory.CreateScope();
var mediator = scope.ServiceProvider
.GetService<IMediator>();
return await mediator.Send(request);
}
我们编写了一个名为EnsureDatabase的静态方法,用于运行数据库的迁移;另一个名为ResetState的静态方法用于在每次测试之前重置数据库状态。 多个数据库操作不会影响彼此的状态。
下面是最后两个静态方法。 你知道该怎么做,所以把它们也加进去。 代码如下所示:
public static async Task AddAsync<TEntity>(TEntity entity) where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider
.GetService<ApplicationDbContext>();
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task<TEntity> FindAsync<TEntity>(int id)
where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider
.GetService<ApplicationDbContext>();
return await context.FindAsync<TEntity>(id);
}
public void Dispose()
{
// Code to run after all tests
}
最后两个静态方法用于添加和查找实体。 最后,Dispose方法是一个全局分解,在每个测试方法之后调用它。
使用测试夹具
为了使用我们创建的类DatabaseFixture,让我们再次在Application.IntegrationTests的根文件夹中创建一个名为DatabaseCollection.cs的 c#文件,并添加以下代码:
using Xunit;
namespace Application.IntegrationTests
{
[CollectionDefinition("DatabaseCollection")]
public class DatabaseCollection :
ICollectionFixture<DatabaseFixture>
{
}
}
该类具有CollectionDefinition属性,并实现了名为DatabaseFixture的ICollectionFixture类型。
您将注意到,DatabaseCollection类中没有代码,因为它仅用于定义集合。 而且,CollectionDefinition属性中的字符串为测试集合标识提供了惟一名称。
接下来,在Application.IntegrationTests项目的根目录中创建一个TourLists文件夹。 然后,在新创建的TourLists文件夹中创建两个文件夹Commands和Queries。
TourLists /查询/ GetTourTests
现在,让我们在Queries文件夹中创建一个名为GetToursTests.cs的 c#文件,并添加以下代码:
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Travel.Application.TourLists.Queries.GetTours;
using Travel.Domain.Entities;
using Xunit;
namespace Application.IntegrationTests.TourLists.Queries
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class GetToursTests
{
public GetToursTests()
{
ResetState().GetAwaiter().GetResult();
}
// test methods here
}
}
那么,前面的代码中发生了什么? 首先,我们使用static DataFixture来访问基类的所有静态方法。
然后,我们有一个Collection属性来标识这个测试类所属的集合。 我们将传递一个DatabaseCollection字符串,使其成为该集合的一部分。
GetToursTests类将能够与其他测试共享DataFixture类的测试上下文。
在GetTourTests构造函数中,我们调用ResetState方法在每次测试运行之前将数据库重置为其原始状态。
以下是GetToursTests测试类的测试方法。 编写以下代码:
[Fact]
public async Task ShouldReturnTourLists()
{
var query = new GetToursQuery();
var result = await SendAsync(query);
result.Lists.Should().NotBeEmpty();
}
[Fact]
public async Task
ShouldReturnAllTourListsAndPackages()
{
await AddAsync(new TourList
{
City = "Manila", Country = "Philippines",
About = "Lorem Ipsum",
TourPackages = new List<TourPackage>
{
new()
{
Name = "Free Walking Tour Manila",
// Removed for brevity. Go to this repo's Github.
}
}
});
var query = new GetToursQuery();
var result = await SendAsync(query);
result.Lists.Should().HaveCount(1);
}
最后,ShouldReturnTourLists测试方法不应该为空,而ShouldReturnAllTourListsAndPackages方法应该有一个对象。
TourLists/Commands/CreateTourListTests
接下来,我们在TourLists目录的Commands文件夹中创建一个名为CreateTourListTests.cs的 c#文件,并添加以下代码:
// Removed for brevity. Go to this repo's Github.
using Travel.Application.Common.Exceptions;
namespace Application.IntegrationTests.TourLists.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class CreateTourListTests
{
public CreateTourListTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequireMinimumFields()
{
var command = new CreateTourListCommand();
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<ValidationException>();
}
// two test methods here
}
}
正如您在前面的代码中看到的,我们重用了static DataFixture、Collection属性和ResetState函数。 在我们稍后将创建的最后一个测试类中,可以期待这些。 无论如何,ShouldRequireMinimumFields测试方法应该抛出ValidationException错误,这是我们在Travel.Application项目中创建的自定义验证异常,因为测试试图创建一个没有所需属性的TourList对象。
以下是另外两种测试方法:
[Fact]
public void ShouldRequireAbout()
{
var command = new CreateTourListCommand
{
City = "Antananarivo",
Country = "Madagascar", About = ""
};
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<ValidationException>();
}
[Fact]
public async Task ShouldCreateTourList()
{
var command = new CreateTourListCommand
{
City = "Antananarivo", Country = "Madagascar",
About = "Lorem Ipsum"
};
var id = await SendAsync(command);
var list = await FindAsync<TourList>(id);
list.Should().NotBeNull();
list.City.Should().Be(command.City);
list.Country.Should().Be(command.Country);
list.About.Should().Be(command.About);
}
ShouldRequireAbout测试还应该抛出验证错误,因为About属性为空。 最后,ShouldCreateTourList测试应该创建一个TourList对象,因为所有必需的属性都包含在内了。
TourLists/Commands/DeleteTourListTests
接下来,我们在TourLists目录的Commands文件夹中创建一个名为DeleteTourListTests.cs的 c#文件,并添加以下代码:
using FluentAssertions;
// Removed for brevity. Go to this repo's Github.
using Xunit;
namespace Application.IntegrationTests.TourLists.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class DeleteTourListTests
{
public DeleteTourListTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequireValidTourListId()
{
var command = new DeleteTourListCommand
{
Id = 33
};
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<NotFoundException>();
}
// another test method
}
}
ShouldRequireValidTourListId应该在前面的代码中抛出未找到异常,因为 ID33不存在,但测试方法试图删除它。
下面是另一种测试方法:
[Fact]
public async Task ShouldDeleteTourList()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "Beirut", Country = "Lebanon",
About = "Lorem Ipsum"
});
await SendAsync(new DeleteTourListCommand
{
Id = listId
});
var list = await FindAsync<TourList>(listId);
list.Should().BeNull();
}
ShouldDeleteTourList列表应该是null,因为listId属性用于删除TourList对象。
TourLists/Commands/UpdateTourListTests
接下来,我们在TourLists目录的Commands文件夹中创建一个名为UpdateTourListTests.cs的 c#文件,并添加以下代码:
using FluentAssertions;
// Removed for brevity. Go to this repo's Github.
using Xunit;
namespace Application.IntegrationTests.TourLists.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class UpdateTourListTests
{
public UpdateTourListTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequiredValidTourListId()
{
var command = new UpdateTourListCommand
{
Id = 8, City = "Caracas",
Country = "Venezuela", About = "Lorem Ipsum"
};
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<NotFoundException>();
}
// another test method
}
}
ShouldRequiredValidTourListId应该在前面的代码中抛出未找到异常,因为它试图更新一个不存在的TourList对象。
对于下一个测试方法ShouldUpdateTourList,运行以下代码:
[Fact]
public async Task ShouldUpdateTourList()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "Al Ghanim", Country = "Qatar",
About = "Lorem Ipsum"
});
var command = new UpdateTourListCommand
{
Id = listId, City = "Doha",
Country = "Qatar", About = "Lorem Ipsum"
};
await SendAsync(command);
var list = await FindAsync<TourList>(listId);
list.City.Should().Be(command.City);
list.Country.Should().Be(command.Country);
list.About.Should().NotBeNull();
}
相反,在调用FindAsync方法之后,ShouldUpdateTourList测试不应该是null,因为已经创建并更新了TourList对象。
现在,让我们在Application.IntegrationTests项目的根目录中创建一个TourPackages文件夹。 然后,在新创建的TourPackages文件夹的中创建一个名为Commands的文件夹。
TourPackages/Commands/CreateTourPackageTests
接下来,我们在TourPackages目录的Commands文件夹中创建一个名为CreateTourPackageTests.cs的 c#文件,并添加以下代码:
using FluentAssertions;
// Removed for brevity. Go to this repo's Github.
using Xunit;
namespace Application.IntegrationTests.TourPackages.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class CreateTourPackageTests
{
public CreateTourPackageTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequireMinimumFields()
{
var command = new CreateTourPackageCommand();
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<ValidationException>();
}
// next test method here
}
}
ShouldRequireMinimumFields应该在前面的代码中抛出一个验证异常,因为它试图创建一个没有所需属性的TourPackage对象。
对于下一个测试方法ShouldCreateTourPackage,运行以下代码:
[Fact]
public async Task ShouldCreateTourPackage()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "New York", Country = "USA",
About = "Lorem Ipsum"
});
var command = new CreateTourPackageCommand
{
ListId = listId,
// Removed for brevity. Go to this repo's Github.
WhatToExpect = "Lorem Ipsum"
};
var packageId = await SendAsync(command);
var package = await
FindAsync<TourPackage>(packageId);
package.Should().NotBeNull();
package.ListId.Should().Be(command.ListId);
package.Name.Should().Be(command.Name);
// Removed for brevity. Go to this repo's Github.
package.WhatToExpect
.Should().Be(command.WhatToExpect);
}
然后,ShouldCreateTourPackage应该创建一个TourPackage对象并找到它,因为所有需要的属性都被填充了。
TourPackages/Commands/DeleteTourPackageTests
接下来,我们在TourPackages目录的Commands文件夹中创建一个名为DeleteTourPackageTests.cs的 c#文件,并添加以下代码:
using System.Threading.Tasks;
// Removed for brevity. Go to this repo's Github.
using Xunit;
namespace Application.IntegrationTests.TourPackages.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class DeleteTourPackageTests
{
public DeleteTourPackageTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequireValidTourPackageId()
{
var command = new DeleteTourPackageCommand
{
Id = 69
};
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<NotFoundException>();
}
// second test method here
}
}
ShouldRequireValidTourPackageId应该在前面的代码中抛出未找到异常,因为它试图删除一个不存在的TourPackage对象。
对于第二个测试方法ShouldDeleteTourPackage,运行以下代码:
[Fact]
public async Task ShouldDeleteTourPackage()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "Tashkent", Country = "Uzbekistan",
About = "Lorem Ipsum"
});
var packageId = await SendAsync(new
CreateTourPackageCommand
{
ListId = listId, Name = "Silk Road Adventures",
// Removed for brevity. Go to this repo's Github.
WhatToExpect = "Lorem Ipsum"
});
await SendAsync(new DeleteTourPackageCommand
{
Id = packageId
});
var list = await FindAsync<TourPackage>(listId);
list.Should().BeNull();
}
另一方面,ShouldDeleteTourPackage应该创建TourPackage对象,删除对象,并在调用FindAsync方法后接收null。
TourPackages/Commands/UpdateTourPackageDetailTests
接下来,我们在TourPackages目录的Commands文件夹中创建一个名为UpdateTourPackageDetailTests.cs的 c#文件,并添加以下代码:
using FluentAssertions;
using Travel.Application.Common.Exceptions;
using Xunit;
// Removed for brevity. Go to this repo's Github.
namespace Application.IntegrationTests.TourPackages.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class UpdateTourPackageDetailTests
{
public UpdateTourPackageDetailTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequireValidTourPackageId()
{
var command = new UpdateTourPackageCommand
{
Id = 88,
Name = "Free Walking Tour"
};
FluentActions.Invoking(() => SendAsync(command))
.Should()
.Throw<NotFoundException>();
}
// another test method here
}
}
ShouldRequireValidTourPackageId应该在前面的代码中抛出未找到异常,因为它试图更新一个不存在的TourPackage对象。
对于下一个测试方法ShouldUpdateTourPackage,运行以下代码:
[Fact]
public async Task ShouldUpdateTourPackage()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "Zagreb", Country = "Croatia",
About = "Lorem Ipsum"
});
var packageId = await SendAsync(new
CreateTourPackageCommand
{
ListId = listId,
Name = "Free Walking Tour Zagreb",
// Removed for brevity. Go to this repo's Github.
WhatToExpect = "Lorem Ipsum"
});
var command = new UpdateTourPackageDetailCommand
{
Id = packageId,
ListId = listId,
// Removed for brevity. Go to this repo's Github.
WhatToExpect = "Lorem Ipsum"
};
await SendAsync(command);
var item = await
FindAsync<TourPackage>(packageId);
item.ListId.Should().Be(command.ListId);
// Removed for brevity. Go to this repo's Github.
.Be(command.WhatToExpect);
}
相比之下,ShouldUpdateTourPackage方法应该创建TourPackage对象,更新它,并找到它,因为所有必需的属性和验证都已经满足。
TourPackages /命令/ UpdateTourPackageTests
接下来,我们在TourPackages目录的Commands文件夹中创建一个名为UpdateTourPackageTests.cs的 c#文件,并添加以下代码:
using Travel.Application.Common.Exceptions;
// Removed for brevity. Go to this repo's Github.
using Xunit;
namespace Application.IntegrationTests.TourPackages.Commands
{
using static DataFixture;
[Collection("DatabaseCollection")]
public class UpdateTourPackageTests
{
public UpdateTourPackageTests()
{
ResetState().GetAwaiter().GetResult();
}
[Fact]
public void ShouldRequireValidTourPackageId()
{
var command = new UpdateTourPackageCommand
{
Id = 4, Name = "Free Walking Tour"
};
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<NotFoundException>();
}
// two more test methods here..
}
}
在前面的代码中,ShouldRequireValidTourPackageId测试方法应该抛出未找到异常,因为它试图更新一个不存在的TourPackage对象。
对于下一个测试方法ShouldUpdateTourPackage,复制以下代码:
[Fact]
public async Task ShouldUpdateTourPackage()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "Rabat", Country = "Morocco",
About = "Lorem Ipsum"
});
var packageId = await SendAsync(new
CreateTourPackageCommand
{
ListId = listId,
Name = "Free Walking Tour Rabat",
// Removed for brevity. Go to this repo's Github.
WhatToExpect = "Lorem Ipsum"
});
var command = new UpdateTourPackageCommand
{
Id = packageId,
Name = "Night Free Walking Tour Rabat."
};
await SendAsync(command);
var item = await
FindAsync<TourPackage>(packageId);
item.Name.Should().Be(command.Name);
item.WhatToExpect.Should().NotBeNull();
}
ShouldUpdateTourPackage应该创建一个带有TourPackage对象作为属性的TourList对象,更新TourPackage,并在调用FindAsync方法后查看它,因为所有需求都已满足。
对于最后一个测试方法ShouldRequireUniqueName,复制以下代码:
[Fact]
public async Task ShouldRequireUniqueName()
{
var listId = await SendAsync(new
CreateTourListCommand
{
City = "Bogota", Country = "Colombia",
About = "Lorem Ipsum"
});
await SendAsync(new CreateTourPackageCommand
{
ListId = listId, Name = "Bike Tour in Bogota"
});
await SendAsync(new CreateTourPackageCommand
{
ListId = listId, Name = "Salt Cathedral Tour"
});
var command = new UpdateTourPackageCommand
{
Id = listId, Name = "Salt Cathedral Tour"
};
FluentActions.Invoking(() =>
SendAsync(command)).Should()
.Throw<ValidationException>()
.Where(ex => ex.Errors.ContainsKey("Name"))
.And.Errors["Name"].Should()
.Contain("The specified name already
exists.");
}
ShouldRequireUniqueName方法应该抛出一个包含Name键的验证异常,因为它试图使用另一个TourPackage对象中已经存在的名称。 我们已经编写了应用中所需的所有必要测试。 现在,转到解决方案的根目录,重新构建应用,并运行以下dotnet命令:
dotnet test
前面的dotnet test命令将运行Application.IntegrationTests和Application.UnitTests项目的测试。
运行所有测试的结果显示在这里:
Passed! – Failed: 0, Passed: 18, Skipped: 0, Total: 18, Duration: 2 s
没有失败的测试。 它意味着0测试失败,18测试通过,并且没有跳过测试。 另外,它们总共跑了2秒,非常快。 它们可能会运行3到5秒,这取决于您的计算机的规格或计算机上打开的应用。
您还可以在 IDE 中使用测试资源管理器运行所有测试。
下面是在 Visual Studio 2019 的测试资源管理器上运行测试的结果:

图 18.3 -在 Visual Studio 2019 的测试管理器上通过测试
前面的截图显示所有测试都通过了 Visual Studio 2019。
下面是在 Visual Studio for Mac 的测试管理器中运行测试的结果:

图 18.4 -在 Visual Studio 中通过测试
下面的截图显示了在 JetBrains Rider 的Test Explorer中运行测试的结果:

图 18.5 -通过 JetBrains Rider 的 Test Explorer 测试
这是它! 我们已经使用 xUnit 编写了自动化测试,并且以一种务实的方式编写了这些测试,但在我们的测试中包含了最佳实践,并使用了最新的包/库。
但是,我仍然鼓励您阅读更多关于使用 xUnit 编写测试的内容,以巩固在这里学到的知识。 这里有一些参考资料:
- .NET 测试:https://docs.microsoft.com/en-us/dotnet/core/testing/
- 使用 xUnit 测试 c#代码:https://auth0.com/blog/xunit-to-test-csharp-code/
- xUnit.net:https://xunit.net/
您将看到来自上述链接的测试中的断言不是FluentAssertions。 为了测试你学到了什么,你需要做一些练习。
活动
将您在链接的测试示例中看到的所有 xUnit 断言转换到FluentAssertions。
对于像您这样忙碌的开发人员来说,这确实是一个非常好的章节。 现在,让我们总结一下解决的问题。
总结
您已经了解了什么是自动化测试,以及它给软件开发带来的价值。 您已经学习了如何为 Linux 或 macOS 用户在 Docker 容器中安装 MS SQL Server 数据库。 您已经了解了什么是 xUnit,为什么它适合于构建现代的。net 应用,以及如何使用 xUnit 与FluentAssertions。 您还了解了单元测试和集成测试的优缺点。
在下一章中,我们将学习 ASP.NET Core 和 Vue.js 应用部署在容器和 Azure 云中。**
十九、使用 GitHub Actions 和 Azure 自动部署
在前一章中,我们在应用中编写了集成测试,以确保组件能够正确交互。 现在我们将部署 ASP。 使用新的持续集成(CI)/持续部署(CD)工具,GitHub Actions 将 NET Core 和 Vue.js 应用应用到 Azure 云服务中。 CI/CD 自动化了应用的构建、测试和部署,从而节省了时间并提高了应用开发的效率。
在本章中,我们将涵盖以下主题:
- 介绍 GitHub Actions -一个 CI/CD 工具
- 理解 GitHub 的行为
- 了解在何处部署
- 使用 GitHub Actions 自动部署到 Azure 应用服务
技术要求
以下是本章已完成存储库的链接:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter19/。
以下是你完成本章所需要的东西:
- An Azure account:https://azure.microsoft.com/en-us/free/
- A GitHub 账户:https://github.com/
介绍 GitHub Actions -一个 CI/CD 工具
你还记得在 CI/CD 到来之前将应用投入生产的方法吗? 当我们将应用部署到生产环境中时,需要使用复制粘贴、FTP、脚本或 SSH。 然后出现了 CI/CD,它帮助快速构建和发布应用,而无需担心手动配置和部署过程。 然后,一个名为 Hudson 的 CI/CD 工具诞生了,它最终成为 Jenkins。 最后,我们开始使用诸如 Octopus Deploy、Visual Studio Team Services、Azure DevOps 等工具进行部署,并与我们的存储库进行更好的集成。 上面提到的 CI/CD 工具为我们提供了一个单独的地方,在那里我们可以拥有我们的工具、所有代码和交付应用所需的配置。
然后 GitHub action 出现了。
了解 GitHub 操作
GitHub Actions是一个自动化软件开发工作流的平台。 CI/CD 管道是您可以使用 GitHub 操作自动化的多个工作流之一。
GitHub Actions 是一个基于 yaml 的工作流,用于获取存储库。 这个工作流可以通过 webhook 或 schedules 来触发,也可以通过手动单击按钮来启动工作流。
运行工作流的运行程序有两种形式。 你可以选择使用 GitHub 托管或者自己托管。 GitHub 托管的工作流提供了不同的操作系统,如 Ubuntu、macOS 或 Windows,它们都预先安装了软件,包括几个版本的。net Core SDK。
GitHub 操作在运行时也会生成日志。 您可以通过 GitHub 门户中提供的日志查看构建的结果。
此外,还可以使用社区构建和共享的自定义工作流。 与 Azure 团队一样,一些组织已经贡献并开发了带有不同场景的操作和工作流,供我们在工作流中作为构建块使用。
。net 应用的 GitHub 动作
那么,我们如何让。net Core 应用与 GitHub Actions 连接起来呢? 有一个针对。net 的 GitHub 动作,叫做setup-dotnet。 什么是 setup-dotnet? 它为运行器提供了一个。net CLI 环境来指定要使用的。net 版本。 我们现在可以在setup-dotnet操作中使用 GitHub 操作来在我们的工作流中选择目标版本。
我们还可以配置我们的环境来使用私有的包存储库或源。 例如,如果我们有托管为 NuGet 包、GitHub 包或 Azure DevOps 工件的库或包,我们可以通过使用动作安全地利用它们。
了解在何处部署
稍后您将看到工作流模板,使用这些模板,您不必从头开始编写工作流文件。 你可以部署到 Azure, AWS, GCP, IBM Cloud,阿里巴巴 Cloud, OpenShift 等等。
我们将专注于 Azure,因为我们使用的是 c#和。net Core。 下面是 Azure 中最常见的服务:
- Azure 应用服务
- Azure 的功能
- Azure 静态 Web 应用
- Azure Kubernetes 服务
让我们看看这些是什么以及它们的用例。
什么时候部署到 Azure App Service?
如果您遇到以下问题,请考虑使用 Azure App Service:
- 当你有一个 web 应用来自任何编程语言和框架,或 ASP.NET 剃须刀页面,ASP.NET MVC,或者 asp.net.NET Web API,甚至服务器端 Blazor
- 当您希望部署专用的高性能应用或可伸缩应用时
- 当你想要更多的控制和配置的环境没有得到整个虚拟机或一个完整的操作系统,因为应用服务具有丰富的特性集,如槽,VNet 支持和 Azure Active Directory,等等,是建立
什么时候部署到 Azure 功能?
Azure Functions是 Azure 无服务器架构空间的一部分。 那么,什么时候应该考虑部署到 Azure 函数?
- 当您想要运行小型的无状态任务时,您可以使用无服务器架构在 Azure 中运行函数。 然而,还有一些有状态的和持久的函数,它们允许我们运行复杂的场景。
- 当你想要一个带有触发器和绑定的事件驱动架构; 例如,将 blob 文件放到 Azure Storage 容器中。
什么时候部署到 Azure 静态 Web 应用?
Azure Static Web Apps,或Azure SWA,是 Azure 中的新服务,允许您拥有一个静态站点。 那么,什么时候应该考虑部署到 Azure 静态 Web 应用?
- 当你根本不需要一个服务器来托管你的前端应用
- 当你使用 Blazor WebAssembly 应用
现在让我们看看 Azure Kubernetes Service,简称 AKS。
什么时候部署到 Azure Kubernetes 服务?
Azure Kubernetes 服务或AKS是来自 Azure 的托管 Kubernetes 服务。 那么,您应该在什么时候考虑将部署到 Azure Kubernetes 服务?
- 当您在容器编排方面没有任何专业知识,但您想使用容器进行部署
- 当你想要使用 Kubernetes 扩展和管理 Docker 容器或其他基于容器的应用
这些是部署到 Azure 时的选择。
因此,GitHub Actions 是快速设置 CI/CD 和 GitHub 存储库的一个很好的解决方案,用于构建和部署。NET Core 应用。 GitHub 提供了工作流、GitHub 门户内的运行器和可查看的日志,让我们的生活更轻松。 每当我们想要部署 ASP 时,我们都有大量可用的自定义工作流.NET Core 应用到 Azure 或其他类型的应用到其他已知的云服务。
现在,让我们将转移到下一节,以部署一个 ASP.NET Core Web API withProgressive Web App(PWA)Vue.js TypeScript App。
使用 GitHub Actions 自动部署到 Azure 应用服务
在最后一节中,我们将部署一个 ASP.NET Core 5 和 Vue.js 应用,尝试使用 GitHub 操作自动部署到 Azure 应用服务。
ASP.NET Core 5 应用将是一个 Web API 项目,而 Vue.js 应用将使用 TypeScript 构建,以显示我们可以在项目的csproj文件中添加一个 TypeScript 编译器。 我们还将把 Vue.js 应用转换为 PWA。 如果您还不熟悉 PWAs,那么 PWA 是一个具有移动应用特性的 web 应用。 PWAs 可以安装在桌面或移动设备上,并且可以离线运行。
我们将使用现有的 ASP.NET Core 和 Vue 应用简化部署到 Azure 使用 GitHub 行动。 这里的目标是尝试部署,并只关注 GitHub Actions 和应用服务,这是本章的主要主题。
好的,让我们开始:
-
创建一个名为
Travel的文件夹,然后使用命令行进入Travel文件夹,并运行以下dotnetCLI 命令:dotnet new sln -
Create a
webapiproject by running the following command:dotnet new webapi –name WebUI将创建一个名为
WebUI的项目。 -
Add the project to the .NET solution:
dotnet sln add WebUI/WebUI.csprojweb 项目现在在 IDE 中可见。
-
Go to the
WebUIproject directory:cd WebUI在
WebUI项目中,我们将在这里创建我们的 Vue.js 应用。 -
Run the following
vueCLI command:vue create client-app前面的命令将启动
vueCLI 来搭建 Vue.js 项目。 -
选择手动选择功能选项,然后添加TypeScript、PWA、Router。
-
取出绒毛,按进入,然后选择版本
3.x。 然后,敲击键盘上的Enter键几次,为其余配置选择默认设置。 -
We rename the
client-appfolder inside theWebUIproject toClientApp.如果你熟悉 ASP。 asp.net Angular 和 asp.net.NET React 模板,您将在 SPA 文件夹的命名中注意到模板和
ClientApp命名约定。 -
After renaming the folder of the Vue app, let's update
WebUI.csprojof the project with the code from the following GitHub repository: https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter19/Travel/WebUI/WebUI.csproj.上面链接中的代码类似于
Travel.WebApi项目的csproj文件。 唯一的区别是我们在TargetFramework属性下面添加了三个额外的属性,如下所示:<TypeScriptCompileBlocked>true</ TypeScriptCompileBlocked> <TypeScriptToolsVersion>Latest</ TypeScriptToolsVersion> <IsPackable>false</IsPackable>TypeScriptCompileBlocked被设置为true来帮助我们调试 Vue TypeScript 应用。TypeScriptToolsVersion针对的是最新的 TypeScript 版本。设置为
false的IsPackable将不会打包项目。 -
Next, we update the
Startup.csfile for theWebUIproject with the code from the following link: https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter19/Travel/WebUI/Startup.cs.
前面链接中的代码也很简单,可以帮助我们快速部署应用。 它是一个简单的 Web API 项目,将`VueCliMiddleware`与`SpaStaticFiles`服务一起导入。
- Next, we run the following command inside the
WebUIproject:
```
dotnet run
```
前面的命令将构建并运行后端和前端应用。 您应该会看到`webpack`进度达到`100%`,如下截图所示:

图 19.1 - Vue 应用的构建和运行
图 19.1 显示应用正在运行,没有任何问题。 您还可以检查`https://localhost:5001`以查看应用是否正在运行。
- Publish the project to your own GitHub account and put it in a private repository. After publishing your
Travelproject to GitHub, go to the Actions tab menu on your project's repository page.
您应该看到如图 19.2 所示的建议工作流,以及用于部署、持续集成和其他步骤的其他工作流。 这些工作流程将使我们的生活更轻松:

图 19.2 - GitHub 操作中的云服务
- 现在,在. net部分下的上单击设置此工作流。 不要点击提交,因为我们将不配置我们的工作流; 我们将让 Azure App Service 为我们做这件事:

图 19.3 -现成工作流示例
图 19.3 之前的是我们可以调整的预先配置的工作流。
现在让我们检查工作流文件。
工作流文件语法
让我们学习工作流文件的基本语法,以了解如何编写工作流的配置。
optional name属性是描述工作流正在做什么的地方。
on是声明事件的地方。 这里已经有了push和pull_request事件。 push每次有人推送到主分支时触发作业,pull_request每次使用主分支创建pull请求时再次触发作业。 一个很好的例子是测试我们的应用,以确保 pull 请求是可合并的。 您可以通过https://docs.github.com/en/actions/reference/events-that-trigger-workflows查看更多事件的详细解释和用法。
现在让我们来谈谈工作。 作业包含称为步骤的任务序列。
工作
工作是一组行动。 可以在工作流文件中定义一个或多个作业; 例如,一个为build,一个为test,另一个为deploy。 作业可以是任意的,比如工作流的名称。 该示例运行在ubuntu-latest上,它是运行在 GitHub 上的代理。 第一步或任务是actions/checkout@v2,它在运行构建或测试之前检出代码。
actions/checkout@v2中的动作路径意味着它是 GitHub 中托管的预定义动作之一。 这是惊人的,因为我们不需要自己创造行动。 但是等等,还有更多。 您可以访问https://github.com/actions查看包含您可能需要的所有操作的不同存储库的列表。 您还可以查看操作文档(带有一些代码示例的https://docs.github.com/en/actions)。
第二步使用名为setup-dotnet的操作,它是此操作列表中的另一个存储库。 这个操作实际上是用文件中定义的dotnet的特定版本准备环境。 与 Jenkins 不同的是,你不需要在 GitHub Actions 中指定你想在环境中安装或配置任何东西。
如您所注意到的,您通过在 use 中定义操作来使用操作。 要运行命令,可以使用run属性。 在示例工作流,build的一步,也称为构建的步骤,触发dotnet build --no-restore命令,一步test,也被称为【显示】测试步骤,运行dotnet test --no-build --verbosity normal命令。****
所有的步骤都是在流程中的相同环境中完成的,在此流程中签出代码,安装 dotnet 版本,然后在相同的环境中调用dotnet build和dotnet test。
继续,现在让我们在 Azure Portal 中创建一个 Azure 应用服务。
在 Azure 门户中创建 Azure App Service 实例
我们将创建一个 AzureApp Service 实例,这样我们就可以轻松地将应用部署到 Azure 上:
-
Go to Azure App Service in https://portal.azure.com/#home:
![Figure 19.4 – Creating an App Service instance in Azure]()
图 19.4 -在 Azure 中创建一个 App Service 实例
前面的图 19.4显示了我们将要创建的 App Service 实例的详细信息。 我们将使用代码和。net 5 运行时发布应用。 点击Create按钮,添加配置后您将看到这个按钮。
-
Now go to the Azure App Service instance that has been created and click on Deployment Center:
![Figure 19.5 – The deployment center of App Service]()
图 19.5 - App Service 部署中心
图 19.5 显示CI/CD 未配置。 让我们通过进入设置来配置 CI/CD。
-
Add the private GitHub repository you created earlier:
![Figure 19.6 – Adding the private GitHub repository]()
图 19.6 -添加私有 GitHub 存储库
图 19.6 显示了我们如何添加之前创建的私有。net 项目。 我们也使用。net 5 作为运行时。
-
Then, click Preview file to see the workflow:
![Figure 19.7 – Workflow YAML configuration file]()
图 19.7 -工作流 YAML 配置文件
图 19.7 显示了 App Service 为我们创建的工作流 YAML 配置文件中的五个步骤。 由于 YAML 配置文件是代码或存储库的一部分,所以我们不应该将纯文本凭证放在文件中。 我们可以使用占位符来引用秘密。 稍后我将向您展示这些秘密在 GitHub 中的位置。
-
现在点击Save并进入项目的 GitHub 存储库。
-
Let's check out the Actions tab to see whether App Service could add the workflow file:
![Figure 19.8 – Workflow run]()
图 19.8 -工作流运行
图 19.8显示了一个带有绿色图标的工作流,这意味着在等待几秒钟后,应用已经成功构建并部署。
-
Let's click the workflow to see the jobs summary:
![Figure 19.9 – Summary of jobs]()
图 19.9 -作业汇总
图 19.9 显示了工作流的状态和总持续时间。
-
Now let's click on build-and-deploy to see the steps associated with the workflow job:
![Figure 19.10 – A build-and-deploy workflow job]()
图 19.10 -构建和部署工作流作业
图 19.10 显示了工作流作业的五个步骤。 步骤如下:
运行 actions/checkout@master
设置。net Core
Build with dotnet
dotnet publish
部署到 Azure Web App
您还会注意到,在运行配置文件的 5 个步骤之前和之后还有一些步骤。
-
Now go to App Service and click the Overview menu:
![Figure 19.11 – App Service overview]()
图 19.11 - App Service 概述
图 19.11展示了 App Service 的概述。
-
Now click the Browse link that will redirect us to the application's URL. You should see that Vue.js is running in the browser:

图 19.12 - Vue.js + TypeScript App
图 19.12 显示了我们之前创建的**Vue.js + TypeScript App**。
- Let's check out the end of the URL bar to see whether we can install the application:

图 19.13 -可安装的 PWA Vue.js 应用
*图 19.13*显示 Vue.js 应用是可安装的,这意味着它是一个 PWA 应用,并且仍然可以在离线模式下工作。
- 现在,转到 GitHub 知识库的设置标签,然后点击秘密菜单:

图 19.14 -动作秘密
图 19.14 显示了我们可以安全地保存应用的敏感密钥和秘密的位置。
现在让我们测试 GitHub Actions 的 CI/CD 能力和我们已经设置的工作流。
测试 GitHub Actions 的 CI/CD 能力和工作流
这里,我们将来看看我们的 CI/CD 配置和 GitHub Actions 中的设置是否满足我们的需求:
-
运行
git pull命令将 GitHub 存储库中的更改转移到本地机器上。 -
Update the
Welcome to Your Vue.js + TypeScript Apptext or message like so:![Figure 19.15 – Edited HelloWorld msg props]()
图 19.15 -编辑 HelloWorld 的 msg 道具
图 19.15 显示了
Home.vue文件中HelloWorld组件编辑后的msg道具。 -
Next, we commit and push your changes to the master. Go to GitHub Actions to see that the workflow was triggered. Wait for the build and deploy job to finish:
![Figure 19.16 – Testing CI/CD]()
图 19.16 -测试 CI/CD
图 19.16 显示 CI 功能正在工作,几秒钟后 CD 功能也在工作。
-
Open a new tab in the browser and go to the web app's URL to see that the changes we made in the application are now reflected on the internet:
![Figure 19.17 – Showing the updated Vue.js + TypeScript app]()
图 19.17 -显示更新后的 Vue.js + TypeScript 应用
图 19.17显示了修改后的单词和表情符号。
-
现在,转到
{yourSubDomain}.azurewebsites.net/weatherforecast查看 Web API 的WeatherForecast端点是否可以响应 JSON 数据:

图 19.18 -来自天气预报端点的响应
图 19.18 显示WeatherForecast端点以 JSON 格式的数组响应。
Vue.jsTypeScript 和 asp.net 的控制器.NET CoreWeb API 项目使用 GitHub Actions 在 CI/CD 中正常工作。
现在让我们总结一下所学到的东西。
总结
这是一个包装。 以下是本章的一些要点。 您了解了 GitHub 操作是什么,以及使用 GitHub 操作可以多么容易地部署。 您了解了部署到 Azure 应用服务、Azure 功能、Azure SWA 和 Azure AKS 的正确时间和用例。 您还学习了如何使用 GitHub 操作部署到 Azure App Service,包括工作流的 YAML 配置、可用的操作和 CI/CD。
你已经走了这么远了。 谢谢你完成了这本书,我为你和你学习新工具和新事物的热情感到骄傲。 如果你的项目需求与你从书中学到的问题和解决方案相匹配,你可以将你在这里学到的知识应用到一个项目中。
该课程已经教你如何构建一个 ASP.NET Core 应用和 Vue.js 应用,像一个高级开发人员,把的价值给你的客户或客户。
我建议你的下一步是买一本关于独立 ASP 的新书.NET Core 或 Vue.js 主题来巩固你从本书中学到的知识。
我们代表整个 Packt 团队和编辑,祝愿您在事业和生活的各个阶段都一切顺利。
这就跟你问声好!
我是 Devlin Duldulao, ASP 的作者.NET Core 和 Vue.js。 我真的希望您喜欢阅读这本书,并发现它对提高您在 ASP 中的生产力和效率有用.NET Core 和 Vue.js。
如果你能在 Amazon 上留下一篇评论,分享你对 ASP 的想法,这将对我(和其他潜在的读者!) NET Core 和 Vue.js。

您的评论将帮助我了解这本书的优点,以及未来版本中需要改进的地方,所以非常感谢。
最好的祝愿,
Devlin 巴西兰 Duldulao

第一部分:开始学习
本节讨论一个真实的场景:如何启动一个 web 应用项目。 本节包括以下章节:
第二部分:后端开发
本节讨论开发 ASP 的真实场景.NET Core 5 应用。 本节包括以下章节:
- 第三章, NET Core 项目
- 第四章,在 ASP 中应用 Clean Architecture.NET Core 解决方案
- 第五章,DbContext 和控制器的设置
- 第六章、深入 CQRS
- 第七章、CQRS 的作用
- 第八章、ASP 中 API 版本控制与登录 NET Core
- 第九章,固位 ASP NET Core
- 第 10 章、Redis 性能提升
第三部分:前端开发
本节讨论开发 Vue.js 3 应用的真实场景。 本节包括以下章节:
- 第十一章,Vue.js 在 Todo App 中的基本原理
- 第十二章,Using a UI Component Library and Creating Routes and navigation
- 第 13 章、用 ASP 集成 Vue.js 应用 NET Core
- 第十四章,使用 Vuex 和发送 GET HTTP 请求简化状态管理
- 第十五章,在 Vue.js 中使用 Vuex 发送 POST, DELETE, PUT HTTP 请求
- 第十六章,Vue.js 中添加认证
第四部分:测试和部署
本节讨论现实世界和趋势测试,以及将 web 应用部署到生产环境。 本节包括以下章节:








































浙公网安备 33010602011771号