ASP.NET Core Razor Pages in Action 11 客户端技术和 AJAX
本章涵盖
• 选择您的客户端技术
• 从客户端代码调用页面处理程序
• 在 Razor Pages 中使用 JSON
• 基于每个页面隔离 CSS 样式
我们使用的所有代码示例都依赖于在服务器上完整生成的页面的 HTML,除了一个领域:客户端验证。验证错误消息是使用客户端库生成的,特别是 Microsoft 的 jQuery Unobtrusive Validation 库。作为一项功能,客户端验证增强了网站的可用性。它为用户提供有关表单验证错误的即时反馈,而无需等待将整个表单提交到服务器进行处理。
如果您想让用户满意,可用性至关重要,而使用客户端技术的服务器端应用程序的可用性可以显著提高。在本章中,我们将介绍如何在 Razor Pages 应用程序中实现一些使用客户端技术的常见模式。由于它作为默认项目模板的一部分包含在内,因此我们将查看 jQuery 的 DOM作和发出由浏览器发起的异步 HTTP 请求。我们还将探索使用纯 JavaScript 的替代方案,并考虑 jQuery 的未来。
异步 HTTP 请求(通常称为 AJAX)使您能够从服务器获取数据,而无需整页回发,因此用户看不到这些数据。您可以对这些数据执行任何作。您可以使用它来执行计算或更新页面的某些部分。或者,您可以将数据作为文件下载提供。数据本身可以有多种形式。它可以是 HTML,也可以是 XML 结构,或者更常见的是 JSON。在本章中,您将了解如何使用页面处理程序方法从 AJAX 请求生成 HTML 和 JSON 数据,并了解此方法的局限性。您还将了解如何将数据成功发布到页面处理程序。
如果您想广泛使用 JSON,建议的方法是将 Web API 控制器构建到您的应用程序中,这为 RESTful HTTP 服务提供了基础。在本书中,我不介绍 API 控制器,但我们将探索 .NET 6 中引入的新简化的请求处理功能(基于最小的 API),该功能以相当少的仪式提供类似的收益。我们还将介绍 .NET 6 中的另一项新功能,该功能使您能够将 CSS 样式隔离到单个页面的范围,而无需增加 HTTP 请求的数量。在介绍一些代码示例之前,我们将讨论如何进行客户端开发。
11.1 选择客户端技术
毫无疑问,jQuery 库是有史以来使用最多的 JavaScript 库。它于 2006 年推出,很快成为作 DOM、处理事件、管理 CSS 转换和执行异步 HTTP 请求 (AJAX) 的实际方式。当 jQuery 启动时,这些 API 的实现在不同浏览器中差异很大。jQuery 充当适配器,提供一组在所有支持的浏览器中以相同方式工作的 API。
许多其他客户端库都依赖于 jQuery,包括领先的 UI 框架库 Bootstrap,它捆绑到默认的 ASP.NET Core Web 应用程序模板中。从 .NET 6 开始的新模板中包含的最新版本的 Bootstrap(版本 5)消除了对 jQuery 的依赖。如今,大多数浏览器都比以前更严格地遵守标准。他们中的许多人共享相同的底层技术。jQuery 旨在解决的问题已基本消失。
尽管如此,jQuery 仍包含在默认的 Razor Pages 应用程序模板中,以提供对客户端验证的支持。从长远来看,这种情况很可能会改变,因为 GitHub 上有一个未解决的问题,它讨论了客户端验证的潜在替代,以便它不再依赖于 jQuery (https://github.com/dotnet/aspnetcore/issues/8573)。尽管如此,jQuery 仍然被广泛使用,因此在本章中,我们将研究使用 jQuery 和纯 JavaScript 实现客户端解决方案。
存在其他客户端库。其中使用最广泛的可能是 React 和 Angular。Angular 更准确地称为应用程序框架,但两者都主要用于构建单页应用程序 (SPA),其中工作流逻辑在浏览器中执行,而不是在服务器上执行。它们可以用作 Razor Pages 应用程序的一部分,但如果您只想添加客户端功能的隔离实例,那么它们就有点矫枉过正了。
11.2 从 JavaScript 调用页面处理程序
本节重点介绍如何从客户端代码调用命名页面处理程序方法。我们将介绍如何使用部分页面返回可用于更新页面部分的 HTML 片段。然后,我们将探索如何将标准表单发布到页面处理程序和独立数据。执行此作时,我们将特别注意请求验证,请记住,默认情况下,它已融入 Razor Pages 框架中。最后,我们将介绍在与客户端代码中的页面处理程序交互时如何使用 JSON。
11.2.1 使用部分页面返回 HTML
在第一个示例中,您将了解如何将 HTML 代码片段异步加载到页面中。具体来说,当用户单击列表中的属性名称时,您会将属性的详细信息加载到 Bootstrap 模式中。在本练习中,您将使用部分页面。一个部分将包含模态框的大纲 HTML,即 head 和 foot,它们将包含在现有的 City 页面中。当用户单击 city 页面上的属性列表时,将从服务器加载模态主体。您将添加客户端代码,用于标识单击了哪个属性,然后向返回 PartialResult 的页面处理程序发出请求,如果您还记得第 3 章,则表示对部分页面的处理。它非常适合生成 HTML 片段,例如可能用于更新页面区域的片段。
您将创建要开始的模态框的轮廓。将新的 cshtml 文件添加到 Pages\Shared 文件夹。请记住,没有用于分部视图的模板,因此,如果您使用的是 Visual Studio,则可以选择 Razor View > Empty 模板,然后删除默认内容。将部分文件命名为 _PropertyModalPartial.cshtml,并将任何现有内容替换为以下代码。
列表 11.1 基于 Bootstrap 5 模态的 PropertyModalPartial 内容
<div class="modal fade" id="property-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Property Details</h5>
<button type="button" class="btn-close"
➥ data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
➥ data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
这段代码或多或少直接摘自 Bootstrap 5 的模态框文档。它是一个标准模态,其正文中没有内容。您已更改标题以使其与您的应用程序更相关,并添加了 id 属性,以便您可以从客户端代码中识别模式。您还向模态框添加了 fade 类,因此它在出现和消失时会发出动画效果。
现在,您需要为模态框提供一些内容。这将放置在另一个名为 _PropertyDetailsPartial.cshtml 的部件中,该部件也位于 Pages\Shared 文件夹中。它的内容很简单。将 Property 作为 Razor 文件的模型传入,并按如下方式呈现属性的详细信息。
清单 11.2 包含将要加载的属性详细信息的 partial
@model Property
<h3>@Model.Name</h3>
<address>
@Model.Address
</address>
<p>Availability: @(Model.AvailableFrom < DateTime.Now ?
➥ "Immediate" : Model.AvailableFrom.ToShortDateString())</p>
<p>Day Rate: @Model.DayRate.ToString("c")</p>
<p>Max Guests: @Model.MaxNumberOfGuests</p>
<p>Smoking permitted? @(Model.SmokingPermitted ? "Yes" : "No")</p>
接下来,您需要一个使用此部分生成 HTML 的页面处理程序。将以下处理程序方法添加到现有 OnGetAsync 方法之后的 City.cshtml.cs 文件中。
清单 11.3 将 HTML 生成为 PartialViewResult 的命名处理程序
public async Task<PartialViewResult> OnGetPropertyDetails(int id)
{
var property = await _propertyService.FindAsync(id);
return Partial("_PropertyDetailsPartial", property);
}
这是一个命名的处理程序方法。它采用 int 作为参数,表示所选属性的标识。它使用属性服务从数据库中获取属性详细信息,然后将其传递给部分文件,返回呈现的结果。您需要将属性服务注入到 PageModel 的构造函数中。
清单 11.4 将属性服务注入 CityModel 构造函数
private readonly ICityService _cityService;
private readonly IPropertyService _propertyService;
public CityModel(ICityService cityService, IPropertyService propertyService)
{
_cityService = cityService;
_propertyService = propertyService;
}
最后的步骤涉及对 City.cshtml 文件的一些更改。迭代所选城市中的属性集合,并在此文件中呈现其详细信息。属性的名称显示在 h5 标题中,该标题分配了 role=“button”,因此当用户将鼠标悬停在标题上时,光标将变为指针。您需要修改标题以添加一些数据属性。一个将帮助您识别特定属性,其他用于触发模态。在下面的清单中,我用几行中断了 h5 元素的结果声明,因此更容易注释代码。
清单 11.5 修改后的 h5 元素旨在触发模态框
<h5 role="button"
data-id="@property.Id" ❶
data-bs-toggle="modal" ❷
data-bs-target="#property-modal" ❸
>@property.Name</h5>
❶ data-id 属性具有分配给它的属性 Id 。您将检索此值并将其传递给您刚刚添加的命名页面处理程序。
❷ 这个 Bootstrap 自定义属性使元素成为模态框的触发器。
❸ 此属性采用目标模态框的 ID。
最后,在页面中包含模态部分(清单 11.6),并使用 scripts 部分添加一个 JavaScript 块。客户端代码使用 jQuery 向所有 h5 元素添加 click 事件处理程序。在单击处理程序中,从 data-id 属性中检索指定属性的 ID。构建一个 URL,其中查询字符串参数表示页面处理程序方法的名称及其 id 参数。这用于 jQuery 加载方法,该方法使用 GET 方法发出 HTTP 请求,然后将响应加载到指定的元素中。
清单 11.6 向页面添加 partial 和 script 块
<partial name="_PropertyModalPartial" /> ❶
@section scripts{
<script>
$(function(){
$('h5').on('click', function() { ❷
const id = $(this).data('id'); ❸
$('.modal-body').load(`?handler=propertydetails&id=${id}`); ❹
});
});
</script>
}
❶ 使用 partial 标签辅助函数来包含模态 partial。
❷ 为所有 h5 元素添加 click 事件处理程序。
❸ 在处理程序中,从 data-id 属性中检索所选属性的 ID。
❹ jQuery 加载方法使用 GET 方法调用指定的 URL,并将响应插入选择器中指定的元素中。在本例中,这就是模态体。
运行应用程序,单击主页上的一个城市,然后在结果列表中单击该属性的名称。详细信息应显示在模式中。
消除对 jQuery 的依赖,您可以改用 Fetch API,所有现代浏览器都支持它。您唯一需要做的更改是 scripts 部分。
清单 11.7 使用 Fetch API 调用命名处理程序
@section scripts{
<script>
document.querySelectorAll('h5').forEach(el => { ❶
el.addEventListener('click', (event) => { ❶
const id = event.target.dataset.id; ❷
fetch(`?handler=propertydetails&id=${id}`) ❸
.then((response) => {
return response.text(); ❹
})
.then((result) => {
document.querySelector('.modal-body').innerHTML = result; ❺
});
});
});
</script>
}
❶ 为所有 h5 元素添加 click 事件处理程序。
❷ 在处理程序中,从 data-id 属性中检索所选属性的 ID。
❸ 像以前一样向指定的 URL 发出请求。
❹ 在 Response 对象上调用 text() 方法,以字符串形式获取返回的内容。
❺ 将其分配给模态体。
如果您进行这些更改并运行应用程序,则应该不会看到行为有任何差异。Fetch 示例比 jQuery 示例更冗长一些,但一旦您了解了代码中发生的事情,它就没有那么复杂了。
Fetch 与 Promise 一起使用,这类似于 .NET 任务。它们表示异步作的未来结果。Fetch HTTP 调用以流的形式返回响应。使用 text() 方法将流读入字符串,然后使用结果字符串更新模态体。如果你不熟悉它并想了解更多关于 Fetch 的信息,我推荐 Mozilla Developer Network 文档作为一个很好的起点:http://mng.bz/vXO4。
在我们继续之前,必须了解进行这些异步调用时幕后发生的事情。您将使用浏览器开发人员工具查看正在发出的实际请求并检查其响应。每当您进行客户端工作时,浏览器开发人员工具都是必不可少的帮助,因为它们揭示了浏览器中发生的情况。您应该参考浏览器的文档以获取有关如何访问和使用它们的更多信息,但对于 Chrome 和 Edge 用户,您只需在打开浏览器时按 F12 键即可。显示工具后,单击 Network 选项卡,其中显示网络流量(请求)的详细信息,如图 11.1 所示。然后导航到其中一个城市页面,并单击属性名称。您应该会看到请求的详细信息出现。
图 11.1 该请求记录在 Network (网络) 选项卡中。
在此示例中,请求 URL 为 berlin?handler=properydetails&id=39。HTTP 响应状态代码为 200,类型指定为 Fetch,因为我的屏幕截图是使用 Fetch 示例获取的。如果我使用 jQuery 代码进行抓取,则类型将记录为 xhr,表示 jQuery 进行 AJAX 调用所依赖的浏览器的 XmlHttpRequest 对象。根据您的配置,您可能有不同的标题,但您可以右键单击任何标题来自定义您的视图。如果点击 Network 选项卡中的请求名称,则可以看到实际请求的更多详细信息(图 11.2),包括请求和响应标头以及请求中传递的任何数据的详细信息。
图 11.2 单击请求可查看更多详细信息。
您可以使用 Response (响应) 选项卡在右侧查看从服务器返回的实际响应。在图 11.3 所示的示例中,您可以立即从 Unicode 字符的存在中看出,除了我在第 3.1.5 节中介绍的默认基本拉丁字符集之外,我没有配置任何其他编码。
图 11.3 在 Network (网络) 选项卡中捕获原始响应。
因此,这是一个成功完成的简单 GET 请求。在下一个练习中,您将尝试将表单的内容发布到页面处理程序。
11.2.2 发布到页面处理程序
在模窗中拥有所选住宿的详细信息后,选择抵达和离开日期、指定同行宾客人数并获取住宿总费用将非常有用。在本节中,您将向模式添加一个表单,以便您执行此作,然后将表单内容发布到另一个处理程序方法,该方法将返回住宿的总成本。
首先,您需要一个输入模型来包装 Property 实例和表单值。将以下清单中的类声明添加到 CityModel 类中,使其嵌套在其中。
列表 11.8 将 BookingInputModel 作为嵌套类添加到 CityModel 中
public class BookingInputModel
{
public Property Property { get; set; }
[Display(Name = "No. of guests")]
public int NumberOfGuests { get; set; }
[DataType(DataType.Date), Display(Name = "Arrival")]
public DateTime? StartDate { get; set; }
[DataType(DataType.Date), Display(Name = "Departure")]
public DateTime? EndDate { get; set; }
}
除了 Property 之外,此类还包括您将添加到模态框的表单字段的属性。BookingInputModel 将替换 Property 作为模态框的模型。您尚未包含任何 BindProperty 属性,因为您将 BookingInputModel 作为参数传递给处理请求的处理程序方法。handler 方法的代码如清单 11.9 所示。它根据天数、日房价和客人数量计算住宿总费用。这也添加到 CityModel 中。
清单 11.9 OnPostBooking 处理程序方法
public ContentResult OnPostBooking(BookingInputModel model)
{
Var numberOfDays = (int)(model.EndDate.Value -
➥ model.StartDate.Value).TotalDays;
var totalCost = numberOfDays * model.Property.DayRate *
➥ model.NumberOfGuests;
return Content(totalCost.ToString("c"));
}
此处理程序方法返回 ContentResult,这是一种将字符串作为响应返回的方法。它不是您在实际应用程序中可能经常使用的东西。但是,它有助于简化此示例。此外,计算成本的基本逻辑通常属于业务逻辑层中的服务。但同样,我想让这个例子保持简单。下一个列表显示了添加到 _PropertyDetailsPartial.cshtml 文件的表单。同样,为了保持示例简单,我没有包括 validation。
清单 11.10 添加到物业详情部分的预订表单
<form id="booking-form">
<input type="hidden" asp-for="Property.DayRate" /> ❶
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" /> ❷
</div>
<div class="form-group">
<label asp-for="EndDate" class="control-label"></label>
<input asp-for="EndDate" class="form-control" /> ❷
</div>
<div class="form-group">
<label asp-for="NumberOfGuests" class="control-label"></label>
<input asp-for="NumberOfGuests" class="form-control" max="@Model.Property.MaxNumberOfGuests" /> ❸
</div>
</form>
❶ 包括一个包含所选属性的日价的隐藏字段。
❷ 添加到达日期和离开日期的输入。
❸ 使用 max 属性将数字输入限制为所选住宿可以容纳的最大客人数。
您还需要更改部分的模型类型。目前它是一个 Property。您将将其更改为 BookingInputModel,因此请将 _PropertyDetailsPartial.cshtml 的第一行替换为以下内容:
@model CityModel.BookingInputModel
接下来,向 modal partial 属性添加一个新按钮;充当动态加载属性详细信息的框架的 Partial。将以下清单中的粗线添加到 footer 元素中。
Listing 11.11 在模态框部分添加了 Book 按钮
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
➥ data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success"
➥ data-bs-dismiss="modal">Book</button>
</div>
最后,您需要将 new 按钮连接到一个 click 处理程序,该处理程序将表单发布到页面处理程序方法。jQuery 版本如下面的清单所示。
清单 11.12 使用 jQuery 向新按钮添加新的处理程序
$('.btn-success').on('click', function(){
const data = $('#booking-form').serialize(); ❶
$.post('?handler=booking', data, function(totalCost){ ❷
alert(`Your stay will cost ${totalCost}`); ❸
});
});
❶ 使用 jQuery serialize 方法将表单字段值转换为 URL 编码的字符串以进行发布。
❷ 发布到页面处理程序并定义一个将响应作为参数的回调函数。
❸ 将响应合并到浏览器警报中显示的字符串中。
您已准备好对此进行测试。在浏览器中获得新版本的 City 页面后,请确保在 network 选项卡上打开开发人员工具。然后点击住宿,输入预订的开始日期和结束日期,然后指定客人人数。单击 Book 按钮。现在,您应该在 Network (网络) 选项卡中看到 400 错误代码(图 11.4)。
图 11.4 请求失败,出现 400 错误码。
400 HTTP 状态代码表示格式错误的客户端请求。在 Razor Pages 中,此错误最常见的原因是 POST 请求缺少请求验证令牌。如果您回想一下第 6 章,当 form 标记帮助程序的方法设置为 post 时,标记将生成为隐藏字段。如果检查在清单 11.10 中添加的代码,则 form 元素上没有 method 属性;因此,未生成隐藏的输入。
在这种情况下,修复方法很简单。您只需将 method=“post” 添加到表单元素中,然后重新运行应用程序。将生成隐藏字段,并将其包含在发布到服务器的序列化值中。但是,当您使用 AJAX 发布值时,完全没有 form 元素的情况并不少见。例如,您可能希望发布计算结果,而不是表单字段的内容。那么在这些情况下,您有什么选择呢?
首先,您可以考虑禁用请求验证。这必须在 PageModel 级别通过在处理程序类上添加 IgnoreAntiforgeryToken 属性来完成。您不能在页面处理程序级别禁用请求验证(与 MVC作方法不同),因此,如果您的页面上有其他处理程序处理 POST 请求,您也将禁用这些处理程序的请求验证。这可能是不可接受的,而且禁用此安全功能通常是不可取的。这样,您就需要生成一个令牌值并将其包含在 AJAX 请求中。
可以使用 Razor 页面的 Html 帮助程序属性上的 AntiForgeryToken 方法呈现具有令牌值的隐藏输入:
@Html.AntiForgeryToken()
或者,您可以使用 @inject 指令将 IAntiforgery 服务注入页面,并使用其 GetAndStoreTokens 方法生成令牌集并访问生成的 RequestToken 属性。
清单 11.13 从 IAntiforgery 服务生成请求验证令牌
@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery antiforgery
@{
var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}
仅当 JavaScript 代码嵌入到 Razor 页面中时,此方法才适用,因为需要在脚本中呈现 C# 令牌变量。您不能在外部 JavaScript 文件中包含 C# 代码,因此,如果您希望将脚本排除在页面之外,则必须使用 AntiForgeryToken 方法来呈现隐藏字段。
接下来,在请求中包含令牌,作为表单字段(发布表单值)或作为请求标头值。表单字段的默认名称为 __RequestVerificationToken(带有两个前导下划线),请求标头的默认名称为 RequestVerificationToken(没有前导下划线)。重做此示例,而不使用 form 元素进行说明。首先,从属性详细信息中部分删除 form 标记,以便输入不再包含在表单中。接下来,将对 Html.AntiForgeryToken 方法的调用添加到 City Razor 页面。我将我的放在 partial 标签帮助程序的正上方:
@Html.AntiForgeryToken()
<partial name="_PropertyModalPartial" />
@section scripts{
您只需更改在脚本块中分配数据的方式。由于没有表单,因此无法再序列化表单,因此请创建一个对象,其属性将镜像页面处理程序期望作为参数的输入模型。您只需指定页面处理程序所需的属性值。下面的清单显示了 changed 的 button click 事件处理程序。
清单 11.14 更改 button click 事件处理程序以使用对象
$('.btn-success').on('click', function(){
const data = { ❶
startdate: $('#StartDate').val(), ❷
enddate: $('#EndDate').val(), ❷
numberofguests: $('#NumberOfGuests').val(), ❷
__requestverificationtoken: $('[name="__RequestVerificationToken"]').val(), ❸
property:{ ❹
dayrate: $('#Property_DayRate').val() ❹
} ❹
}
$.post('?handler=booking', data, function(totalCost){
alert(`Your stay will cost ${totalCost}`);
});
});
❶ 创建一个对象。
❷ 使用 jQuery val() 方法获取表单字段值并将其分配给属性。
❸ 令牌的 hidden 字段没有 id 属性,因此您可以使用其名称作为 jQuery 选择器。
❹ 输入模型的 Property 属性是嵌套的。
jQuery 库负责将 data 变量表示的 JavaScript 对象转换为 URL 编码的字符串,以便发布和为请求分配正确的内容类型标头 (x-www-form-urlencoded)。在按钮单击处理程序的纯 JavaScript 版本(在章节下载中提供)中,您可以通过包含顶级 DayRate 属性来拼合输入模型。这样,您就可以使用浏览器的 URLSearchParams 接口 (http://mng.bz/49vj) 生成适合轻松发布的 URL 编码值字符串。此接口无法序列化具有嵌套属性的对象。
11.2.3 使用 JsonResult
到目前为止,您已将 simple values 和 JavaScript 对象传递给页面处理程序。本节将开始研究如何使用 JSON,JSON 已成为 Web 应用程序中客户端和服务器之间交换数据的实际数据格式。在此示例中,您将使用纯 JavaScript,并将您从表单字段构建的 JavaScript 对象序列化为 JSON,然后再将其发布到页面处理程序。然后,您将转换页面处理程序方法以返回 JsonResult 而不是 ContentResult。
当您指定 x-www-form-urlencoded 作为请求的内容类型时,框架知道它应该解析请求正文中构成您发送到服务器的数据的已发布名称-值对。您可以通过将内容类型设置为 application/json 来让框架知道您何时发布 JSON。但你还要告诉页面处理程序从何处获取数据。您可以通过将 FromBody 属性应用于页面处理程序参数来实现此目的。清单 11.15 显示了页面处理程序方法在处理 JSON 所需的更改后的外观。在代码中,您创建一个匿名类型来表示返回的数据。虽然这适用于特殊情况,但更可靠的方法将涉及为返回类型声明类或记录,以便它们是可测试的。
清单 11.15 使用 JsonResult 和 FromBody 属性
public JsonResult OnPostBooking([FromBody]BookingInputModel model) ❶
{
var numberOfDays = (int)(model.EndDate.Value -
➥ model.StartDate.Value).TotalDays;
var totalCost = numberOfDays * model.Property.DayRate *
➥ model.NumberOfGuests;
var result = new { TotalCost = totalCost }; ❷
return new JsonResult(result); ❸
}
❶ 将返回类型更改为 JsonResult,并将 [FromBody] 属性添加到处理程序参数,以告知框架在何处查找 JSON 数据。
❷ 创建一个对象来表示返回的数据。
❸ 将对象传递给 JsonResult 构造函数,并对其进行适当的序列化。
清单 11.16 显示了按钮点击事件处理程序的纯 JavaScript 版本,该处理程序生成 JSON,将其发布到服务器,并处理结果。有一些要点需要注意。这次必须在标头中传递请求验证令牌。您不能将其包含在请求正文的 JSON 中,因为请求验证是在框架解析 JSON 之前进行的。此外,您必须将请求的内容类型指定为 application/json。最后,当您使用 Fetch API 时,您对响应使用 json() 方法(而不是您之前使用的 text() 方法)来反序列化响应,以便您可以使用它。默认的 JSON 序列化程序使用驼峰式大小写生成属性名称,因此您传递给 JsonResult 构造函数的匿名对象的 TotalCost 属性将变为 totalCost。
清单 11.16 使用 Fetch API 发布 JSON 的按钮点击处理程序
document.querySelector('.btn-success')
.addEventListener('click', () => {
const token = document.querySelector(
➥ '[name="__RequestVerificationToken"]').value;
const data = {
startdate: document.getElementById('StartDate').value,
enddate: document.getElementById('EndDate').value,
numberofguests: document.getElementById(
➥ 'NumberOfGuests').value,
property: {
dayrate: document.getElementById(
➥ 'Property_DayRate').value
}
};
fetch('?handler=booking', {
method: 'post',
headers: {
"Content-type": "application/json", ❶
"RequestVerificationToken" : token ❷
},
body: JSON.stringify(data) ❸
}).then((response) => {
return response.json(); ❹
}).then((result) => {
alert(`Your stay will cost ${result.totalCost}`); ❺
});
});
❶ 您必须将内容类型指定为 application/json。
❷ 您必须将请求验证令牌作为请求标头传递。
❸ 使用 JSON.stringify 方法将 JavaScript 对象序列化为 JSON 格式。
❹ 在响应中使用 json 方法,该方法将返回的 JSON 解析为对象。
❺ 访问结果对象的 totalCost 属性。
请注意,令牌的标头名称没有前导下划线。如果您在使用 JSON 时收到 400 错误代码,请在检查标头是否存在后检查标头的名称。如果您未将内容类型指定为 application/json,它将默认为 text/plain,并且您的处理程序将出错,因为它不会尝试解析请求正文。
jQuery 版本(清单 11.17)使用 ajax 方法而不是速记 post 方法来请求,因为 ajax 方法使您能够设置标头。请求的内容类型是使用设置中的一个 contentType 选项指定的,而不是显式设置标头值。
清单 11.17 使用 jQuery 和页面处理程序发布和处理 JSON
$('.btn-success').on('click', function(){
const token = $('[name="__RequestVerificationToken"]').val(); ❶
const data = {
startdate: $('#StartDate').val(),
enddate: $('#EndDate').val(),
numberofguests: $('#NumberOfGuests').val(),
property:{
dayrate: $('#Property_DayRate').val()
}
}
$.ajax({
url: '?handler=booking',
method: "post",
contentType: "application/json", ❷
headers: {
"RequestVerificationToken" : token ❸
},
data: JSON.stringify(data) ❹
})
.done(function(response){
alert(`Your stay will cost ${response.totalCost}`);
});
});
❶ 获取 token 值。
❷ 通过 contentType 选项设置内容类型。
❸ 在 header 中传递令牌。
❹ 使用 JSON.stringify 方法将 JavaScript 对象序列化为 JSON 格式。
使用页面处理程序和 JSON 的要点如下:
• 请记住将 FromBody 属性应用于处理程序参数。
• 将请求的内容类型设置为 application/JSON。
• 在请求标头中传递请求验证令牌。
• 使用浏览器开发人员工具中的 Network (网络) 选项卡来诊断问题。
在我看来,使用页面处理程序方法处理和返回 JSON 是可以接受的。请记住,每次执行页面处理程序时,都会实例化 PageModel 的一个实例,并解析其所有依赖项。如果您发现您正在注入仅 JSON 处理页面处理程序所需的其他依赖项,则表明有一点代码异味。此时,或者如果你的需求更广泛,你应该考虑使用最少的请求处理 API,这是 .NET 6 中引入的一项新功能。
11.3 最小请求处理 API
在 .NET 6 之前,在 ASP.NET Core 中通过 HTTP 处理大量服务(例如使用和生成 JSON 的服务)的推荐方法是使用构成 MVC 框架一部分的 Web API 框架。为此,您需要创建从 ApiController 派生的类,向它们添加处理请求所需的作方法,将相关服务添加到您的应用程序,并将控制器作方法映射为终端节点。在您的应用程序中合并 Web API 控制器涉及一定程度的仪式。
如果您还记得第 1 章,引入 Razor Pages 的很大一部分动机是减少现有 MVC 框架在服务器上生成 HTML 所需的仪式。减少仪式的努力在 .NET 6 中继续进行。在第 2 章中,您已经看到了应用程序引导和配置是如何基于新的最小托管 API 简化为一个文件的。作为整体最小 API 功能的一部分,还引入了一种精简的处理请求的方法,允许您将路由映射到函数,并且它确实有效。
使用 WebApplication 上的 Map[HttpMethod] 方法(MapPost、MapGet、MapPut 等)注册最小 API 请求处理程序,其约定与在 PageModel 类中注册页面处理程序的约定相同。回想一下,WebApplication 类型的实例是从生成器返回的。Program.cs 中的 Build 方法调用。传入路由模板和路由处理程序,即在路由匹配时执行的标准 .NET 委托。这可以是命名函数或可以接受参数的 lambda 表达式(图 11.5)。
图 11.5 最小 API 请求处理程序剖析。这是您将构建的实际请求处理程序的非作性、精简版本,纯粹是为了说明移动部件而设计的。
路由处理程序可以配置为返回许多内置响应类型之一,包括 JSON、文本和文件。内置返回类型中明显遗漏的是 HTML。这就是 Razor Pages 的用途。
11.3.1 最小 API 示例
在下一个练习中,您会将当前预订页面处理程序迁移到最小 API 方法。您将在 /api/property/booking 中定义一个响应 POST 请求的终端节点。它将 BookingInputModel 作为参数并返回 JSON 响应。打开 Program.cs 文件,就在 app 之前。Run()中,添加以下清单中的代码行。
清单 11.18 将页面处理程序迁移到最小的 API
app.MapPost("/api/property/booking", (BookingInputModel model) => ❶
{
var numberOfDays = (int)(model.EndDate.Value -
➥ model.StartDate.Value).TotalDays;
var totalCost = numberOfDays * model.Property.DayRate *
➥ model.NumberOfGuests;
return Results.Ok(new { TotalCost = totalCost }); ❷
});
❶ 使用 MapPost 方法将 POST 请求映射到指定的路由,并接受 BookingInputModel 作为参数。
❷ 使用 Results.Ok 方法将数据序列化为 JSON 并返回。
接下来,您将修改客户端脚本以调用此新终端节点。默认情况下,浏览器不允许脚本向网页中的其他域发出 HTTP 请求。此安全功能旨在减少跨站点请求伪造,因此不需要请求验证,因此不会为 API 启用。清单 11.19 显示了相应地修改的客户端代码。它只包括 Fetch request 部分,其余部分与前面的示例相同。修改请求的 URL 以反映传递给 MapPost 方法的模板,并删除请求验证标头。
清单 11.19 对最小 API 的 Fetch 请求
fetch('/api/property/booking', { ❶
method: 'post',
headers: {
"Content-type": "application/json", ❷
},
body: JSON.stringify(data)
❶ 将 URL 更改为指向 API 注册中指定的 URL。
❷ 无需包含请求验证令牌
路由处理程序本身的代码与您之前使用的 OnPostBooking 页面处理程序没有太大区别。但是,路由处理程序本身的性能更好,尤其是因为不需要实例化 PageModel。另请注意,您不必告诉请求处理程序在何处查找 BookingInputModel 参数的数据。我们将在下一节中更详细地介绍这些参数。
您传递给请求处理程序的路由模板类似于第 4 章中讨论的路由模板。您可以以相同的方式指定路由参数和使用约束。生成的模板将与您的页面一起添加到终端节点集合中。
11.3.2 最小 API 中的参数
最少的 API 参数来自多个来源。他们是 (按顺序)
• Route values
• Query string
• Headers
• Body
• Services
• Custom
您可以使用其中一个 From* 属性让框架明确知道要以哪个源为目标:
• FromRoute
• FromQuery
• FromHeader
• FromBody
• FromServices
当从正文绑定参数时,最小的 API 期望参数格式为 JSON,并尝试对其进行反序列化。.NET 6 不支持从表单进行绑定。如果您使用的是 .NET 的更高版本,则应查看文档以查看此情况是否已更改,尽管正如您所看到的,在将表单传递给 API 之前,在客户端上将表单序列化为 JSON 非常简单。
在以下示例中,您将把预订计算迁移到您将向依赖关系注入系统注册的服务,然后作为参数添加到请求处理程序中。这是分离应用程序 logic 的推荐方法,因为它可以更轻松地进行调试和测试。首先,向 Models 文件夹添加一个新类来表示预订。该类名为 Booking,其属性表示计算预订所需的数据。
清单 11.20 Booking 类
namespace CityBreaks.Models
{
public class Booking
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int NumberOfGuests { get; set; }
public decimal DayRate { get; set; }
}
}
将以下界面添加到 Services 文件夹。它指定一个返回 decimal 并采用 Booking 类实例的方法。
Listing 11.21 IBooking 接口
using CityBreaks.Models;
namespace CityBreaks.Services
{
public interface IBookingService
{
decimal Calculate(Booking booking);
}
}
将 IBooking 接口的以下实现添加到 Services 文件夹中。Calculate 方法的代码与当前请求处理程序中的代码基本相同。
Listing 11.22 BookingService类
using CityBreaks.Models;
namespace CityBreaks.Services
{
public class BookingService : IBookingService
{
public decimal Calculate(Booking booking)
{
var numberOfDays = (int)(booking.EndDate -
➥ booking.StartDate).TotalDays;
var totalCost = numberOfDays * booking.DayRate *
➥ booking.NumberOfGuests;
return totalCost;
}
}
}
现在,您在 Program.cs 中注册服务:
builder.Services.AddSingleton<IBookingService, BookingService>();
最后,您修改请求处理程序以将服务作为参数,并修改路由处理程序以利用它来执行计算。
Listing 11.23 将BookingService作为参数的请求处理程序
app.MapPost("/api/property/booking",
(BookingInputModel model, IBookingService bookingService) => ❶
{
var booking = new Booking { ❷
StartDate = model.StartDate.Value, ❷
EndDate = model.EndDate.Value, ❷
NumberOfGuests = model.NumberOfGuests, ❷
DayRate = model.Property.DayRate ❷
}; ❷
var totalCost = bookingService.Calculate(booking); ❸
return Results.Ok(new { TotalCost = totalCost });
});
❶ 处理程序将 IBookingService 作为参数。
❷ 从输入模型实例化 Booking 类的实例。
❸ 它作为参数传递给服务,返回的值被发送回客户端。
在此示例中,将推断绑定源。框架将从所有来源中搜索它们,直到找到匹配的来源。要使它们明确,从而缩小搜索范围,请相应地添加 From* 属性:
app.MapPost("/api/property/booking",
([FromBody]BookingInputModel model,
[FromServices]IBookingService bookingService)
11.3.3 最小 API 返回类型
在到目前为止的示例中,您已使用 Results.Ok 方法返回序列化为 JSON 且状态代码为 200 的数据。这是您可以与最小 API 请求处理程序一起使用的几种返回类型之一。在 .NET 6 中引入的静态 Results 类包括用于不同返回类型的其他方法,这些方法都实现 IResult。表 11.1 中详细介绍了 Razor Pages 应用程序中最常使用的 Razor Pages。
表 11.1 Results 类中的常用方法
除了这些选项之外,您还可以返回纯字符串或更复杂的类型。例如,您的处理程序可以从服务获取总成本,并简单地返回:
return bookingService.Calculate(booking);
响应将仅包含值,内容类型设置为 text/plain。请注意,您需要在客户端上调整 Fetch 代码以使用 text() 方法而不是 json() 方法,并且您将向警报中的插值字符串提供整个响应:
}).then((response) => {
return response.text();
}).then((result) => {
alert(`Your stay will cost ${result}`);
});
如果返回复杂类型,则值将序列化为 JSON,并且内容类型将设置为 application/json:
var totalCost = bookingService.Calculate(booking);
return new { TotalCost = totalCost };
这些选项在某些情况下可能很方便,但与使用 IResult 选项相比,它们只能节省几次击键,这些选项是强类型并且适合进行测试。
路由处理程序授权
路由处理程序可以与您的页面一起参与授权。您可以在任何参数之前传入 Authorize 属性。例如
app.MapPost("/api/property/booking", [Authorize](BookingInputModel model) =>
或者,您可以将 RequireAuthorization 方法链接到处理程序:
app.MapPost("/api/property/booking", (BookingInputModel model) => {
...
}).RequireAuthorization();
RequireAuthorization 方法将 params string[] 作为参数,使您能够传入应应用于路由处理程序的任何授权策略的名称。
11.4 Razor Pages 中的 CSS 隔离
在第 2 章中,我简要提到了 Shared 文件夹中的 Layout.cshtml.css 文件,其中包含应用于 _Layout.cshtml 文件的 CSS 样式声明。我提到过,样式表的稍微奇怪的名称是 CSS 隔离使用的约定的一部分,该约定已引入 .Net 6 中的 Razor Pages。本节讨论什么是 CSS 隔离及其工作原理。
首先,看看 CSS 隔离旨在缓解的问题类型。在构建 Web 应用程序时,通常会将 CSS 样式声明放在主布局文件中引用的全局样式表中。这样,样式表中的声明对所有使用该布局的页面都可用,无论它们在特定页面中是否需要。随着您继续开发应用程序,将添加与特定页面甚至部分相关的新样式。例如,您可能希望更改单个页面的默认字体,以便向样式表中添加新的 CSS 选择器,该选择器可用于仅定位该页面上的元素,并相应地更新目标元素的类属性。你的全局样式表不断增长。您发现自己越来越依赖编辑器的搜索功能来查找您可能想要更新的特定样式的声明。随着时间的推移,您会忘记哪些样式声明实际上正在使用,哪些可以安全地删除。
例如,假设您希望将一些 CSS 应用于 City 页面上的 h5 标题。它们当前由事件处理程序定位,该事件处理程序侦听 click 事件并使用属性详细信息填充模式对话框。通常,当用户将鼠标悬停在网页上的可点击元素上时,他们希望光标从箭头变为指向手指。因此,将 cursor:pointer 规则应用于页面上的所有 h5 元素是有意义的。您不希望将此更改应用于应用程序中的所有其他 h5 元素,因此您需要缩小 CSS 规则的适用范围。你可以向 h5 元素添加一个 CSS 类,然后使用它有选择地定位标题:
h5.clickable{
cursor: pointer;
}
将此添加到全局样式表中,您就可以开始工作了。显然,您必须记住 clickable 类的用途,并且必须记住将其应用于页面上的所有 h5 元素。可能是你想修改不同页面上的样式。您可以通过添加更多 CSS 类来充当过滤器来实现此目的。或者,您可以使用 Razor 中的部分来包含特定于页面的样式表:
@section styles{
<link href="/css/city.css" rel="stylesheet">
}
这种方法的缺点是会增加站点的 HTTP 请求数,尤其是在为多个页面添加特定于页面的样式表时。你不能真的使用 bundle 来组合所有这些特定于页面的样式表,因为这会破坏练习的目标。
Razor Pages 中的 CSS 隔离有助于创建特定于页面的样式表,这些样式表不依赖于部分,而是捆绑到一个文件中。该功能在 Razor Pages 中默认启用,因此无需添加其他包或配置任何服务或中间件。你所要做的就是在它要影响的页面旁边放置一个样式表。您只需遵循特定的命名约定:末尾带有 .css 的 Razor 页面文件名。
以上面的示例为例,将名为 City.cshtml.css 的文件添加到 Pages 文件夹,并在其中放置样式声明以影响可点击的 h5 元素:
h5{
cursor: pointer;
}
对样式表的引用位于布局页面上。您在引用中使用的文件名采用以下格式:[name_of_application].styles.css。您的应用程序的名称是 CityBreaks,您应该已经在布局页面上找到了该引用。它作为项目模板的一部分被放置在那里:
<link href="~/CityBreaks.styles.css" rel="stylesheet" />
当您运行应用程序并导航到 City 页面时,当您将鼠标悬停在属性名称上时,您可以看到光标变为指针(图 11.6)。如果导航到 /claims-manager,则可以看到用于显示用户名的 h5 元素不受同一 CSS 规则的影响,尽管您刚刚添加的样式表在布局文件中被全局引用。
图 11.6 独立的 CSS 样式声明仅适用于此页面上的 3 级标题。
那么它是如何工作的呢?如果查看 City 页面的呈现源代码,可以看到一个附加属性 (b-jbmzjjkv6t) 已注入到 City.cshtml 模板中的每个 HTML 元素中(图 11.7)。
图 11.7 将附加属性 (b-jbmzjjkv6t) 注入到“城市”页面中的每个 HTML 元素中。
此属性(范围标识符)唯一标识 City.cshtml 中的元素。请注意,带有容器 CSS 类的 main 和 div 元素注入了不同的范围标识符。它们属于 _Layout.cshtml 文件。每个具有附带独立样式表的 Razor 文件都会获得其标识符。
在构建时,所有隔离的样式表都合并为一个,并且它们的样式声明被更改,因此它们仅适用于具有与它们所定位的页面关联的标识符的元素。下面是一个代码段,它显示了为我的示例生成的样式表的前几行,其中包括 City.cshtml.css 文件内容以及 _Layout.cshtml.css 内容。
Listing 11.24 在构建时将所有隔离的样式表合并为一个
/* _content/CityBreaks/Pages/City.cshtml.rz.scp.css */ ❶
h5[b-jbmzjjkv6t]{ ❷
cursor:pointer;
}
/* _content/CityBreaks/Pages/Shared/_Layout.cshtml.rz.scp.css */ ❶
a.navbar-brand[b-wjjjhz4rtp] { ❷
white-space: normal;
text-align: center;
word-break: break-all;
}
❶ 注入一条评论,显示以下样式适用于哪个页面。
❷ 样式与注入了 specific 属性的元素隔离。
范围标识符由框架生成,格式为 b-{10 character string},默认情况下,每个文件都会获得其唯一的字符串,从而保证样式的隔离。但是,您可能希望在少量页面之间共享样式。您可以通过自定义生成的标识符来实现此目的,以便多个页面获得相同的值。这是在项目文件中完成的。以下示例导致 Layout 和 City 页面共享相同的标识符:shared-style。
清单 11.25 自定义所选页面的隔离标识符
<ItemGroup>
<None Update="Pages/Shared/_Layout.cshtml.css" CssScope="shared-style" />
<None Update="Pages/City.cshtml.css" CssScope="shared-style" />
</ItemGroup>
鉴于 CSS 隔离是一项构建时功能,因此标记帮助程序不支持它,因为它们在运行时生成输出。
本章总结了我们对 Razor Pages 中的应用程序开发的了解。最后几章更侧重于配置和发布您的应用程序,以及确保它免受恶意活动的影响。我们将在下一章开始介绍减少应用程序中错误的方法,如何在错误发生时妥善处理它们,以及如何使用日志记录来了解应用程序上线后发生的情况。
总结
客户端技术可以帮助您提高应用程序的可用性。
虽然它仍然是一个很棒的库,但随着越来越多的浏览器与标准保持一致,使用 jQuery 来作 DOM 和发出异步 (AJAX) 请求的情况正在减少。
您可以使用 PartialResult 将 HTML 块返回到 AJAX 调用。
在通过 AJAX 将表单值发布到页面处理程序方法时,必须确保包含请求验证令牌。
当请求内容类型为 x-www-form-urlenencoded 时,请求验证令牌可以包含在标头或请求正文中。
在请求正文中发布 JSON 时,必须将令牌作为标头包含在内。
将 JSON 发布到页面处理程序时,必须使用 FromBody 属性标记处理程序参数,以告知框架要使用哪个绑定源。
请求处理程序是 .NET 6 中的一项新功能,是最小 API 的一部分。
请求处理程序将路由映射到函数,并且可以采用参数。
请求处理程序参数绑定源可以是隐式的,也可以使用 From 属性显式地表示源,其中通配符 表示源 — Body、Services、Route 等。
请求处理程序返回序列化为 JSON 的 IResult、字符串或其他类型。
请求处理程序可以通过 Authorize 属性或 RequireAuthorization 方法参与授权。
Razor Pages 的 CSS 隔离是 .NET 6 中的新增功能。
CSS 隔离使您能够在单独的文件中维护范围限定为页面的样式,以便于维护,而不会增加整个应用程序中的 HTTP 请求数。
CSS 隔离在构建时将属性注入 HTML 元素,并使用它们将 CSS 声明的范围限定为页面。
只生成一个样式表,其中包括所有范围的样式,从而减少了 HTTP 请求。
范围样式仅影响 HTML 元素,而不影响标记帮助程序。