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="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/23/@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="https://www.cnblogs.com/" />
</head>
<body>
<div id="app">Loading...</div>
<script src="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/23/_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="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/23/@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了,沒必要非得等我寫教程
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/448025.html
標籤:.NET技术
