Razor引擎的详细指南

387 阅读8分钟

许多.NET开发者都熟悉Razor语法。它存在于ASP.NET Core视图.cshtml)、Razor页面Razor组件文件.razor)、BlazorRazor类库等。一般来说,Razor提供了评估.NET代码的功能,如HTML内的C#。尽管只有少量的语法原语,让人感觉很简单,但要使一切正常工作,背后有很多事情要做。在这篇文章中,你将探索Razor引擎的基本原理。然后你将学习它如何处理Razor语法以产生.NET程序集,并运行这些程序集来创建内容。然后在后续的文章中,一旦你对Razor引擎有了很好的掌握,你将会看到一些开源工具,这些工具以各种方式利用Razor引擎,而不仅仅是在.NET Core中,以及你如何利用这些库来创建Razor模板来发送电子邮件或用于其他目的。当你读完这两部分时,你应该对Razor的架构和使用有一个很好的把握,以满足你在ASP.NET之外的用途。

什么是Razor

在了解Razor引擎的任何技术细节之前,先退一步考虑一般的模板语言。考虑一下模板这个词,以及它是如何描述要填写的东西的,就像一个模板。换句话说,模板语言允许你将原始输出与程序化语句混合起来,以产生一个结果。

在幕后,大多数模板语言通过将模板文件 "翻转 "为一个程序来完成这一工作,在这个程序中,模板的程序化部分被执行,而原始内容部分则通过代码作为字符串输出。有许多模板语言,包括HandlebarsMustacheLiquid。大多数模板语言都是为特定的主机语言专门设计的,Razor也不例外。Razor是专门为C#和其他.NET代码设计的。

例如,考虑下面这个简短的Razor模板:

<div>Hi Dave,</div>
<div>Today is @DateTime.Now.ToString("m").</div>
<div>See you later!</div>

这个模板的输出看起来会是这样的:

<div>Hi Dave,</div>
<div>Today is March 15.</div>
<div>See you later!</div>

注意@DateTime.Now.ToString("m") ,这是一个需要被评估的程序性语句。Razor为这个模板生成的代码的简化版本可能看起来像:

await writer.WriteLineAsync("<div>Hi Dave,</div>");
await writer.WriteAsync("<div>Today is ");
await writer.WriteAsync(DateTime.Now.ToString("m"));
await writer.WriteLineAsync(".</div>");
await writer.WriteLineAsync("<div>See you later!</div>");

值得注意的是,模板语言和标记语言之间是有区别的。像Razor这样的模板语言将程序控制和评估与原始输出混合在一起,而没有模板功能的更一般的标记语言通常只用特殊的语法来描述输出。这种标记语言的一个好例子是Markdown标记语言。你在Markdown中写的内容会根据所使用的Markdown语法被转换成不同的东西(即_emphasis_ 被呈现为<em>emphasis</em> ),但没有关于变量评估或基于程序化结构控制输出的规定。在模板语言中,你通常写的是混合着代码的原始输出,而在标记语言中,你通常写的是打算转化为最终内容的伪输出。

Razor作为一个引擎

由于Razor是一个低级别的黑盒子,你不需要看它的内部,也不需要了解它,所以很容易过度简化它的工作原理。然而,由于Razor的使用环境各不相同,其底层技术需要既灵活又可扩展。大多数时候,当你谈论Razor时,你是在谈论Razor引擎的ASP.NET Core实现。这就是提供部分视图布局标签助手等机制的原因。然而,在所有这些之下是一个更通用的Razor引擎,它了解Razor语法以及如何处理它,但在ASP.NET Core网站或页面的背景下,它对Razor一无所知。你从ASP.NET Core中了解并喜爱的Razor功能实际上是建立在这个通用引擎之上的。

Razor的每一种 "风味",如ASP.NET Core MVC、Blazor等,都以略微不同的方式和略微不同的惯例和能力来实现这个通用的Razor引擎。也可以利用相同的通用引擎来构建Razor的自定义实现,有几个第三方库可以做到这一点,与ASP.NET Core的Razor实现有不同程度的兼容性(在下一篇文章中会有更多的介绍)。与此同时,底层的Razor引擎正逐渐变得更加复杂,以支持越来越多的环境和功能,如Blazor,并在MSBuild任务中使用

区分Razor引擎和ASP.NET Core的实现很重要,因为并不是所有能够使用通用Razor引擎处理Razor语法的工具都能支持ASP.NET Core中实现的所有功能。如果你认为Razor是一个单一的单体库,这可能会让你感到困惑,甚至是崩溃。为了更好地理解这一点,你需要深入了解通用的Razor引擎,看看它处理文件所经历的四个主要阶段中的每一个。

Razor处理阶段

当你编译并执行一个Razor模板时,引擎大致分四个阶段执行工作:解析代码生成编译执行。请注意,这是简化的,因为这些步骤中有许多是递归的,或者可以以不同的顺序发生,但它在高水平上一般是正确的:

Razor Processing Phases Diagram

在这个过程的各个阶段,Razor引擎还可以执行额外的工作,如缓存、观察文件变化、重新处理等。通过挂钩这些阶段中的一个或多个阶段,一个库可以在ASP.NET核心实现的范围之外提供Razor功能,甚至可以在自己的功能之外,将ASP.NET核心的一部分分层。

解析

在第一阶段,模板(即你的文档)被解析,任何表示C#代码或控制结构的Razor语法都与原始输出内容分离。在这个阶段,实际上有两个解析器参与。在第一遍中,Razor解析器寻找表示从代码到内容或反过来的语法,如@ 或HTML元素(当在代码中时)。然后,一旦代码被识别并从内容中分离出来,Roslyn(C#编译器)解析器就会检查代码部分,并从模板中构建一个语法树,包括代码和内容的节点。

如果你不熟悉语法树(有时称为 "抽象语法树 "或 "AST"),它们以树的形式提供了一个文档结构的表示。语法树中的每个节点都代表文档中的一个逻辑部分的语法,而语法树的层次结构反映了该语法是如何嵌套的。一般来说,语法树省略了非相关的文本,如空白处(通常称为 "琐碎的文本 "或 "琐事")。在这种情况下,是否包含空白和其他琐事取决于语法树中的节点是否代表C#代码或原始输出内容。

Syntax Tree From Razor Parsing

代码生成

使用语法树,模板会被转换为可以执行的C#代码。代码生成分一系列阶段进行,每个阶段都可以操作上一阶段生成的代码。这使得各种Razor功能甚至整个Razor引擎的实现都可以逐步增强之前的代码生成。这一部分是在一个中间的树状结构中进行的,直到所有的阶段都完成,实际的最终C#代码可以被生成。

代码生成阶段的结果通常是为每个Razor文档提供一个类,当你想执行(即渲染)那个特定的Razor文档时,可以调用特定的渲染方法作为入口。原始模板中的原始输出被转换为语句,将原始输出写入传递到渲染方法的流中。在大多数Razor实现中,这个渲染方法被命名为RenderAsync ,而渲染方法所在的类通常来自于一个基础视图类,该类将模型类型作为一个通用类型参数来表示。这就是为什么在你的Razor模板内使用属性Model 是强类型的。

编译

在Razor模板被处理并生成代码后,生成的代码会被Roslyn(C#编译器)编译成一个汇编。根据环境和其他条件,如缓存设置,这个程序集可能只存在于内存中,也可能被缓存到磁盘中,以便以后更快地访问。

执行

当需要将Razor文档渲染成最终内容时,编译过的程序集中的渲染方法将由主机(即ASP.NET Core运行时)执行。这通常需要某种项目系统来定位像部分视图、布局等文件,并递归处理和渲染它们。来自Razor引擎之外的其他运行时信息,如视图上下文,也会在这个阶段被传入渲染方法。编译与执行的分离使得Razor模板可以被处理一次,但随后会用不同的变量和外部环境进行多次渲染。

接下来的步骤

通过窥探Razor的幕后,我希望你已经对Razor引擎的灵活性有了一定的了解,以及当你渲染网页时真正发生了什么。在下一篇文章中,你将看到一些可用的开源实现,它们以各种方式与Razor引擎挂钩,并使Razor在ASP.NET项目之外的使用更加容易。更具体地说,你将考虑如何利用Razor模板与开源的Razor引擎相结合,轻松创建强大的动态电子邮件模板,只需几行代码就可以用SendGrid发送电子邮件。