八、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 的版本:*
*<image/Figure_8.1_B15970.jpg>
图 8.1 -版本选择器
图 8.1 显示了一个下拉菜单,让您选择要查看的 api 版本。
现在让我们看看版本 1 中的WeatherForecast控制器。 你会注意到在下一张图片图 8.2中,版本 1WeatherForecast在 Swagger UI 如何呈现端点细节方面与版本 2 不同:
<image/Figure_8.2_B15970.jpg>
图 8.2 -弃用 API
图 8.2 中中的默认样式,带有褪色的颜色和一个插入的端点名称,告诉开发人员该端点已经被标记为 deprecated。
现在,让我们尝试WeatherForecast端点的版本 2,这是一个 POST 请求,需要添加一个query参数的关键和城市的价值正如它的名字你想用你的请求的数据:
**<image/Figure_8.3_B15970.jpg>
图 8.3 -天气预报第二版
在 Swagger UI 的帮助下,我们可以方便地在输入框中插入查询参数。 让我们在文本框中键入Oslo并执行它。
让我们看看点击Execute按钮后端点的响应:
<image/Figure_8.4_B15970.jpg>
图 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 产生的日志事件:
<image/Figure_8.5_B15970.jpg>
图 8.5 -日志目录中的 SQLite 文件和 JSON 文件
让我们使用 SQLite 浏览器或 SQLiteStudio 来查看 SQLite 文件。 在图 8.6中,你会注意到RenderedMessage和Properties是 JSON 格式的:
<image/Figure_8.6_B15970.jpg>
图 8.6 -在 SQLite 文件中记录事件
格式将能够帮助任何数据可视化工具在 UI 图中呈现数据。 它还有助于任何数据可视化工具查询您想从日志事件中提取的特定细节。
现在让我们检查 JSON 文件。 你还会在图 8.7中看到日志事件以 JSON 格式存储:
<image/Figure_8.7_B15970.jpg>
图 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如下:
<image/Figure_9.1_B15970.jpg>
图 9.1 -识别离子的两种形式
为了说明这一点,请参见前面的图 9.1,其中学生可能有两种形式的身份证明:他们的学生证和他们的驾驶证。 学生证上有学生号和学校名称。 他们的驾照上有身份证和出生日期。 每个标识都足够灵活,允许访问不同的资源。 ClaimsPrincipal从相关的身份继承索赔列表。 在这个场景中,驾照索赔和学生证索赔识别学生用户。
Cookie 认证是 ASP 中最常见的认证方式.NET Core 应用。 很高兴知道 ASP.NET Core 内置了中间件,使应用能够利用基于 cookie 的认证。
另外,为了更好地概述 cookie 认证,让我向您展示一个图表图 9.2,如下图:
<image/Figure_9.2_B15970.jpg>
图 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 应用:
<image/Figure_9.3_B15970.jpg>
图 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 是自包含的,并且它在自身内保存所需的标识信息,如下图所示:
<image/Figure_9.4_B15970.jpg>
图 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:
<image/Figure_9.5_B15970.jpg>
图 9.5 - OpenAPI 的安全定义和需求
-
Now let’s check out the
/api/v1/Users/authroute of theUserscontroller:<image/Figure_9.6_B15970.jpg>
图 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:<image/Figure_9.7_B15970.jpg>
图 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:
<image/Figure_9.8_B15970.jpg>
图 9.8 -编码和解码的 JWT
我们看到从应用的 auth API 接收到的已编码的 JWT。 同时,右边是已解码的 JWT。 已解码的令牌显示头和有效负载。 报头显示签名或加密算法和令牌类型。 随后,解码的令牌还显示了令牌的有效负载,其中包含用户的 ID,在时间之前无效,过期时间,以及时间颁发的*。*
-
Now let’s check out the Authorize button of Swagger UI by clicking it:
<image/Figure_9.9_B15970.jpg>
图 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:
<image/Figure_9.10_B15970.jpg>
图 9.10 -使用 Postman 进行授权 API 测试
下面的截图显示了一个包含我们所期望数据的 200 状态码:
<image/Figure_9.11_B15970.jpg>
图 9.11 - 200 受保护 API 的 OK 响应
如果我们发送错误的 JWT 怎么办? 你认为申请表会寄回来什么? 删除标记中的一些字母,并将它们替换为您的名字。 然后按发送按钮发送请求:
<image/Figure_9.12_B15970.jpg>
图 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 服务器并不理想。
这里有一条建议。 在运行一个解决方案的多个实例时,不建议使用内存缓存,因为数据将不一致。 在多台服务器上工作时,有一种更好的缓存方法,这将在下一节中讨论。
分布式缓存
分布式缓存或全局缓存是具有专用网络的单实例或一组缓存服务器。 当应用到达分布式缓存时,如果与应用请求相关的缓存数据不存在,请求将重定向到数据库来查询数据。 否则,分布式缓存将只响应应用所需的数据。
这是两个服务器共享同一个分布式缓存实例的图:
<image/Figure_10.1_B15970.jpg>
图 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:
<image/Figure_10.2_B15970.jpg>
图 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响应:
<image/Figure_10.3_B15970.jpg>
图 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:<image/Figure_10.4_B15970.jpg>
图 10.4 -没有 Redis 缓存的 API 响应
-
Let’s send the same request to the same API to see whether the response time will be shorter:
<image/Figure_10.5_B15970.jpg>
图 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 上运行:
<image/Figure_10.6_B15970.jpg>
图 10.6 - Windows 上的另一个 Redis 桌面管理器
下面的截图显示了在 macOS 上运行的AnotherRedisDeskTopManager:
<image/Figure_10.7_B15970.jpg>
图 10.7 -另一个在 macOS 上的 Redis 桌面管理器
下面的截图显示了在 Ubuntu 上运行的 Redis GUI:
<image/Figure_10.8_B15970.jpg>
图 10.8 - Ubuntu 上的 Redis GUI
如果你不喜欢任何仪表盘或 GUI 的 Redis,你也可以使用一个 CLI 命令来调试或监控每一个命令处理你的 Redis 服务器。 运行命令开启监控:
redis-cli monitor
下面的屏幕截图显示了在运行redis-cli monitor后,您的请求在命令行中的样子:
<image/Figure_10.9_B15970.jpg>
图 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:
<image/Figure_11.1_B15970.jpg>
图 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:<image/Figure_11.2_B15970.jpg>
图 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:<image/Figure_11.3_B15970.jpg>
图 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:<image/Figure_11.4_B15970.jpg>
图 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,如下截图所示:
<image/Figure_11.5_B15970.jpg>
图 11.5 - VS Code 扩展
Vetur 是 Vue 语言服务器,而 vcode -icons 的设计目的是将图标带到 VS code 文本编辑器的文件和文件夹中。
现在你可以使用 Visual Studio Code 打开项目,查看 Vue CLI 为你生成的文件和文件夹:
<image/Figure_11.6_B15970.jpg>
图 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 应用的运行情况,如下图所示:
<image/Figure_11.7_B15970.jpg>
图 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按钮,在浏览器中看到结果:
<image/Figure_11.8_B15970.jpg>
图 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,如下所示:
<image/Figure_11.9_B15970.jpg>
图 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 表单:
<image/Figure_11.10_B15970.jpg>
图 11.10 -简单的表单
表单还没有准备好呈现我们在输入中添加的内容,但是我们可以在控制台中看到它是否工作。 现在打开你的 Chrome DevTools,进入控制台标签。 将check all emails写入输入字段,点击应用的Add New Todo按钮,可以看到它正在控制台注册:
**<image/Figure_11.11_B15970.jpg>
图 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,然后按输入,再次尝试该表单。 添加后,用户界面会看到检查所有邮件,如下截图所示:
<image/Figure_11.12_B15970.jpg>
图 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状态为空时,你应该看到一个悲伤的表情:
<image/Figure_11.13_B15970.jpg>
图 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。 然后点击检查所有邮件,用划线标记,如下图所示:
<image/Figure_11.14_B15970.jpg>
图 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功能。 运行应用并检查浏览器。 我们会得到以下截图:
<image/Figure_11.15_B15970.jpg>
图 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 仍然在工作,如下面的截图所示:
<image/Figure_11.16_B15970.jpg>
图 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 时,你会看到,如下截图所示:
<image/Figure_11.17_B15970.jpg>
图 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*****
498

被折叠的 条评论
为什么被折叠?



