Blazor教程 第一课:模板语言Razor

Blazor

1. 什么是Razor

这里说的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的语法之前,先建立起一些基本的认知:

2. 在介绍Razor语法之前,你需要知道的几件事

2.1 模板语言的本质是什么?

你可能在很多编程领域都听到过“模板/template”这个术语,那么这个术语到底指什么呢?想一想,程序设计语言中有“模板”这个概念,后端语言比如C++,Java,C#等语言中有“模板类”,“模板函数”。前端语言JS/TS中,在字符串常量上也有一个“模板字符串常量”的概念。然后在服务端渲染领域,任何服务端渲染框架都会使用一门“模板语言”去将标记语言HTML和程序设计语言杂糅在一起。

其实仔细想,无论是“模板类”,还是“模板字符串”,还是“模板网页”,都有一个共通的特点:90%的内容是写死的,10%的内容需要动态插入。

  • 模板类/模板函数:90%死内容指的是“类型无关的程序逻辑”,10%待填充的内容是“类型信息”
  • 模板字符串:90%的死内容指的是“写死的字符串部分”,10%待填充的内容是“待求值的变量”
  • 服务端渲染领域的模板语言:90%的死内容指的是“写死的HTML”,10%待填充的内容是“杂糅在模板语言中的代码片段,这部分代码片段会在运行期动态的插入生成剩下的内容”

这么想一下,是不是非常有道理?

2.2 Razor和JSP类似:你以为你写的是HTML,其实是类

这里简单的说一下Razor引擎的工作模式,这个工作模式其实和古早的JSP框架里的JSP引擎是大差不差的。

简单来讲,就是把大象装冰箱的三步曲

  1. 你所写的模板,会被一个叫“Razor引擎”的东西,在项目编译之前,先转译成C#类定义的代码文件。每个*.cshtml*.razor文件都会被转译成一个*.cshtml.cs*.razor.cs文件,转译后的一个个代码文件里,写着的是一个个的“类”的定义。
  2. 在编译期,C#的编译器csc会把转译后的*.cshtml.cs*.razor.cs与其它项目代码文件一起,编译成IL,封装成assembly。在这一步,csc的眼里,它完全不关心它所编译的C#代码文件是你手写的,还是由Razor引擎转译模板生成的。或者换个说法:它完全不知道,也不需要知道。
  3. 在运行期渲染HTML页面时,类会被实例化,然后内部的Render()方法会被调用,这个方法的返回值,才是真正的HTML代码。
    • 对于Razor Page,Render()方法是在服务端被调用的,这个方法返回的结果会被写在HTTP Response的Body中。
    • 对于Razor Component,如果是WASM客户端项目,那么Render()是在客户端的浏览器上被执行的,类似于其它前端SPA框架。

引擎生成的方法不一定就非得叫Render(),这里就只是说那么个意思

3. HTML代码和C#代码是怎么杂糅在一起的

对于写代码的程序员来说,最重要的是要知道:怎么才能把C#代码嵌在HTML中。对于处理Razor的引擎来说,最重要的是要知道:怎么才能分辨、分离出代码中的HTML和C#代码。

Razor使用了一个关键字符来做区分:@。无论是嵌入C#表达式,还是C#代码段,还是其它任何形式的C#代码,关键都在这个猴头字符上,这是一个贯穿所有Razor语法的至尊字符,请牢记:@。而在你确实需要输入一个@的时候,使用连续的两个@@转义就可以了。

甚至,我在这里给你提前剧透:在C#代码中如果要插入HTML,还是这个字符,神奇吧?怎么做到的呢?来不着急,后面我会慢慢给我说,但请在这里再巩固一下这个关键知识点:

  1. @是至高无上的
  2. @@是对@的转义

3.1 在HTML中插入C#表达式:Implicit Razor expressions & Explicit Razor expressions

效果:运行期会对表达式进行求值,并把表达式的值进行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>

有两个知识点比较生僻,但你需要知道:

  1. 默认情况下,表达式的值会被转义,即@("<span>Hello</span>")会最终被渲染成&lt;span&gt;Hello&lt;/span&gt;
    • 如果你不想转义,就是要用C#直接写HTML内容,那么可以使用@Html.Raw()。比如@Html.Raw("<span>Hello</span>")。不过不用我过多强调你也知道:这个东西比较危险,切记不要用这种方式去渲染任何来自客户端的内容,谨防注入。
  2. 当你用表达式的值来给HTML的Attribute赋值时,如果表达式的求值结果是falsenull,且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">

3.2 在HTML中插入C#代码块,再在C#代码块中插入HTML

这里牵扯两个知识点:

  • Razor引擎在碰到使用@{}括起来的东西时,会切换到C#模式,将内部括起来的东西都当成C#代码去处理
  • 在Razor引擎处理C#代码的时候,如果碰到了一整行长得像HTML的东西,又会切换到HTML模式,把这一整行当成标记语言去处理

说起来比较绕,我们循序浅进的来解释,

初级:插入代码块是怎么回事

首先看一个“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方法中
  • Razor中的HTML代码,都被token化后,转化成了__builder.xxx方法

中级:在代码块中声明方法,并在代码块中插入HTML

来看这个方法:

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。

只不过呢,嵌的时候的语法不一样,我们目前知道的语法是:

  • 引擎在处理HTML时,如果碰到@, @(),会把后续内容或括号里的内容解释成一个“C#表达式”。引擎在碰到@{}时,会认为内部是C#代码。无论是单行表达式,还是多行代码块,引擎都会切换到“C#模式”
  • 引擎看处理C#时,即处于C#模式时,如果碰到了:
    • 一整行
    • 并且长得像标记语言 引擎就会把当前整行看作是HTML代码,临时切换到HTML模式去处理当前行,处理完后再切换回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.");
}

这个例子的亮点如下:

  1. 在HTML中用@{}插入了代码块
  2. 在代码块中声明了一个local function
  3. 在代码块中插入了标记语言
  4. 使用调用local function的方式复用了代码

上面已经说了最基础的一种姿势:一个看起来像是HTML代码的整行,会被视为HTML

这里还有两种在C#中插入HTML的方式:

  1. 可以使用<text>标签把整行内容包裹起来,Razor引擎最终会把<text>标签本身移除。
  2. 在行首使用@:声明一下,这样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包起来。这些特殊的关键字包括:

  1. 条件判断:@if()...@switch(){}
  2. 循环:@for(){}, @foreach(){}, @while(){}, @do{}while()
  3. 自动作用域:@using{}
  4. 异常控制:@try{} catch{} finally{}
  5. 多线程中的手动锁管理:@lock(){}

3.3 操纵背后的类

截止到现在为止,我们所接触的所有知识点其实都是在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来给模板类声明类型参数。例子如下:

有三点需要注意:

  1. 都要写在脑门
  2. 在使用@attribute来声明在类上应用的注解时,注解类还是需要带中括号
  3. @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>

仅适用于Razor Page: 指定Model类

使用@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字段(其实不是),当你设置@preservewhitespacetrue时,渲染结果中的空白字符是不会被移除的,包括模板中书写的缩进空白字符。

这个值默认是false,大多数情况下你都不需要去修改它。

3.4 魔改的HTML属性:Directive attributes

在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 一个辅助功能,用来把渲染树中的节点指针暴露出来,使得程序员可以拿到当前渲染元素的对象指针

3.5 叫做Templated Razor delegates,实际上就是一个函数

设想以下模板代码:

@{
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)
    }
}

这里有几个要点:

  1. Razor引擎碰到一个@后跟着一个HTML Tag的时候,会把后续的内容翻译成一个Lambda表达式。Lambda表达式有一个类型为dynamic的入参,有一个类型为object的返回值,实际返回类型是IHtmlContent,入参的变量名叫item
  2. 表达式体内Razor引擎是切换到HTML模式处理的,所以引用变量还要使用@variable的语法
  3. 在C#代码块中,如果@后面跟的既不是冒号@:,也不是HTML标签名@<xx>,那么这行就会被Razor引擎解释为“对某个特殊函数的调用”,而这种“特殊函数”,就是类似上面的Lambda表达式的函数
  4. 仅有一个入参的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小模板”吧

3.6 仅适用于Razor Page的知识点:Tag Helper

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,这个知识点我们就不解释了。

4. 最后两个小知识点

4.1 在Blazor项目中,如何查看转译后的C#类定义?

在项目文件中添加如下声明:

<PropertyGroup>
+  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

这样转译后的C#类定义就会被保存在编译目录中

4.2 在模板文件中,怎么写注释?

这其实应该放在语法章节里去介绍。

在Razor模板文件中,注释有两种:

  • 第一种是Razor注释,它在转译过程中会被完全抹除。
  • 第二种是C#注释或者HTML注释,它在转译过程中不会被抹除。C#的注释和HTML注释都会被完整保留在转译后的C#类定义中,不过C#注释会随着编译器csc的工作而被抹除,但HTML注释甚至在最终输出的时候都不会被抹除,只是浏览器不展示而已。

第二种注释就按正常写就行了,唯一的知识点是第一种注释我们现在还没讲怎么写:它只有一种写法,不分单行注释和多行注释,统一一种写法,如下:

@*
...
*@