2017 年 11 月

第 33 卷,第 11 期

领先技术 - ASP.NET MVC Core 视图指南

作者 Dino Esposito | 2017 年 11 月

Dino Esposito尽管 ASP.NET Core 表面上看起来与传统 ASP.NET MVC 非常相似,但是它们之间有很多不同之处。控制器、Razor 视图甚至模型类通常都能轻而易举地进行迁移,但从架构上讲,ASP.NET Core 与之前非 Core 版本的 ASP.NET 有很大的不同。

主要原因在于重写的管道,它为 ASP.NET Core 应用程序提供了至少有两种生成基于 HTML 响应的方法。正如所预期的,应用程序可以采用 MVC 编程模型,并从通过控制器操作调用的 Razor 视图中生成 HTML。或者,应用程序可以作为围绕某些终止中间件构建的极薄 Web 服务器,而终止中间件中的代码可以完成所有工作,包括返回被浏览器作为 HTML 进行处理的字符串。最后,在 ASP.NET Core 2.0 中,你可以使用全新的方法 - Razor 页面。Razor 页面是不需要 MVC 基础结构的 Razor 视图,但可以直接从 Razor 文件生成并提供 HTML。

在本文中,在对迷你服务器、终止中间件和 Razor 页面的简短讨论之后,我将对照和比较 MVC 编程模型中构建视图的方法。特别需要指出的是,我将重点介绍 ASP.NET Core 中的新功能,包括标记帮助器、视图组件和依存关系注入 (DI),以及它们对实际编码实践的影响。

来自终止中间件的 HTML

终止中间件是 ASP.NET Core 管道中的最后一大块代码,可用于处理请求。基本上,它是一个直接 (lambda) 函数,可处理 HTTP 请求以生成任意类型的可检测响应,可以是纯文本、JSON、XML、二进制或 HTML。以下是一个可达到此目的的示例启动类:

public class Startup
{
  public void Configure(IApplicationBuilder app)
  {
    app.Run(async context =>
    {
    var html = BuildHtmlFromRequest(context);
    await context.Response.WriteAsync(html);
    });
  }
}

通过在响应的输出流中编写 HTML 格式的文本,并设置适当的 MIME 类型,你可以将 HTML 内容提供给浏览器。这一切都是以一种非常直接的方式进行的,不需要筛选器,也没有中介,但是确实可行,并且比任何 ASP.NET 的早期版本都更快。终止中间件能够比任何其他选项更快地控制流。显然,直接在终止中间件中编写 HTML 工厂代码与可维护的灵活解决方案尚相距甚远,但这种设想是可行的。

Razor 页面

在 ASP.NET Core 2.0 中,Razor 页面给出了一种提供 HTML 内容的附加方式,可直接调用 Razor 模板文件,而无需通过控制器和操作。只要 Razor 页面文件位于 Pages 文件夹中,并且其相对路径和名称与所请求的 URL 相匹配,视图引擎就会处理内容并生成 HTML。

Razor 页面和 Razor 视图的真正区别在于,Razor 页面可以是一个单独的文件(与 ASPX 页面非常相似),包含代码和标记。Razor 页面是一个用 @page 指令标记的 CSHTML 文件,可以绑定到从系统提供的 PageModel 类继承的视图模型。正如我已经提到的,所有 Razor 页面都位于新的 Pages 根项目文件夹下,并且路由遵循简单的模式。该 URL 起源于 Pages 文件夹,并且已将 .cshtml 扩展名剥离出实际的文件名。有关 Razor 页面的详细信息,请查看 bit.ly/2wpOdUE。此外,对于更高级的示例,请查看 msdn.com/magazine/mt842512

如果你习惯于使用 MVC 控制器,预计你会发现 Razor 页面从根本上说是毫无意义的,或许只有在那些极其罕见的情况下可提供微小的帮助,这些情况是,你具有可在没有任何业务逻辑的情况下呈现视图的控制器方法。另一方面,如果你不熟悉 MVC 应用程序模型,则 Razor 页面提供了另一种掌握 ASP.NET Core 框架的选项。对一些人来说,Razor 页面针对框架使用提供了一个较低的准入门槛。

标记帮助器

本质上,Razor 语法始终是散布 C# 代码片段的 HTML 模板。@ 符号用于告诉 Razor 分析程序 HTML 静态内容和代码片段之间出现转换的位置。会根据 C# 语言的语法规则分析 @ 符号后面的任何文本。连接在文本分析期间发现的项目,从而形成一个动态构建的 C# 类,此类使用 .NET 编译器平台(“Roslyn”)动态编译。执行 C# 类只是在响应流中累积 HTML 文本,将静态内容和动态计算的内容混合在一起。最终,Razor 语言的表达能力仅限于 HTML5 的表达能力。

使用 Razor,ASP.NET 团队还引入了一个称为 HTML 帮助器的工件。HTML 帮助器是一种小型的 HTML 工厂,它可以获得一些输入数据,并 且只发出你想要的 HTML。但是,HTML 帮助器从来没有赢过开发者,因此在 ASP.NET Core 中添加了一个更好的工具:标记帮助器。标记帮助器起着和 HTML 帮助器同样的作用(它们作为 HTML 工厂运行),但是提供更加简洁和自然的语法。特别是不需要任何 C# 代码来绑定标记帮助器与 Razor 模板代码。相反,它们看起来像扩展的 HTML 语法的元素。以下是标记帮助器示例:

<environment names="Development">
  <script src="~/content/scripts/yourapp.dev.js" />
</environment>
<environment names="Staging, Production">
  <script src="~/content/scripts/yourapp.min.js" 
    asp-append-version="true" />
</environment>

环境标记元素不是逐字发送给浏览器的。相反,它的整个子树是通过标记帮助器组件在服务器端进行分析的,该组件能够读取属性,并检查和修改当前的 HTML 树。在此示例中,负责环境元素的标记帮助器将当前的 ASP.NET Core 环境与 names 属性的内容进行匹配,并仅发出应用的脚本元素。另外,脚本元素由其他标记帮助器处理,该帮助器会检查寻找 asp-append-version 自定义属性的树。如果可以找到,则生成的实际 URL 将追加一个时间戳,以确保链接的资源永远不会被缓存。

标记帮助器是通常从基类继承的 C# 类,或者以声明的方式绑定到标记元素和属性。每个打算使用标记帮助器的 Razor 文件必须使用 @addTagHelper 指令来进行声明。该指令只是从给定的 .NET Core 程序集中注册标记帮助器类。@addTagHelper 指令可能出现在单独的 Razor 文件中,但更常见的是存在于 _ViewImports.cshtml 文件中,并全局应用于所有视图:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

识别为标记帮助器的属性和元素在 Visual Studio 中也用一种特殊颜色进行强调。ASP.NET Core 附带了一整套可以分为几类的预定义标记帮助器。有些会影响 Razor 模板中的特定 HTML 元素,例如 form、input、textarea、label 和 select。而存在的某些帮助器可简化和自动化窗体中验证消息的显示。所有预定义的标记帮助器都共享 asp-* 名称前缀。有关详细信息,请访问 bit.ly/2w3BSS2

标记帮助器有助于使 Razor 源代码保持可读性和简洁性。因此,不存在避免使用帮助器的理由。总的来说,我会建议使用标记帮助器来自动化编写长时间重复性的标记代码块,而不是创建类似特定视图语言的任何内容。实际上,使用标记帮助器的次数越多,距离使用纯 HTML 就越遥远,呈现 HTML 视图的成本也就越高。标记帮助器是一项很酷的技术,但它们代表了服务器端的表达能力和呈现成本之间的的权衡。说到好处,还值得一提的是,标记帮助器不会改变整体标记语法,因此你可以在任何 HTML 编辑器中轻松打开带有标记帮助器的 Razor 文件,而不会产生分析问题。HTML 帮助器却并非如此。

视图组件

从技术上讲,视图组件是包含逻辑和视图的自包含组件。在 ASP.NET Core 中,它们替换了传统 ASP.NET MVC 5 中提供的子操作。你可以通过 C# 块在 Razor 文件中引用视图组件,并向它们传递所需的任何输入数据:

@await Component.InvokeAsync("LatestNews", new { count = 4 })

从内部来说,视图组件将运行自己的逻辑,处理传入的数据并返回一个准备呈现的视图。ASP.NET Core 中不存在预定义的视图组件,这意味着它们会在严格的应用程序基础上创建。上面的代码行提供了一个 LatestNews 视图组件,它是一个打包的标记块,概念上类似于部分视图。部分视图和视图组件的区别在于内部实现。部分视图是一个普通的 Razor 模板,可以选择性地接收输入数据,并将其合并到它所制作的 HTML 模板中。除了格式化和呈现之外,部分视图不会有任何其自己的行为。

视图组件是部分视图的更复杂形式。视图组件可选择性地接收通常用于检索和处理其数据的普通输入参数。一旦可用,数据将合并到嵌入式 Razor 模板中,这与部分视图的行为非常类似。但是,视图组件在实现方面速度更快,因为它不需要通过子操作需要通过的控制器管道。这意味着不需要进行模型绑定,也不需要操作筛选。

总的来说,视图组件服务于可帮助组件化视图的崇高目标,因而它是独特且自制的组件组合的结果。这一特性是一把双刃剑。将视图拆分为独特且独立的组件似乎是一种便捷的组织工作方式,并且可能通过让不同的开发者关注各个部分来使其发展更加平衡。但是,视图组件在任何情况下都无助于加快速度。这完全取决于每个组件在内部所执行的操作。在讨论视图组件和及其愿景下的组合 UI 模式时,经常会以大型面向服务的系统的体系结构为例向你展示,这是绝对正确的。

但是,当在一个单片紧凑系统的上下文中盲目地使用视图组件和模式时,最终可能会导致查询逻辑优化不佳。每个组件可以查询其数据,生成重复查询和不必要的数据库流量。在这种情况下可以使用视图组件,但是你应该考虑缓存图层以避免不好的结果。

向视图传递数据

有三种不同的非排他性方式将数据传递给 Razor 视图(通过 @inject 指令的 DI 除外)。你可以使用两种内置词典之一(ViewData 或 ViewBag),也可以使用强类型的视图模型类。纯粹从功能角度来看,这些方法之间没有区别,即使从性能的角度来看,这种差别也是可忽略不计的。

但从设计角度来看,我推荐单通道视图模型方法。任何 Razor 视图都应该有单个数据源,即视图模型对象。这种方法要求密切关注视图模型的基类设计,避免使用词典甚至 @inject 指令。

我知道这个方法听起来很严格,但却是经验之谈。即使在视图中调用 DateTime.Now 也是很危险的,因为它会将数据注入到对应用程序来说可能不够抽象的视图中。DateTime.Now 表示服务器的时间,而不是应用程序的时间。这对于根据当天时间运行的任何应用程序来说都是一个问题。为了绕开这些陷阱,花一些时间来设计视图模型类,以预设所有视图都需要的通用数据。此外,必须要始终通过模型将数据(任何数据)传递给视图,尽可能避免使用词典和 DI。虽然使用词典和 DI 后速度会更快,但当你之后必须在大型应用程序中重构某些域功能时,速度就不会变得更快。视图中的 DI 是一种很酷的技术,但它的推广应用仍是困难重重。

总结

视图是 Web 应用的基础。在 ASP.NET Core 中,视图是处理模板文件(通常是 Razor 模板文件)的结果,该模板文件与最常作为控制器方法的调用方提供的数据混合在一起。在本文中,我尝试比较了构建视图并将数据传递给它们的各种方法。大多数 ASP.NET Core 文献都强调标记帮助器和视图组件,甚至是 Razor 页面和 DI 在视图中的作用。虽然这些都是令人信服的资源,但做到不盲目使用非常重要,因为它们具有自然缺点,如果不小心,就会产生一些不好的结果。


Dino Esposito* 是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2014 年)和《Programming ASP.NET Core》(Microsoft Press,2018 年)的合著者。JetBrains 公司的 Pluralsight 创建者和开发者 Esposito 在 Twitter 上分享了他对软件的看法:@despos。*

衷心感谢以下 Microsoft 技术专家对本文的审阅:Steve Smith


在 MSDN 杂志论坛讨论这篇文章