主頁 > .NET開發 > Blazor 002 : 一種開歷史倒車的UI描述語言 -- Razor

Blazor 002 : 一種開歷史倒車的UI描述語言 -- Razor

2022-03-24 06:11:22 .NET開發

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="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/23/@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="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目錄中的檔案結構應當如下所示:

project_layout

現在,打開命令列,在HelloRazor目錄中,執行以下命令:

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

其中

  • dotnet restore 是下載專案編譯所需要的依賴包
  • dotnet build 是編譯專案
  • dotnet run 是運行專案

效果大致如下:

build_and_run

然后在瀏覽器中打開http://localhost:5000https://localhost:5001即可看到如下效果:

hello_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 代碼生成的類:

ilspy_overview

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>");
	}
}

這就簡直沒什么懸念了,我們可以暫時總結出下面三條規律:

  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>

最終呈現效果如下:

expression

語法很簡單,就是在一個@后加一個合法的 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 引擎轉譯器”的思路了

一些額外知識點:

  1. 有些人會把 Razor 檔案轉譯器叫做 Razor 引擎,而有些人會把從轉譯到整個渲染運行的所有相關的類別庫加在一起,叫Razor 引擎,我可能在后續文章中不會特別區別,可能會混著叫,大家按背景關系自行甄別
  2. @代表著 Razor 引擎會把后續當作是一個 C#運算式去處理,而如果你真的想輸入一個@字符的話,連續寫兩個@@就可以了
  3. Razor 引擎也并不是簡單無腦的把所有@字符后面的后續當成 C#運算式去處理,一些場景它會智能分析,比如像<a href="mailto:[email protected]">[email protected]</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了,沒必要非得等我寫教程

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/448025.html

標籤:.NET技术

上一篇:dotnet 6 使用 string.Create 提升字串創建和拼接性能

下一篇:dotnet 6 使用 string.Create 提升字串創建和拼接性能

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more