Razor是一门相当怪异丑陋的标记语言,但在实际使用中却十分高效灵活。本文主要介绍了Razor是什么,以及Razor引擎的一些浅薄的背后机理。
1. 什么是 Razor,它和 Blazor 有什么关系?写文章前我本想一口气把Razor的基本语法,以及Blazor Server App的编译过程都介绍出来的,奈何文章到了这个长度自由互联的Markdown编辑器实在不堪重负了。就只能将这些零碎的、无聊的基础语法知识,Blazor Server App与Blazor WASM App 编译过程的差别,放在下一篇文章再去讲了。
我们上文提到了 Web UI 框架三大重点:
- 调 DOM API
- 描述交互逻辑
- 调用服务端函数或 API
我们也介绍了 Blazor 的两种工作方式:Blazor Server 和 Blazor WebAssembly。虽然 Blazor 有两套工作方式,但都逃不脱一个问题:如何用代码描述视觉和交互逻辑。
描述交互逻辑,就必然要用一种程序设计语言去表达这些逻辑。主流前端框架选择了 JavaScript,这出于两点考虑:
- 因为浏览器天然的有 JS 的运行环境。
- 因为交互逻辑要放在浏览器中执行
Blazor 选择了 C#,由于浏览器不支持 C#的运行环境,所以 Blazor 有以下妥协
- 其中一条路就是把 C#编译成 WebAssembly,这就是 Blazor Assembly 工作方式
- 另一条路就是,既然浏览器没有 C#的运行环境,那就不要把交互逻辑放在浏览器中执行,直接放在服务端算了,这就是 Blazor Server
在 React/Angular/Vue 之前,HTML 天然的就支持在其内部引用 JavaScript 代码
在服务端渲染流行的年代,JSP 和 ASP .NET 这种技术,就是发明了一种四不像的语言,用来在 HTML 文档中嵌入 Java 代码段或 C#代码段,处理过程分两部分
- 第一步,编译时将这种四不像文档转译成 Java 或 C#的类,
- 第二步,浏览器的每次请求其实都是在调用这种类中的一个方法,这个方法会给客户端返回生成的 HTML+CSS 文档
主流前端框架摒弃了服务端渲染,进一步融合了 JavaScript 或 TypeScript,HTML 和 CSS,典型的就是 React 主推的*.jsx
和*.tsx
。这些特殊的脚本并不能直接跑在浏览器中,最终会被工具链转换成 HTML 文档、JS 代码文件和 CSS 文件
Blazor 则开了一点历史的倒车:它把服务端渲染的那套四不像的东西又拉出来了,就是 Razor。
- 第一步依然是相同的,Blazor 依然会把这种四不像脚本语言先转译成一个 C#的类
- 第二步是不同的
- Blazor WebAssembly 会将这个 C#类进一步转译成 WebAssembly 代码跑在浏览器上
- Blazor Server 虽然会在服务端像 ASP .NET 一样直接跑类中的方法,但最终返回给客户端的并不是渲染好的全新的 HTML+CSS 文档,而是发送更新 UI 的指令
而 Blazor 使用的这套,将视觉和交互逻辑融合起来的四不像脚本语言,就叫 Razor。我们上面也说了,Razor 其实是在服务端渲染时代就存在的一个东西,这个东西其实就俩使命:
- 把 HTML&CSS 和 C#嵌合在一起,使用上更像是在 HTML&CSS 中嵌 C#,而不像现在的前端框架,在 JS/TS 中嵌 HTML 标签
- 它最终会被转译成一个类。换句话说,Razor 虽然写着像是标记语言,像是在 HTML&CSS 中嵌了一些 C#代码,但实际上它是一个 C#类
Razor 脚本的历史其实很长,ASP .NET 时代它就是 UI 描述语言,那时候大家用*.cshtml
来做脚本文件的后缀,也很好理解嘛,把 html 和 CSharp 结合在一起,叫*.cshtml
是非常河鲤的。最近,特别是在 Blazor 框架下,大概是微软的人觉得用*.cshtml
太土了,所以又启用了一个新的文件后缀,就叫*.razor
,其实就是喵叫了个咪,没有什么本质区别。
最重要的要谨记以下两点:
- Razor 是一门四不像语言,在 HTML 中掺 C#
- Razor 文件虽然看起来像是 HTML,但其实是个 C#的类
特别是第二点,不清晰的认识到第二点,就很难理解 Razor 语法中很多奇怪的地方
2. Razor 是怎么被转译成 C#类的?上面我们介绍了什么是 Razor,按常理来说,我们接下来应该介绍 Razor 怎么写,即 Razor 的语法。但我觉得有必要,在讲解 Razor 的语法之前,探究一下 Razor 文件是怎么被转译成一个 C#类 的这个过程。
虽然从框架的使用者的视角来说,并没有必要去了解、理解框架的工作方式,只需要掌握使用方法就行了。但 Razor 太拧巴了,就像上面说的,这是一门四不像的标记语言,如果不了解、理解它背后的工作原理,那么 Razor 中很多奇怪的语法、用法,使用者就无法理解。并且当代码出错时,就完全没有调试纠错的思路。
而更要命的是,上一篇文章我们仅是走马观花的介绍了,使用默认的.Net Core 项目模板创建出来的 BlazorWASM 和 BlazorServer 项目,如果你回过头去看上一篇文章我们介绍的项目中的目录与文件明细,会发现很多不明所以的内容(特别是对之前完全不了解.Net 框架的人来说)。所以这里还得先给大家介绍,如何一步步的纯手动的创建一个 Blazor 项目。
所以这个小节有两个主要任务:
- 介绍如何以最原始的方式创建一个最简单的 BlazorWASM 项目。
- 再介绍如何从命令行编译这个项目,以及编译的过程中都发生了什么,以及最终这个项目是怎么 run 起来的。
在上面两部分内容介绍完毕后,我们会再简短的介绍一下如何创建一个类似的 BlazorServer 项目
2.1 徒手创建并运行一个 BlazorWASM 项目现在,让我们抛开dotnet new
这个命令行工具,我们直接徒手开始搓一个项目。这里你不需要用到 Visual Studio,甚至不需要用到 VSCode,你需要的只是一个文本编辑器。
csproj
文件
显然,我们需要先创建一个目录(文件夹,我在系列文章中将使用目录这个术语,后续不再说明),我们打开 powershell,或者如果你在 Linux 平台或 Mac OS 平台,打开 Terminal,如下使用mkdir
命令创建一个目录:
>> mkdir HelloRazor
首先,所有的.Net 项目都由一个*.csproj
文件声明,这个文件里,以 XML 的形式描述了这个项目的类型、结构、包含多少源代码,你可以把这个文件理解为项目的声明文件+项目的编译脚本。一般来说,像 Visual Studio,以及dotnet new
这种工具,创建项目时会为你自动生成一个*.csproj
文件,但这里我们决定,在HelloRazor
目录下新建一个名为HelloRazor.csproj
的文件,其内容如下:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
</ItemGroup>
</Project>
上面的文件内容主要说了三件事:
- 这个项目面向.Net 6.0
- 这个项目依赖两个包:
M.A.C.WebAssembly
和M.A.C.WebAssembly.DevServer
- 虽然没有明说,但这个项目,会把当前目录下的所有
*.cs
文件视为项目的源代码文件,即所有的*.cs
文件都会参与编译
如所有程序设计语言一样,.Net 项目也需要一个入口类,一个入口函数。这种函数在 C 语言中叫int main(int argc, char ** argv)
,在 C#中叫static void Main(string[] args)
。现在我们将在HelloRazor
目录下与HelloRazor.csproj
平齐,再创建一个文件Program.cs
,它的内容如下:
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace HelloRazor;
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
}
在上一篇文章中我们讲过,Blazor WebAssembly 的工作方式和 Angular、React、Vue 是类似的。那么类比一下:
- 一个 React 的前端项目:
- 开发人员要在本地把它 run 起来一边开发一边调试,就需要把它塞给 Webpack dev server,也就是一个本地的 Web Server
- 而实际部署到生产环境时,打包后的前端编译产物会被拷贝到 Nginx 中托管起来,此时 Nginx 充当了 Web Server 的角色
- 一个 Blazor WebAssembly 项目:
- 开发人员要在本地把它 run 起来一边开发一边调试,就需要把它塞给一个类似于 Webpack dev server 的东西中去。
- 而实际部署到生产环境时,编译,或者叫打包后的产物,也一样是会被拷贝到 Nginx 中托管起来
.Net 工具链中,有没有一个类似于 webpack dev server,或者 Nginx 的东西呢?答案是:有,也没有
webpack dev server 和 Nginx,以及其它的 Web Server 软件,它们本质上都是一个现成的、可执行的二进制,然后通过配置文件中的信息去寻找应当如何处理 Http 请求。
.Net 中没有这样现成的、可执行的二进制,但有一个库,叫 Kestrel,这个库的作用,就是用来处理Http 请求里有关网络收发的繁杂工作:比如网络层的连接管理、将 TCP 解析为 HTTP Request,再将 HTTP Request 解析为.Net 技术栈中相应的对象,以及在回包时,将.Net 技术栈中相应的对象,再翻译成 HTTP 回应报文,再通过网络层发回去。
在.Net 技术栈中,把这个名为 Kestrel 的库,也称为 Web Server,但它和 Nginx、Apache、以及 webpack dev server 有着本质的区别:
- Kestrel 只是一个库,你需要进行编译、编译、链接后才能得到一个可执行的二进制
- 相较于 webpack dev server, Nginx, Apache 这种只能直接托管静态资源的 Web Server,.Net 的开发人员可以在 Kestrel 库的基础上,自己编写能动态处理 HTTP 请求的应用程序 -- 这,其实就是 ASP .NET Core 整个技术栈的工作方式
所以现在回过头再去看上面Program.cs
的代码,以下的注释你就能稍微理解了:
// 创建一个hostBuilder,它用来创建一个Host实例,这个Host
// 1. 在服务端会打包一个叫 `App` 的Blazor Component,类似于React中的根组件,以WebAssembly的方式返回给浏览器
// 2. 而这个 `App` 的Blazor Component在渲染完成之后,会用渲染成果替换掉HTML文档中那个叫 `app` 的元素
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
上面这段代码,和 React 的下面这段代码有异曲同工之妙,但工作方式完全不同:
ReactDOM.render(<App />, document.getElementById("app"));
step 3 : 编写根 Blazor Component :App
和 React 一样的是,Blazor 项目都由一个根组件一层层嵌套渲染起来。Blazor 的根组件,一般是一个前端路由器。现在,我们在HelloRazor
目录中再创建一个名为App.razor
的文件,其内容如下:
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
<h1>Page Not Found</h1>
</NotFound>
</Router>
目前我们还没有学习 Razor 中的语法以及特殊元素,所以上面的代码我们并看不懂。但根据单词基本能猜个大概,这个前端路由器的功能是:
- 如果用户访问的路径能被路由表匹配,那么就去渲染
@routeData
,也就是渲染对应的子 Blazor Component - 如果不能,则渲染
<h1>Page Not Found</h1>
上面我们说了,根组件(后续文章中,Component和组件将频繁出现,其实它俩是同一个意思)其实就是个前端路由器,我们当然不能只写个路由器,不然用户访问哪都是<h1>Page Not Found</h1>
,我们现在就来编写一个真正意义的组件,一个真正意义上的Razor Page
。
在HelloBlazor
目录中新建一个文件叫Index.Razor
,其内容如下:
@page "/"
<h1>Hello, Razor!</h1>
<p>This is a Razor page, but only contains standard HTML code.</p>
这个文件包含两部分内容:
- 脑门上的
@page "/"
,其实是路由声明:声明这个组件仅匹配路由路径"/"
,也就是根目录 - 余下两行就是标准的 HTML 代码,没有任何魔法
如同 React 一样,前端渲染框架都要有一个默认的 HTML 文档,这个文档中一般有一个 ID 为root
或app
的<div>
元素,它其实就是前端框架渲染结果的占位符。在部署的 Web 服务器中,这个文档是纯纯的静态文件
Blazor 也一样,我们依然也需要创建这样一个默认文档。而这个文档也是一个纯纯的静态文件,而 Kestrel 库默认情况下,会把项目目录下一个名为wwwroot
的子目录中的所有东西都托管为静态资源的。所以,这次,我们要在HelloRazor
目录下创建一个名为wwwroot
的子目录,然后在子目录下创建一个名为index.html
的文档。
鉴于我们在Program.cs
中已经写明了,那个占位符元素的 ID 是app
,所以这个index.html
的内容应当如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>HelloRazor</title>
<base href="/" />
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
可以看到除了那个 ID 为app
的空<div>
,还有一行引用了_framework/blazor.webassembly.js
这个文件:这其实就是 Blazor 组件打包后生成的文件
现在,你的HelloRazor
目录中的文件结构应当如下所示:
现在,打开命令行,在HelloRazor
目录中,执行以下命令:
>> dotnet restore
...
...
>> dotnet build
...
...
>> dotnet run
...
...
其中
dotnet restore
是下载项目编译所需要的依赖包dotnet build
是编译项目dotnet run
是运行项目
效果大致如下:
然后在浏览器中打开http://localhost:5000
或https://localhost:5001
即可看到如下效果:
现在我们已经手动,from scratch 的创建了一个 Blazor 项目,简单的总结一下:
- 我们编写了两个 Blazor 组件:
App.razor
: 没有视觉,是一个前端路由器Index.razor
: 一个视觉组件,绑定在路径"/"
,即根目录上
- 我们通过编写代码,编写了一个 Web Server,这个 Web Server 做了两件事:
- 托管了静态文档
wwwroot/index.html
,这个文档内部有两个重点- 存在一个
<div id="app">
用来当 Blazor 组件渲染的占位符 - 引用了一个 js 文件
_framework/blazor.webassembly.js
,这实际上是 Blazor 组件编译打包后的产物
- 存在一个
- 通过
Program.cs
中的几行代码,让这个 Web Server 对 Blazor 组件进行打包,也就是生成上面所谓的blazor.webassembly.js
- 托管了静态文档
实际项目运行时,我们运行的是编译链接后的 Web Server,浏览器访问localhost:5000
时,同时下载了index.html
和服务端生成的blazor.webassembly.js
。之后,浏览器执行blazor.webassembly.js
渲染了两个组件,并最终将浏览器中的<div id="app">
替换为渲染成果。
整体流程故事就是这样,但这里有一个核心点需要我们关注:服务端是怎么生成blazor.webassembly.js
的?
过程分如下三步走
- 在项目编译期,所有 Razor 页面,也就是 Blazor 组件,也就是
*.razor
文件,都被转译成了 C#文件,然后进一步的,编译成了 dll 中的 IL 代码。也就是一个名为HelloRazor.dll
的可执行二进制- 是的,你没有看错,.Net Core 项目的可执行二进制的后缀依然是
*.dll
。。- 这种可执行比较怪,或许不应该叫“可执行二进制”:对于一般的 console application,由于不牵涉额外的运行时依赖的特殊类库,可以直接用
dotnet *.dll
方式运行,但对于一些复杂应用,比如 Blazor App,直接以dotnet *.dll
试图运行时可能会找不到对应的运行时依赖库
- 这种可执行比较怪,或许不应该叫“可执行二进制”:对于一般的 console application,由于不牵涉额外的运行时依赖的特殊类库,可以直接用
- 如果你想得到 Windows 下标准的 PE 可执行二进制文件(
*.exe
),或 Linux 下标准的 ELF 可执行二进制,需要进一步的使用dotnet publish
命令进行发布 - 发布的过程,其实就是把编译产生的
*.dll
文件包装成了一个 PE 文件或 ELF 文件,就是套了一层壳子,外加把依赖库集中放置在身边 -- 或者self-contained
形式的话整体打成一个二进制
- 是的,你没有看错,.Net Core 项目的可执行二进制的后缀依然是
- 在项目编译期,上一步编译出来的 Blazor 组件类,被打包转译成了 Web Assembly 代码。再然后连同.Net 的一些类库也被打包成 Web Assembly 代码,最后全捏在一起,形成了一个
blazor.webassembly.js
,并放置在运行目录的wwwroot
子目录中 - 在项目运行期,上一步生成的
blazor.webassembly.js
就变成了一个在wwwroot
目录下托管的普通静态资源,变得与index.html
别无二致
这其中我们要重点关注第一步,即要关注一个我们从上一篇文章就强调的概念:Blazor 组件,其实就是 C#类,只不过书写成了*.razor
这种形式。我们要重点关注,这个从*.razor
到*.cs
的转译过程中,发生了什么。了解这背后的机理,有助于我们理解*.razor
中一些奇怪的语法。
至于第二步,我们作为框架的使用者,没必要去过分关心。有两个理由:
- 在 Blazor WASM(WASM 即是 Web Assembly 的缩写,后续文章不再说明)工作方式中,交互逻辑才需要被转译成 WASM 代码下发给浏览器。而在 Blazor Server 工作方式中,交互逻辑直接在 Web Server 端执行,不需要转译。过分关注从 C#到 WASM 的转译过程只对 Blazor WASM 工作方式的应用有用,对 Blazor Server App 是没有意义的
- 这部分知识过于艰深晦涩,并且对业务开发几乎没有任何意义
在执行了dotnet build
后,项目目录下就会默认的生成两个子目录:obj
和bin
,这两个目录下有海茫茫的文件与目录,目录你可以这样简单的理解这两个目录:
obj
: 存放编译产物与中间产物,包括编译前必要的一些准备工作所需的临时文件等bin
: 可执行二进制。比如在bin
目录下除了会有可执行二进制外,还会有wwwroot
目录存储着运行时需要托管的静态资源(包括生成的blazor.webassembly.js
,以及运行时所需的相关库文件)
对于我们这个项目来说,有两个编译产物需要关注
obj/Debug/net6.0/HelloRazor.dll
和bin/Debug/net6.0/HelloRazor.dll
,可以认为这两个文件是同一个文件。后续文章将直接以HelloRazor.dll
来描述bin/Debug/net6.0/wwwroot/_framework/blazor.webassembly.js
。这个是最终生成的 Web Assembly 代码
我们上面说了,*.razor
文件会被先转成*.cs
,然后再编进二进制中。但在 Blazor WASM 工作模式下,中间那一步是不可见的,即obj
目录下是没有App.cs
和Index.cs
这样的中间文件的:它们被一步到位的编译进了HelloRazor.dll
中
没有中间的*.cs
文件,意味着我们没法直接观察中间的*.cs
长什么样子。但有个比较曲折的方式:我们可以使用反编译工具ILSpy,去查看由 IL 代码反编译生成的 C#代码长什么样。
下面是HelloRazor.dll
的反编译结果:
首先是整个 dll 中包含了三个类:Program
是我们写的入口类,App
和Index
则是 Razor 代码生成的类:
App
类
以下是App.razor
的内容
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
<h1>Page Not Found</h1>
</NotFound>
</Router>
下面是反编译的App
的类
// HelloRazor.App
using HelloRazor;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.CompilerServices;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
public class App : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.OpenComponent<Router>(0);
__builder.AddAttribute(1, "AppAssembly", RuntimeHelpers.TypeCheck(typeof(App).Assembly));
__builder.AddAttribute(2, "Found", (RenderFragment<RouteData>)((RouteData routeData) => delegate(RenderTreeBuilder __builder2)
{
__builder2.OpenComponent<RouteView>(3);
__builder2.AddAttribute(4, "RouteData", RuntimeHelpers.TypeCheck(routeData));
__builder2.CloseComponent();
}));
__builder.AddAttribute(5, "NotFound", (RenderFragment)delegate(RenderTreeBuilder __builder2)
{
__builder2.AddMarkupContent(6, "<h1>Page Not Found</h1>");
});
__builder.CloseComponent();
}
}
通过对比,不难看出一些一一对应的行。我们也可以简单的总结一些规律
App.razor
是一个前端路由器,虽然书写上均是 XML 元素+属性的形式,但用到的元素和属性都不属于 HTML 的范畴。这里,我们可以把非 HTML 范畴的 XML 元素标签或属性简单的理解为Blazor 框架内部为我们已经实现的组件。
比如很明显的:
<Router>
就对应着OpenComponent<Router>
,我们可以理解为 Blazor 框架内部为我们实现了一个名为Router
的组件<Found>和<NotFound>
虽然也是 XML 元素,但对应的 C#代码其实是Router
组件中的一个属性<RouteView>
对应着OpenComponent<RouteView>
,可以理解这是一个名为RouteView
的组件
除了这种组件,还有一行特别瞩目:
<h1>Page Not Found</h1>
被转译成了AddMarkupContent(.., "<h1>Page Not Found</h1>")
根据这个,我们目前可以简单的认为,*.razor
中原生的 HTML 代码其实是被直接以字符串的形式转译过去的。
关于这一点,我们可以在 Index.razor
中得到验证
Index
类
以下是Index.razor
的内容:
@page "/"
<h1>Hello, Razor!</h1>
<p>This is a Razor page, but only contains standard HTML code.</p>
以下是反编译的Index
类的代码:
// HelloRazor.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
[Route("/")]
public class Index : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>\r\n\r\n");
__builder.AddMarkupContent(1, "<p>This is a Razor page, but only contains standard HTML code.</p>");
}
}
这就简直没什么悬念了,我们可以暂时总结出下面三条规律:
- 原生的 HTML 代码会被转译成
AddMarkupContent
,以字符串的形式喂给__builder
- Blazor 框架已经为我们实现了一些组件,这些组件均会被转译成
OpenComponent<XXX>
- 组件中的属性,一部分以
*.razor
中以 XML 属性的形式出现,一部分以子元素的形式出现
另外,虽然我们不清楚__builder
的具体实现,也没必要去过分纠结,但有一点可以肯定的是:它内部是一个树型的数据结构,它就是对标于 React 框架中的 Virtual Dom 概念的一个东西,它最终的渲染结果,其实就是代表着最终视觉效果的 DOM
在了解了一些浅薄的 Razor -> C#的知识后,我们终于可以开始介绍 Razor 这套标记语言的语法了。本小节将在上一小节的示例项目的基础上,循序渐进的讲解一些基础的 Razor 语法
3.1 Razor 表达式 = 在 Razor 页面中写 C#表达式 :@xxx
与@(xxx)
在 Razor 页面中可以书写 C#表达式,最终呈现的渲染结果将对表达式进行求值,比如我们可以把Index.razor
改写成如下模样:
@page "/"
<h1>Hello, Razor!</h1>
<p>Current Time Is @DateTime.Now.ToString()</p>
最终呈现效果如下:
语法很简单,就是在一个@
后加一个合法的 C#表达式,即可。
表达式最终会被隐式的调用ToString()
转成字符串(也就是说上面显式的调用ToString()
是不必要的),并且为了避免注入,也会对字符串进行转义处理。这都没什么好说的,比较容易理解。
而除过记住这个语法,更重要的是去理解,这种语法在 C#类那边,被转译成了什么样子。下面是更改后,对应的 C#类在 ILSpy 中的样子(using
语句已略,后方不再说明):
[Route("/")]
public class Index : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(1, "<h1>Hello, Razor!</h1>\r\n\r\n");
__builder.OpenElement(1, "p");
__builder.AddContent(2, "Current Time Is ");
__builder.AddContent(3, DateTime.Now.ToString());
__builder.CloseElement();
}
}
这里我们接触到了新的方法:OpenElement
,显然它是用来转译 HTML 原生元素用的。而 C#表达式则被转译成了AddContent
。如果你仔细阅读了上一章节的内容,这里你应该相当豁然开朗,甚至如果你有相关编译原理知识的功底,知道如何写一个 Parser 的话,你大致已经有了一个“自己写一个 Razor 引擎转译器”的思路了
一些额外知识点:
- 有些人会把 Razor 文件转译器叫做 Razor 引擎,而有些人会把从转译到整个渲染运行的所有相关的类库加在一起,叫Razor 引擎,我可能在后续文章中不会特别区别,可能会混着叫,大家按上下文自行甄别
@
代表着 Razor 引擎会把后续当作是一个 C#表达式去处理。而如果你真的想输入一个@
字符的话,连续写两个@@
就可以了- Razor 引擎也并不是简单无脑的把所有
@
字符后面的后续当成 C#表达式去处理,一些场景它会智能分析,比如像<a href="mailto:damn@wtf.com">damn@wtf.com</a>
这种情形,它就能自动分析出来这是电子邮件地址,而不做表达式求值 - 表达式中间是不能有空格的,比如
<p>@DateTime. Now. ToString()</p>
是非法的,引擎仅会把DateTime
当成表达式,而由于这是一个类型,不是一个合法的表达式,编译期就会把这种错误检查出来。 但有时候恰巧加个空格导致一个残疾的表达式在语法上是合法的,这种错误可能就只能等到运行期才可能报错了。 - 要使用复杂的、包含空格或者其它杂技的表达式,一个很简单的方法:加括号,比如
<p>@(DateTime. Now. ToString())</p>
就是合法的。这种由@(xxx)
将表达式整个括起来的写法,被称为Explicit Razor Expression
,我将称其为显式表达式,而不加括号的简便写法,叫Implicit Razor Expressions
,我将称其为隐式表达式 - 隐式表达式是无法使用泛型的,典型的就是调用泛型方法。比如
<p>@GenericMethod<int>()</p>
,这是非法的。这是由于 Razor 引擎无法区分泛型表达式中的尖括号,与 HTML 元素、Blazor 组件的尖括号。这时你只能使用显式表达式,如<p>@(GenericMethod<int>())</p>
- 默认情况下,表达式求值后,会调用
ToString()
转成字符串,再被脱敏进行防注入。意味着<p>@("<h1>Header?</h1>")</p>
最终求值的结果其实是"<h1>Header?</h1>"
。而如果你真的想作大死,就是要输出 HTML 标签,那么你可以使用@((MarkupString)("<h1>Header?</h1>"))
这种方式。其中MarkupString
是一个类型,全名为Microsoft.AspNetCore.Components.MarkupString
,其实就是加一个强制类型转换。。但强烈建议不要这么作死。
鉴于自由互联的markdown编辑器已经开始卡顿了,并且这篇文章已经足够长了,我们就把其它Razor基础语法放在下一篇文章中再介绍吧。
有好奇心的同学其实已经可以顺着这个思路去官网查文档学习Razor Syntax了。没必要非得等我写教程
4. 指正与更正 4.1 评论指正总结总结一下:
-
razor是先进的,我个人对其有着不正确的评价。描述UI势必就要把JS/TS或其它什么程序设计语言,和HTML&CSS掺在一起写。。。到底是面里掺水还是水里掺面,其实大家掺完都长一个逼样。。要说丑,大家都丑。
Razor也只是长的像JSP,其实比JSP这种货色不知道高到哪里去了。 -
文中最大的错误在于对
webassembly.js
的解释是完全错误的:它并不是前端那种打包后的bundle.js
,它里面没有Blazor组件的代码,它也不是WASM代码。
WASM项目打包后其实有三层:webassembly.js
->dotnet.wasm->*.dll,这三层摞在一起才相当于传统前端打包后的那个bundle.js
我可以用是,我知道。我这是在简化问题,先隐去一些细节,目的是之后展示更大的图景。
这句来源于《C# in depth》的话来狡辩,但这个错误确实太硬了,没法洗。 -
关于Kestrel和Web Server。
WASM项目打包后,和其它前端项目一样,就是一堆静态文件。
但在开发时,本地势必要起个dev server,前端那边用的是webpack dev server,Blazor这边用的是自己写的web server:一个内部使用Kestrel库的web server。
在.net 5及以前的版本中,obj
中间目录会有Blazor组件对应的C#代码存在,.net 6后就把这些中间产物移除了(不让我们直接看了),可能是工具链做了某些调整。
位置是obj/Debug/net5.0/Razor/...
,生成的C#代码掺杂了大量的注释,可读性不高,不如直接用ILSpy反着看紧凑。
一个默认模板项目中的App.razor
会被转译成下面这样: