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
是執行專案
效果大致如下:
然後在瀏覽器中開啟http://localhost:5000
或https://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: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
,其實就是加一個強制型別轉換。。但強烈建議不要這麼作死。
3.2 待續
鑑於部落格園的markdown編輯器已經開始卡頓了,並且這篇文章已經足夠長了,我們就把其它Razor基礎語法放在下一篇文章中再介紹吧。
有好奇心的同學其實已經可以順著這個思路去官網查文件學習Razor Syntax了。沒必要非得等我寫教程