Blazor 002 : 一种开历史倒车的UI描述语言 — Razor

  • A+
所属分类:.NET技术
摘要

Razor是一门相当怪异丑陋的标记语言,但在实际使用中却十分高效灵活。本文主要介绍了Razor是什么,以及Razor引擎的一些浅薄的背后机理。


Razor是一门相当怪异丑陋的标记语言,但在实际使用中却十分高效灵活。本文主要介绍了Razor是什么,以及Razor引擎的一些浅薄的背后机理。

写文章前我本想一口气把Razor的基本语法,以及Blazor Server App的编译过程都介绍出来的,奈何文章到了这个长度博客园的Markdown编辑器实在不堪重负了。就只能将这些零碎的、无聊的基础语法知识,Blazor Server App与Blazor WASM App 编译过程的差别,放在下一篇文章再去讲了。

1. 什么是 Razor,它和 Blazor 有什么关系?

我们上文提到了 Web UI 框架三大重点:

  1. 调 DOM API
  2. 描述交互逻辑
  3. 调用服务端函数或 API

我们也介绍了 Blazor 的两种工作方式:Blazor Server 和 Blazor WebAssembly。虽然 Blazor 有两套工作方式,但都逃不脱一个问题:如何用代码描述视觉和交互逻辑。

描述交互逻辑,就必然要用一种程序设计语言去表达这些逻辑。

主流前端框架选择了 JavaScript,这出于两点考虑:

  1. 因为浏览器天然的有 JS 的运行环境。
  2. 因为交互逻辑要放在浏览器中执行

Blazor 选择了 C#,由于浏览器不支持 C#的运行环境,所以 Blazor 有以下妥协

  1. 其中一条路就是把 C#编译成 WebAssembly,这就是 Blazor Assembly 工作方式
  2. 另一条路就是,既然浏览器没有 C#的运行环境,那就不要把交互逻辑放在浏览器中执行,直接放在服务端算了,这就是 Blazor Server

视觉和交互之间最好要互相融合起来,这样框架的使用者用起来会更直观

在 React/Angular/Vue 之前,HTML 天然的就支持在其内部引用 JavaScript 代码

在服务端渲染流行的年代,JSP 和 ASP .NET 这种技术,就是发明了一种四不像的语言,用来在 HTML 文档中嵌入 Java 代码段或 C#代码段,处理过程分两部分

  1. 第一步,编译时将这种四不像文档转译成 Java 或 C#的类,
  2. 第二步,浏览器的每次请求其实都是在调用这种类中的一个方法,这个方法会给客户端返回生成的 HTML+CSS 文档

主流前端框架摒弃了服务端渲染,进一步融合了 JavaScript 或 TypeScript,HTML 和 CSS,典型的就是 React 主推的*.jsx*.tsx。这些特殊的脚本并不能直接跑在浏览器中,最终会被工具链转换成 HTML 文档、JS 代码文件和 CSS 文件

Blazor 则开了一点历史的倒车:它把服务端渲染的那套四不像的东西又拉出来了,就是 Razor。

  1. 第一步依然是相同的,Blazor 依然会把这种四不像脚本语言先转译成一个 C#的类
  2. 第二步是不同的
    1. Blazor WebAssembly 会将这个 C#类进一步转译成 WebAssembly 代码跑在浏览器上
    2. Blazor Server 虽然会在服务端像 ASP .NET 一样直接跑类中的方法,但最终返回给客户端的并不是渲染好的全新的 HTML+CSS 文档,而是发送更新 UI 的指令

而 Blazor 使用的这套,将视觉和交互逻辑融合起来的四不像脚本语言,就叫 Razor。我们上面也说了,Razor 其实是在服务端渲染时代就存在的一个东西,这个东西其实就俩使命:

  1. 把 HTML&CSS 和 C#嵌合在一起,使用上更像是在 HTML&CSS 中嵌 C#,而不像现在的前端框架,在 JS/TS 中嵌 HTML 标签
  2. 它最终会被转译成一个类。换句话说,Razor 虽然写着像是标记语言,像是在 HTML&CSS 中嵌了一些 C#代码,但实际上它是一个 C#类

Razor 脚本的历史其实很长,ASP .NET 时代它就是 UI 描述语言,那时候大家用*.cshtml来做脚本文件的后缀,也很好理解嘛,把 html 和 CSharp 结合在一起,叫*.cshtml是非常河鲤的。最近,特别是在 Blazor 框架下,大概是微软的人觉得用*.cshtml太土了,所以又启用了一个新的文件后缀,就叫*.razor,其实就是喵叫了个咪,没有什么本质区别。

最重要的要谨记以下两点:

  1. Razor 是一门四不像语言,在 HTML 中掺 C#
  2. Razor 文件虽然看起来像是 HTML,但其实是个 C#的类

特别是第二点,不清晰的认识到第二点,就很难理解 Razor 语法中很多奇怪的地方

2. Razor 是怎么被转译成 C#类的?

上面我们介绍了什么是 Razor,按常理来说,我们接下来应该介绍 Razor 怎么写,即 Razor 的语法。但我觉得有必要,在讲解 Razor 的语法之前,探究一下 Razor 文件是怎么被转译成一个 C#类 的这个过程。

虽然从框架的使用者的视角来说,并没有必要去了解、理解框架的工作方式,只需要掌握使用方法就行了。但 Razor 太拧巴了,就像上面说的,这是一门四不像的标记语言,如果不了解、理解它背后的工作原理,那么 Razor 中很多奇怪的语法、用法,使用者就无法理解。并且当代码出错时,就完全没有调试纠错的思路。

而更要命的是,上一篇文章我们仅是走马观花的介绍了,使用默认的.Net Core 项目模板创建出来的 BlazorWASM 和 BlazorServer 项目,如果你回过头去看上一篇文章我们介绍的项目中的目录与文件明细,会发现很多不明所以的内容(特别是对之前完全不了解.Net 框架的人来说)。所以这里还得先给大家介绍,如何一步步的纯手动的创建一个 Blazor 项目。

所以这个小节有两个主要任务:

  1. 介绍如何以最原始的方式创建一个最简单的 BlazorWASM 项目。
  2. 再介绍如何从命令行编译这个项目,以及编译的过程中都发生了什么,以及最终这个项目是怎么 run 起来的。

在上面两部分内容介绍完毕后,我们会再简短的介绍一下如何创建一个类似的 BlazorServer 项目

2.1 徒手创建并运行一个 BlazorWASM 项目

现在,让我们抛开dotnet new这个命令行工具,我们直接徒手开始搓一个项目。这里你不需要用到 Visual Studio,甚至不需要用到 VSCode,你需要的只是一个文本编辑器。

step 1 : 新建目录,创建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> 

上面的文件内容主要说了三件事:

  1. 这个项目面向.Net 6.0
  2. 这个项目依赖两个包:M.A.C.WebAssemblyM.A.C.WebAssembly.DevServer
  3. 虽然没有明说,但这个项目,会把当前目录下的所有*.cs文件视为项目的源代码文件,即所有的*.cs文件都会参与编译

step 2 : 创建入口类

如所有程序设计语言一样,.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 中的语法以及特殊元素,所以上面的代码我们并看不懂。但根据单词基本能猜个大概,这个前端路由器的功能是:

  1. 如果用户访问的路径能被路由表匹配,那么就去渲染@routeData,也就是渲染对应的子 Blazor Component
  2. 如果不能,则渲染<h1>Page Not Found</h1>

step 4 : 编写一个 Hello 页面

上面我们说了,根组件(后续文章中,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> 

这个文件包含两部分内容:

  1. 脑门上的@page "/",其实是路由声明:声明这个组件仅匹配路由路径"/",也就是根目录
  2. 余下两行就是标准的 HTML 代码,没有任何魔法

step 5 : 编写一个默认 HTML 文档

如同 React 一样,前端渲染框架都要有一个默认的 HTML 文档,这个文档中一般有一个 ID 为rootapp<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 组件打包后生成的文件

step 6 : 编译、启动项目

现在,你的HelloRazor目录中的文件结构应当如下所示:

Blazor 002 : 一种开历史倒车的UI描述语言 -- Razor

现在,打开命令行,在HelloRazor目录中,执行以下命令:

>> dotnet restore ... ... >> dotnet build ... ... >> dotnet run ... ... 

其中

  • dotnet restore 是下载项目编译所需要的依赖包
  • dotnet build 是编译项目
  • dotnet run 是运行项目

效果大致如下:

Blazor 002 : 一种开历史倒车的UI描述语言 -- Razor

然后在浏览器中打开http://localhost:5000https://localhost:5001即可看到如下效果:

Blazor 002 : 一种开历史倒车的UI描述语言 -- Razor

2.2 Blazor WASM 编译、运行背后的一些浅层机理

现在我们已经手动,from scratch 的创建了一个 Blazor 项目,简单的总结一下:

  1. 我们编写了两个 Blazor 组件:
    1. App.razor: 没有视觉,是一个前端路由器
    2. Index.razor : 一个视觉组件,绑定在路径"/",即根目录上
  2. 我们通过编写代码,编写了一个 Web Server,这个 Web Server 做了两件事:
    1. 托管了静态文档wwwroot/index.html,这个文档内部有两个重点
      1. 存在一个<div id="app">用来当 Blazor 组件渲染的占位符
      2. 引用了一个 js 文件_framework/blazor.webassembly.js,这实际上是 Blazor 组件编译打包后的产物
    2. 通过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的?

过程分如下三步走

  1. 在项目编译期,所有 Razor 页面,也就是 Blazor 组件,也就是*.razor文件,都被转译成了 C#文件,然后进一步的,编译成了 dll 中的 IL 代码。也就是一个名为HelloRazor.dll可执行二进制
    • 是的,你没有看错,.Net Core 项目的可执行二进制的后缀依然是*.dll。。
      • 这种可执行比较怪,或许不应该叫“可执行二进制”:对于一般的 console application,由于不牵涉额外的运行时依赖的特殊类库,可以直接用dotnet *.dll方式运行,但对于一些复杂应用,比如 Blazor App,直接以dotnet *.dll试图运行时可能会找不到对应的运行时依赖库
    • 如果你想得到 Windows 下标准的 PE 可执行二进制文件(*.exe),或 Linux 下标准的 ELF 可执行二进制,需要进一步的使用dotnet publish命令进行发布
    • 发布的过程,其实就是把编译产生的*.dll文件包装成了一个 PE 文件或 ELF 文件,就是套了一层壳子,外加把依赖库集中放置在身边 -- 或者self-contained形式的话整体打成一个二进制
  2. 在项目编译期,上一步编译出来的 Blazor 组件类,被打包转译成了 Web Assembly 代码。再然后连同.Net 的一些类库也被打包成 Web Assembly 代码,最后全捏在一起,形成了一个blazor.webassembly.js,并放置在运行目录的wwwroot子目录
  3. 在项目运行期,上一步生成的blazor.webassembly.js就变成了一个在wwwroot目录下托管的普通静态资源,变得与index.html别无二致

这其中我们要重点关注第一步,即要关注一个我们从上一篇文章就强调的概念:Blazor 组件,其实就是 C#类,只不过书写成了*.razor这种形式。我们要重点关注,这个从*.razor*.cs的转译过程中,发生了什么。了解这背后的机理,有助于我们理解*.razor中一些奇怪的语法。

至于第二步,我们作为框架的使用者,没必要去过分关心。有两个理由:

  1. 在 Blazor WASM(WASM 即是 Web Assembly 的缩写,后续文章不再说明)工作方式中,交互逻辑才需要被转译成 WASM 代码下发给浏览器。而在 Blazor Server 工作方式中,交互逻辑直接在 Web Server 端执行,不需要转译。过分关注从 C#到 WASM 的转译过程只对 Blazor WASM 工作方式的应用有用,对 Blazor Server App 是没有意义的
  2. 这部分知识过于艰深晦涩,并且对业务开发几乎没有任何意义

在执行了dotnet build后,项目目录下就会默认的生成两个子目录:objbin,这两个目录下有海茫茫的文件与目录,目录你可以这样简单的理解这两个目录:

  • obj: 存放编译产物与中间产物,包括编译前必要的一些准备工作所需的临时文件等
  • bin: 可执行二进制。比如在bin目录下除了会有可执行二进制外,还会有wwwroot目录存储着运行时需要托管的静态资源(包括生成的blazor.webassembly.js,以及运行时所需的相关库文件)

对于我们这个项目来说,有两个编译产物需要关注

  1. obj/Debug/net6.0/HelloRazor.dllbin/Debug/net6.0/HelloRazor.dll,可以认为这两个文件是同一个文件。后续文章将直接以HelloRazor.dll来描述
  2. bin/Debug/net6.0/wwwroot/_framework/blazor.webassembly.js。这个是最终生成的 Web Assembly 代码

我们上面说了,*.razor文件会被先转成*.cs,然后再编进二进制中。但在 Blazor WASM 工作模式下,中间那一步是不可见的,即obj目录下是没有App.csIndex.cs这样的中间文件的:它们被一步到位的编译进了HelloRazor.dll

Razor 代码与 IL 代码

没有中间的*.cs文件,意味着我们没法直接观察中间的*.cs长什么样子。但有个比较曲折的方式:我们可以使用反编译工具ILSpy,去查看由 IL 代码反编译生成的 C#代码长什么样。

下面是HelloRazor.dll的反编译结果:

首先是整个 dll 中包含了三个类:Program是我们写的入口类,AppIndex则是 Razor 代码生成的类:

Blazor 002 : 一种开历史倒车的UI描述语言 -- 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>rnrn"); 		__builder.AddMarkupContent(1, "<p>This is a Razor page, but only contains standard HTML code.</p>"); 	} } 

这就简直没什么悬念了,我们可以暂时总结出下面三条规律:

  1. 原生的 HTML 代码会被转译成AddMarkupContent,以字符串的形式喂给__builder
  2. Blazor 框架已经为我们实现了一些组件,这些组件均会被转译成OpenComponent<XXX>
  3. 组件中的属性,一部分以*.razor中以 XML 属性的形式出现,一部分以子元素的形式出现

另外,虽然我们不清楚__builder的具体实现,也没必要去过分纠结,但有一点可以肯定的是:它内部是一个树型的数据结构,它就是对标于 React 框架中的 Virtual Dom 概念的一个东西,它最终的渲染结果,其实就是代表着最终视觉效果的 DOM

3. 基础的 Razor 语法

在了解了一些浅薄的 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> 

最终呈现效果如下:

Blazor 002 : 一种开历史倒车的UI描述语言 -- Razor

语法很简单,就是在一个@后加一个合法的 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>rnrn"); 		__builder.OpenElement(1, "p"); 		__builder.AddContent(2, "Current Time Is "); 		__builder.AddContent(3, DateTime.Now.ToString()); 		__builder.CloseElement(); 	} } 

这里我们接触到了新的方法:OpenElement,显然它是用来转译 HTML 原生元素用的。而 C#表达式则被转译成了AddContent。如果你仔细阅读了上一章节的内容,这里你应该相当豁然开朗,甚至如果你有相关编译原理知识的功底,知道如何写一个 Parser 的话,你大致已经有了一个“自己写一个 Razor 引擎转译器”的思路了

一些额外知识点:

  1. 有些人会把 Razor 文件转译器叫做 Razor 引擎,而有些人会把从转译到整个渲染运行的所有相关的类库加在一起,叫Razor 引擎,我可能在后续文章中不会特别区别,可能会混着叫,大家按上下文自行甄别
  2. @代表着 Razor 引擎会把后续当作是一个 C#表达式去处理。而如果你真的想输入一个@字符的话,连续写两个@@就可以了
  3. Razor 引擎也并不是简单无脑的把所有@字符后面的后续当成 C#表达式去处理,一些场景它会智能分析,比如像<a href="mailto:damn@wtf.com">damn@wtf.com</a>这种情形,它就能自动分析出来这是电子邮件地址,而不做表达式求值
  4. 表达式中间是不能有空格的,比如<p>@DateTime. Now. ToString()</p>是非法的,引擎仅会把DateTime当成表达式,而由于这是一个类型,不是一个合法的表达式,编译期就会把这种错误检查出来。 但有时候恰巧加个空格导致一个残疾的表达式在语法上是合法的,这种错误可能就只能等到运行期才可能报错了。
  5. 要使用复杂的、包含空格或者其它杂技的表达式,一个很简单的方法:加括号,比如<p>@(DateTime. Now. ToString())</p>就是合法的。这种由@(xxx)将表达式整个括起来的写法,被称为Explicit Razor Expression,我将称其为显式表达式,而不加括号的简便写法,叫Implicit Razor Expressions,我将称其为隐式表达式
  6. 隐式表达式是无法使用泛型的,典型的就是调用泛型方法。比如<p>@GenericMethod<int>()</p>,这是非法的。这是由于 Razor 引擎无法区分泛型表达式中的尖括号,与 HTML 元素、Blazor 组件的尖括号。这时你只能使用显式表达式,如<p>@(GenericMethod<int>())</p>
  7. 默认情况下,表达式求值后,会调用ToString()转成字符串,再被脱敏进行防注入。意味着<p>@("<h1>Header?</h1>")</p>最终求值的结果其实是"&lt;h1&gt;Header?&lt;/h1&gt;"。而如果你真的想作大死,就是要输出 HTML 标签,那么你可以使用@((MarkupString)("<h1>Header?</h1>"))这种方式。其中MarkupString是一个类型,全名为Microsoft.AspNetCore.Components.MarkupString,其实就是加一个强制类型转换。。但强烈建议不要这么作死。

3.2 待续

鉴于博客园的markdown编辑器已经开始卡顿了,并且这篇文章已经足够长了,我们就把其它Razor基础语法放在下一篇文章中再介绍吧。

有好奇心的同学其实已经可以顺着这个思路去官网查文档学习Razor Syntax了。没必要非得等我写教程