这里说的Razor是一种模板语言,而不是“雷蛇”这个游戏外设厂商。
Razor模板语言是一种将HTML和C#杂糅起来的胶水模板语言,类似于古早的JSP以及Nunjucks。在先.NET Core时期,Razor主要被用在ASP .NET的服务端渲染领域,.NET Core时代,微软又推出了Blazor框架,Razor又被用在了Blazor中。而服务端webapp领域的应用也没有被舍弃,现在叫ASP .NET Core Webapp。
作为一门模板语言,理论上讲它的语法和用法是独立于框架的,不过在实际应用的时候,在服务端webapp领域和blazor领域里,Razor的语法、关键字是统一的,但细节处稍有不同。这篇文章主要是为也后续介绍Blazor框架铺路,所以在介绍到这部分差异的时候,会以Blazor那边为主。
在ASP .NET Core webapp这边,Razor模板文件以工程项目中以*.cshtml
为后缀名,写出来的“页面”我们一般称其为“Razor Page”。在Blazor那边,则是启用了一个新的后缀*.razor
,由于Blazor框架采用UI组件化的设计,所以写出来的“页面”,或者“UI片段”,在Blazor这里被称为“Razor Component”。
还是那句话:今天我们只从“模板语言的语法”这个层面上,脱离开具体框架和应用领域,来介绍Razor。
说的直白一点,今天介绍的是一种“把HTML和C#糅合在一起的胶水语言”。
而由于“标记语言”和“程序设计语言”本身风格差异极大,天然的,无论是Razor,还是JSP,还是Nunjucks之类的模板语言,都存在一个共性问题:长得太丑了,写出来的代码不熟悉的情况下,第一眼看过去会觉得非常混乱。为了更好的帮忙你理清楚“看起来混乱无比的Razor语法”,我们需要在真正介绍Razor的语法之前,先建立起一些基本的认知:
你可能在很多编程领域都听到过“模板/template”这个术语,那么这个术语到底指什么呢?想一想,程序设计语言中有“模板”这个概念,后端语言比如C++,Java,C#等语言中有“模板类”,“模板函数”。前端语言JS/TS中,在字符串常量上也有一个“模板字符串常量”的概念。然后在服务端渲染领域,任何服务端渲染框架都会使用一门“模板语言”去将标记语言HTML和程序设计语言杂糅在一起。
其实仔细想,无论是“模板类”,还是“模板字符串”,还是“模板网页”,都有一个共通的特点:90%的内容是写死的,10%的内容需要动态插入。
这么想一下,是不是非常有道理?
这里简单的说一下Razor引擎的工作模式,这个工作模式其实和古早的JSP框架里的JSP引擎是大差不差的。
简单来讲,就是把大象装冰箱的三步曲
*.cshtml
或*.razor
文件都会被转译成一个*.cshtml.cs
或*.razor.cs
文件,转译后的一个个代码文件里,写着的是一个个的“类”的定义。csc
会把转译后的*.cshtml.cs
或*.razor.cs
与其它项目代码文件一起,编译成IL,封装成assembly。在这一步,csc
的眼里,它完全不关心它所编译的C#代码文件是你手写的,还是由Razor引擎转译模板生成的。或者换个说法:它完全不知道,也不需要知道。Render()
方法会被调用,这个方法的返回值,才是真正的HTML代码。
Render()
方法是在服务端被调用的,这个方法返回的结果会被写在HTTP Response的Body中。Render()
是在客户端的浏览器上被执行的,类似于其它前端SPA框架。引擎生成的方法不一定就非得叫
Render()
,这里就只是说那么个意思
对于写代码的程序员来说,最重要的是要知道:怎么才能把C#代码嵌在HTML中。对于处理Razor的引擎来说,最重要的是要知道:怎么才能分辨、分离出代码中的HTML和C#代码。
Razor使用了一个关键字符来做区分:@
。无论是嵌入C#表达式,还是C#代码段,还是其它任何形式的C#代码,关键都在这个猴头字符上,这是一个贯穿所有Razor语法的至尊字符,请牢记:@
。而在你确实需要输入一个@
的时候,使用连续的两个@@
转义就可以了。
甚至,我在这里给你提前剧透:在C#代码中如果要插入HTML,还是这个字符,神奇吧?怎么做到的呢?来不着急,后面我会慢慢给我说,但请在这里再巩固一下这个关键知识点:
@
是至高无上的@@
是对@的转义效果:运行期会对表达式进行求值,并把表达式的值进行ToString()
字符串化,再把结果字符串进行转义,渲染在原处。
语法:用@()
把表达式括起来即可。当表达式中不包含空格、模板类型参数的时候,可以省略括号,直接把@
放在表达式前面即可。带括号的我们称其为“Explicit Razor Expression, 显式Razor表达式”,不带括号的简写我们称期为“Implicit Razor Expression,隐式Razor表达式。”
额外注意点:Razor引擎在99%的情况下,遇到单个@
,会转变为“C#模式”,但有一个例外:邮件地址。Razor引擎会自动识别邮件地址,当它觉得这个东西长得像邮件地址的时候,就不会切换到C#模式。
以下是常见错误示例:
<p>@GenericMethod<int>()</p>
: 调用模板函数的表达式需要用@()
括号括起来,因为引擎无从分辨<int>
到底是个自定义的HTML Tag,还是C#中的模板类型参数
<p>@(GenericMethod<int>())</p>
<p>Last week: @DateTime.Now - TimeSpan.FromDays(7)</p>
: 包含了空格的表达式,需要用@()
括起来
<p>Last week this time: @(DateTime.Now - TimeSpan.FromDays(7))</p>
<p>[email protected]</p>
: Razor表达式会觉得[email protected]
是个邮件地址。
<p>Age@(joe.Age)</p>
有两个知识点比较生僻,但你需要知道:
@("<span>Hello</span>")
会最终被渲染成<span>Hello</span>
。
@Html.Raw()
。比如@Html.Raw("<span>Hello</span>")
。不过不用我过多强调你也知道:这个东西比较危险,切记不要用这种方式去渲染任何来自客户端的内容,谨防注入。false
或null
,且attribute是类似于class
, checked
的attribute时,渲染结果中并不会出现checked="false"
这种东西,而是会直接把checked
移除
<div class="@false">False or Null</div>
和<div class="@null">False or Null</div>
的渲染结果都是<div>False or Null</div>
<input type="checkbox" checked="@false" />
的渲染结果是<input type="checkbox">
这里牵扯两个知识点:
@{}
括起来的东西时,会切换到C#模式,将内部括起来的东西都当成C#代码去处理说起来比较绕,我们循序浅进的来解释,
首先看一个“HTML中插入C#代码块”的例子:
@{
var quote = "The future depends on what you do today. - Mahatma Gandhi";
}
<p>@quote</p>
@{
quote = "Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.";
}
<p>@quote</p>
它的渲染结果如下:
<p>The future depends on what you do today. - Mahatma Gandhi</p>
<p>Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.</p>
它是怎么工作的呢?这时候我们就需要看一下Razor引擎转译出来的源码长什么样了。我们上面说过了,转译后的Razor文件其实是个C#类定义,下面我们只贴出这个类里的核心方法:BuildRenderTree
,大概长下面这样:
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
var quote = "The future depends on what you do today. - Mahatma Gandhi";
__builder.OpenElement(0, "p");
__builder.AddContent(1, quote);
__builder.CloseElement();
quote = "Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.";
__builder.OpenElement(2, "p");
__builder.AddContent(3, quote);
__builder.CloseElement();
}
你仔细看一下转译前的Razor代码,和转译后的C#代码,大概能得出两个结论:
@{}
代码块中的内容,是被原封不动的放在了BuildRenderTree
方法中__builder.xxx
方法来看这个方法:
void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
如果以纯C#的角度来看,上面的方法是有编译错误的。因为方法体内的语句是一行HTML标记代码,在纯C#的世界里,这玩意是过不了编译的,要过编译,它应当被写成类似于下面的东西:
void RenderName(string name)
{
- <p>Name: <strong>@name</strong></p>
+ return "<p>Name: <strong>" + name + "</strong></p>";
}
这就是Razor引擎做的事情:Razor引擎在转译C#代码时,会自动识别出HTML标记语言,并对其进行相应的处理。也就是说:Razor不光能在HTML中嵌入C#代码,还能再在C#代码中嵌入HTML。
只不过呢,嵌的时候的语法不一样,我们目前知道的语法是:
@
, @()
,会把后续内容或括号里的内容解释成一个“C#表达式”。引擎在碰到@{}
时,会认为内部是C#代码。无论是单行表达式,还是多行代码块,引擎都会切换到“C#模式”上面有关模式切换的描述,其实是错误的,只不过你可以这样去理解而已。
然后再看下面的这段Razor代码:
<h3>Saint</h3>
@{
void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
它的转译结果如下:
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "<h3>Saint</h3>");
void RenderName(string name)
{
__builder.OpenElement(1, "p");
__builder.AddContent(2, "Name: ");
__builder.OpenElement(3, "strong");
__builder.AddContent(4, name);
__builder.CloseElement();
__builder.CloseElement();
}
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
这个例子的亮点如下:
@{}
插入了代码块上面已经说了最基础的一种姿势:一个看起来像是HTML代码的整行,会被视为HTML
这里还有两种在C#中插入HTML的方式:
<text>
标签把整行内容包裹起来,Razor引擎最终会把<text>
标签本身移除。@:
声明一下,这样Razor引擎就会知道:哦,接下来的所有内容直到行尾,都是HTML。两个例子:
@{
var people = new List<string>{"Amy", "Fiona", "Ross", "Jim", "Jon"};
for (var i = 0; i < people.Count; i++)
{
var person = people[i];
<text>Name: @person</text>
}
for (var i = 0; i < people.Count; i++)
{
var person = people[i];
@:Name: @person
}
}
总结一下在C#中插入HTML的三种方式:
语法 | 效果 | 官方名称 |
---|---|---|
<p>xxx</p> |
看起来像HTML的一整行,会被当成HTML去渲染 | Implicit transitions |
<text>xxx</text> |
把一整行用<text> 包裹起来,<text> 会被引擎在渲染结果中移除 |
Explicit delimited transitions |
@:xxx |
在行首使用@: 进行声明,那么该行剩余的所有内容会被当成HTML进行处理 |
explicit line transition |
可以看到,在HTML中插入C#,可以以@{}
的方式插入多行代码块,但好像要在C#代码中插入,只能按行来。。。。。是吗??不是,你可以连续写好多行啊,那效果不跟多行一样?
试想如下的代码块:
@{
if(v % 2 == 0)
{
<p>The value was even.</p>
}
}
一个if
加一行内容,写出来占了五行,是不是有点麻烦?是的,Razor引擎也是这么想的,所以提供了如下简写方式:
@if(v % 2 == 0)
{
<p>The value was even.</p>
}
所以新的语法(糖)已经诞生,以下是正确示例:
@if (value % 2 == 0)
{
<p>The value was even</p>
}
@if (value % 2 == 0)
{
<p>The value was even.</p>
}
else if (value >= 1337)
{
<p>The value is large.</p>
}
else
{
<p>The value is odd and small.</p>
}
@switch (value)
{
case 1:
<p>The value is 1!</p>
break;
case 1337:
<p>Your number is 1337!</p>
break;
default:
<p>Your number wasn't 1 or 1337.</p>
break;
}
@for (var i = 0; i < people.Length; i++)
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
}
@foreach (var person in people)
{
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
}
@{ var i = 0; }
@while (i < people.Length)
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
i++;
}
@{ var i = 0; }
@do
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
i++;
} while
@using (Html.BeginForm())
{
<div>
Email: <input type="email" id="Email" value="">
<button>Register</button>
</div>
}
@try
{
throw new InvalidOperationException("You did something invalid.");
}
catch (Exception ex)
{
<p>The exception message: @ex.Message</p>
}
finally
{
<p>The finally statement.</p>
}
@lock (SomeLock)
{
// Do critical section work
}
总结:C#中的,附加关键字的分支跳转或循环语句块,都可以用简化的@keyword
包起来。这些特殊的关键字包括:
@if()...
和@switch(){}
@for(){}
, @foreach(){}
, @while(){}
, @do{}while()
@using{}
@try{} catch{} finally{}
@lock(){}
截止到现在为止,我们所接触的所有知识点其实都是在BuildRenderTree
方法中闪转腾挪大杀四方。这个小节则是要把战场扩大一点:我们跳出BuildRenderTree
方法,开始在“类”上搞事情。
比如我们写了如下代码:
@{
var quote = "Getting old ain't for wimps! - Anonymous";
}
<div>Quote of the Day: @quote</div>
Razor引擎将其转译后的C#代码大致如下:
public class _Views_Something_cshtml : RazorPage<dynamic>
{
public override async Task ExecuteAsync()
{
var output = "Getting old ain't for wimps! - Anonymous";
WriteLiteral("/r/n<div>Quote of the Day: ");
Write(output);
WriteLiteral("</div>");
}
}
另外需要注意的是,这一小节中的功能其实严格来算,不算是“纯语法”范畴,有些功能仅工作在Razor Page环境下,有些功能仅工作在Razor Components环境下。我都会注明的。
默认情况下,Razor引擎会把生成的类放在一个namespace下,这个namespace其实是跟模板文件的路径相关的。 想要改写这个默认行为,可以显式的在模板文件的脑门上如下写:
@namespace Your.Namespace.Here
对应的,要给生成的类文件添加对其它命名空间的引用,可以显式的在模板文件的脑门上如下写:
@using System.IO
需要注意的是:末尾不需要加分号
要为类添加属性、字段、方法、静态函数等,可以使用两个关键字:@code
或@functions
,如下:
@code {
// 在这里写你要添加的新字段、属性或方法
}
@functions {
// 在这里写你要添加的新字段、属性或方法
}
这俩关键字基本是等价的。但@code
仅在Razor Components环境下可用。不得不说,@functions
这个名字是有一定误导性的。
@implements
用来声明实现的接口,@inherits
用来声明父类,@attribute
用来声明在类上应用的Attribute,@typeparam
来给模板类声明类型参数。例子如下:
有三点需要注意:
@attribute
来声明在类上应用的注解时,注解类还是需要带中括号@typeparam
仅适用于Razor Component环境,并且也支持类型限定@implements IDisposable
<h1>Example</h1>
@functions {
private bool _isDisposed;
...
public void Dispose() => _isDisposed = true;
}
@inherits TypeNameOfClassToInheritFrom
@attribute [Authorize]
@typeparam TEntity where TEntity : IEntity
使用@inject
向类内注入一个字段,例子如下:
@page
@model PrivacyModel
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
ViewData["Title"] = "Privacy RP";
}
<h1>@ViewData["Title"]</h1>
<p>PR Privacy</p>
<h2>
MyRoot:MyParent:MyChildName: @Configuration["MyRoot:MyParent:MyChildName"]
</h2>
使用@model
来显式的指定Razor Page里Model类的类型。这个只有在Razor Page环境下可用。
@model LoginViewModel
转译出来的C#类大致如下:
public class _Views_Account_Login_cshtml : RazorPage<LoginViewModel>
@page
在Razor Component中,@page
可以直接来指定当前页面的路由路径。在Razor Page中,@page
的功能只是声明当前模板文件是一个Razor Page。
@layout
仅在Razor Component中可用,它用来指定当前页面使用的布局页面是哪个。这个等我们讲到布局的时候再深入了解。
而@section
仅在Razor Page中可用,它的功能也与布局有关,不过鉴于我们不讨论Razor Page,这里就不介绍了,知道有这么个关键字就行了。
这个功能仅Razor Component中可用,你可以把它想象成生成类里的一个flag字段(其实不是),当你设置@preservewhitespace
为true
时,渲染结果中的空白字符是不会被移除的,包括模板中书写的缩进空白字符。
这个值默认是false
,大多数情况下你都不需要去修改它。
在Razor Component环境中,引擎扩展了HTML元素的属性,你可以给HTML元素写一些奇怪的属性,这些奇怪的属性都以@
开头,以显著区分原生属性、自定义属性与魔改属性。这些奇怪的属性的功能多数都是用来支撑Blazor中的高级功能。
注意,这一小节讲的所有内容,仅适用于Razor Component。这里只是简单的过一下,后续碰到知识点的时候再详细深入
指令 | 功能 |
---|---|
@attributes |
值应当是一个字典类型实例。用于方便程序员将HTML的attribute一股脑的写在一个变量里然后一次性传过来 |
@bind 和@bind:culture |
与数据绑定相关 |
@on{EVENT} ,@on{EVENT} :preventDefault, @on{EVENT}:stopPropagation |
绑定事件处理回调函数 |
@key |
一个辅助功能,用来支持渲染树的diff算法。和React中的key 功能类似 |
@ref |
一个辅助功能,用来把渲染树中的节点指针暴露出来,使得程序员可以拿到当前渲染元素的对象指针 |
设想以下模板代码:
@{
var pets = new List<Pet>
{
new Pet { Name = "Rin Tin Tin" },
new Pet { Name = "Mr. Bigglesworth" },
new Pet { Name = "K-9" }
};
}
@foreach(var pet in pets)
{
<p>You have a pet named <strong>@pet.Name</strong>.</p>;
}
没什么大毛病。而如果你想把@foreach
内部的逻辑抽离成一部分独立的逻辑,比如包装成一个函数,你可以如下写:
@{
void RenderPet(Pet pet)
{
<p>You have a pet named <strong>@pet.Name</strong>.</p>;
}
foreach(var pet in pets)
{
RenderPet(pet);
}
}
这样做。。其实也没什么大毛病,不过Razor还提供了一种写法,如下:
@{
Func<dynamic, object> petTemplate = @<p>You have a pet named <strong>@item.Name</strong>.</p>;
foreach(var pet in pets)
{
@petTemplate(pet)
}
}
这里有几个要点:
@
后跟着一个HTML Tag的时候,会把后续的内容翻译成一个Lambda表达式。Lambda表达式有一个类型为dynamic
的入参,有一个类型为object
的返回值,实际返回类型是IHtmlContent
,入参的变量名叫item
@variable
的语法@
后面跟的既不是冒号@:
,也不是HTML标签名@<xx>
,那么这行就会被Razor引擎解释为“对某个特殊函数的调用”,而这种“特殊函数”,就是类似上面的Lambda表达式的函数1.
里的简写形式,如果函数有多个入参,那么就必须用C#,在@code{}
或@function{}
块里正式定义。但对此类函数的调用,都可以写成@xxx(xx, xx, xx)
的形式。如下:@using Microsoft.AspNetCore.Html
@functions {
public static IHtmlContent Repeat(IEnumerable<dynamic> items, int times,
Func<dynamic, IHtmlContent> template)
{
var html = new HtmlContentBuilder();
foreach (var item in items)
{
for (var i = 0; i < times; i++)
{
html.AppendHtml(template(item));
}
}
return html;
}
}
@Repeat(pets, 3, @<li>@item.Name</li>)
上面这个例子更有意思:
最后一行对Repeat
函数的调用,第三个参数,是一个匿名的Lambda表达式,符合我们上面1.
里说的语法,其实它相当于:
Func<dynamic, object> getName = (dynamic item) => item.Name;
总之,这种临时封装,就地使用的特殊函数,我们一般称之为“Templated Razor delegates”,或者叫“Razor Template”,硬要翻译的话,就叫“Razor小模板”吧
Tag Helper是一种仅适用于Razor Page的高级功能,它使用起来依然长得像HTML标签里的attribute,与Directive Attribute不同的是,它的名字前面不带@
,看起来就像是普通的自定义attribute,不过按照约定俗成,这种attribute一般都以asp-
开头,如下:
<input asp-for="LastName"
disabled="@(Model?.LicenseId == null)" />
上面代码中的asp-for
,就是一个Tag Helper。鉴于我们主要讨论Blazor中的Razor Component,这个知识点我们就不解释了。
在项目文件中添加如下声明:
<PropertyGroup>
+ <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
这样转译后的C#类定义就会被保存在编译目录中
这其实应该放在语法章节里去介绍。
在Razor模板文件中,注释有两种:
csc
的工作而被抹除,但HTML注释甚至在最终输出的时候都不会被抹除,只是浏览器不展示而已。第二种注释就按正常写就行了,唯一的知识点是第一种注释我们现在还没讲怎么写:它只有一种写法,不分单行注释和多行注释,统一一种写法,如下:
@*
...
*@