Blazor 002 : 一種開歷史倒車的UI描述語言 — Razor
Razor是一門相當怪異醜陋的標記語言,但在實際使用中卻十分高效靈活。本文主要介紹了Razor是什麼,以及Razor引擎的一些淺薄的背後機理。
寫文章前我本想一口氣把Razor的基本語法,以及Blazor Server App的編譯過程都介紹出來的,奈何文章到了這個長度部落格園的Markdown編輯器實在不堪重負了。就只能將這些零碎的、無聊的基礎語法知識,Blazor Server App與Blazor WASM App 編譯過程的差別,放在下一篇文章再去講了。
1. 什麼是 Razor,它和 Blazor 有什麼關係?
我們上文提到了 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,你需要的只是一個文本編輯器。
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>
上面的文件內容主要說了三件事:
- 這個項目面向.Net 6.0
- 這個項目依賴兩個包:
M.A.C.WebAssembly
和M.A.C.WebAssembly.DevServer
- 雖然沒有明說,但這個項目,會把當前目錄下的所有
*.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 中的語法以及特殊元素,所以上面的程式碼我們並看不懂。但根據單詞基本能猜個大概,這個前端路由器的功能是:
- 如果用戶訪問的路徑能被路由表匹配,那麼就去渲染
@routeData
,也就是渲染對應的子 Blazor Component - 如果不能,則渲染
<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>
這個文件包含兩部分內容:
- 腦門上的
@page "/"
,其實是路由聲明:聲明這個組件僅匹配路由路徑"/"
,也就是根目錄 - 餘下兩行就是標準的 HTML 程式碼,沒有任何魔法
step 5 : 編寫一個默認 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 組件打包後生成的文件
step 6 : 編譯、啟動項目
現在,你的HelloRazor
目錄中的文件結構應當如下所示:
現在,打開命令行,在HelloRazor
目錄中,執行以下命令:
>> dotnet restore
...
...
>> dotnet build
...
...
>> dotnet run
...
...
其中
dotnet restore
是下載項目編譯所需要的依賴包dotnet build
是編譯項目dotnet run
是運行項目
效果大致如下:
然後在瀏覽器中打開//localhost:5000
或//localhost:5001
即可看到如下效果:
2.2 Blazor WASM 編譯、運行背後的一些淺層機理
現在我們已經手動,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
中
Razor 程式碼與 IL 程式碼
沒有中間的*.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
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>
最終呈現效果如下:
語法很簡單,就是在一個@
後加一個合法的 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:[email protected]">[email protected]</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
,其實就是加一個強制類型轉換。。但強烈建議不要這麼作死。
3.2 待續
鑒於部落格園的markdown編輯器已經開始卡頓了,並且這篇文章已經足夠長了,我們就把其它Razor基礎語法放在下一篇文章中再介紹吧。
有好奇心的同學其實已經可以順著這個思路去官網查文檔學習Razor Syntax了。沒必要非得等我寫教程