1. 程式人生 > 其它 >Blazor 001 : 一個激進的Web開發框架

Blazor 001 : 一個激進的Web開發框架

本文從比較高的位置俯瞰一下 .NET Blazor 技術方向,主要是給大家介紹一下“什麼是 Blazor”
文章後半部分會給出一個 Blazor 中的 Hello World 示例

1. 概覽

1.1 什麼是 Blazor?

Blazor 是一個.NET 全家桶裡的一個 Web UI 開發框架,簡單來說,你可以把它理解為 React/Angular/Vue 的替代品:這是一個用於開發 Web UI 的框架。

從框架使用者的角度來說,最直接的使用體驗,就是使用 C#替換掉了 JavaScript 或 TypeScript

那除了使用 C#而非 JS這一點外, Blazor 和如今流行的三大前端框架 React/Angular/Vue 相比較,還有什麼差異呢?

在聊這些差異之前,我們需要先明確一個基礎知識點:UI = 視覺 + 互動。這個等式並不嚴謹,但你應當明白我說的是什麼意思,所有前端 UI 框架其實都在做兩件核心的事

  1. 指揮 DOM 去渲染視覺樣式(呼叫 DOM API)。這其中框架開發者不可避免的要去學習 HTML 和 CSS 的相關知識
  2. 響應使用者輸入,然後完成一些互動計算,再根據互動計算去高效的更新視覺樣式。而互動計算又分為兩部分
    1. 直接在瀏覽器中,用 JavaScript 做的相關計算
    2. 瀏覽器沒法算的,則是通過通訊方式(HTTP 請求),去呼叫服務端 API

簡化一下,其實就是做了三件事:

  1. 呼叫 DOM API
  2. 使用瀏覽器能執行的程式碼,來完成互動計算
  3. 使用 HTTP 協議呼叫服務端 API

Blazor 其實也是在做這三件事,但 Blazor 有兩種做法:

方法一 : Blazor WebAssembly

  1. 把 C#編譯成 WebAssembly,然後放在瀏覽器上去執行:去呼叫 DOM API 渲染視覺,以及完成互動計算
  2. 依然使用 HTTP 協議呼叫服務端 API

這種做法其實和 React/Angular/Vue 的做法是一樣的,唯一的區別就是為了使用 C#,使用了 WebAssembly 來做中間層。如下圖所示:

代價也是有的,就是 WebAssembly 雖然幹其它事很牛逼,但呼叫 DOM API 時,只能間接的去使用 interop 方式去呼叫 JavaScript 的 DOM API。並且初次載入頁面時,慢到令人窒息。

方法二 : Blazor Server

除了第一次訪問,後續訪問均摒棄 HTTP 協議,使用 WebSocket 在服務端和瀏覽器之間維持一個長連線,瀏覽器上的程式碼只做一件事:根據 WebSocket 訊息來呼叫 DOM API。

  1. 當需要更新 DOM 時,服務端向瀏覽器傳送二進位制訊息。瀏覽器按指令呼叫 DOM API 更新視覺就行
  2. 當有使用者輸入,需要進行互動計算的時候,瀏覽器不算,而是把使用者輸入以二進位制傳送給服務端,服務端來做互動計算,然後回傳計算結果,瀏覽器只管更新 DOM
  3. 互動計算程式碼需要訪問業務邏輯程式碼時,就不存在所謂的“呼叫 API”了,而是直接在服務端呼叫函式即可

如下圖所示:

這種做法非常激進,你以為你點開的網頁是網頁,是 B/S 架構的,其實是一個需要維持長連線的 C/S 構架的應用:瀏覽器只是一個樣式渲染器而已,使用者本質上使用的是一個遠端應用,所有計算,無論大小,都執行在服務端。

總結

  1. 邊緣 Web 開發技術,不流行,缺點相當難以忽視
  2. 優點也相當難以忽視:使用 C#,而不是 JavaScript。配合.NET 服務端技術,一人全棧相當舒服。
    你要認識到,當我們說不使用 JavaScript時,不光是拋開了 JS 或 TS,更重要的是拋開了傳統前端的所有工具鏈: node, webpack, babel 等等等等,這些東西對於一個專業的前端開發工程師來說不算什麼,但對於一個只是想攢一個小應用,小網站,小部落格的個人來說,這些玩意是巨大的心智負擔。
  3. 但你依然無法逃脫 HTML 和 CSS
  4. 目前沒有比較好的,有說服力,廣為人知的商業網際網路產品使用 Blazor 技術棧

1.2 Blazor 中的元件:Razor Components

Blazor 其它 UI 框架一樣,也是分治於“元件”的思想,一個元件可以是一個大到頁面,小到對話方塊、按鈕、輸入表單這樣的 UI 元素。

上面介紹了 Blazor 有兩種工作模式,Blazor 中的元件比較神奇的點在於:元件和具體的工作方式是無關的。配套的工具鏈會幫你去處理兩種工作方式之間的差異,但對於框架開發者來說,元件就是元件,開發元件的時候不需要考慮是 Blazor Server 還是 Blazor WebAssembly。

從程式碼角度來看,React 中的元件是一個 JS 函式,而 Blazor 中的元件則是一個 C#類。在元件內部,要寫上下面的東西:

  1. 元件內部要寫上 UI 渲染邏輯
  2. 元件應當在必要的時候處理使用者的輸入,即處理“事件”
  3. 元件應當是可巢狀的,可複用的
  4. 元件可以被打成一個類庫,或者打成一個 NuGet 包分發在網際網路平臺上,供他人“借用”

雖然本質上,每個 Blazor 元件都是一個 C#的類,但如果全然用public class xxxx去寫這樣的類,是非常不直觀的,於是 Blazor 的解決方式有點類似於上古時期的 JSP 技術:給你一套四不像的標記語言,讓你去寫一種看起來像是 HTML 但又不是 HTML 的東西,然後這個東西再被工具鏈在編譯的時候轉換成*.cs檔案,再編譯成一個.NET 類

這種四不像的檔案以*.razor結尾,在這種檔案裡,你可以寫 HTML,可以寫內嵌的 CSS,然後還可以用一套彆扭的語法去再寫一些 C#程式碼,這個玩意大概長下面這樣:

<div class="card" style="width:22rem">
    <div class="card-body">
        <h3 class="card-title">@Title</h3>
        <p class="card-text">@ChildContent</p>
        <button @onclick="OnYes">Yes!</button>
    </div>
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public string Title { get; set; }

    private void OnYes()
    {
        Console.WriteLine("Write to the console in C#! 'Yes' button selected.");
    }
}

在上面的程式碼示例中,你可以理解為,整個@code {xxx}包起來的區域,相當於你寫了一個partial class,你在裡面可以定義屬性,定義方法,定義欄位等。這樣理解起來,OnYes就是一個方法,而在上面的類 HTML 程式碼中,使用了@onclick="OnYes"這種奇怪的寫法,將一個按鈕的點選回撥,繫結到了一個 C#方法上,而且用@Title@ChildContent這樣奇怪的語法,將類中的屬性引用到了 HTML 中。

那麼雖然目前我們並不明白所有有關*.razor的書寫規範以及工作原理,但我們已經能大致猜出來它的工作流程了:在執行時,這個特殊的類會被例項化,它內部的兩樣東西:

  1. 它要控制頁面如何渲染,或者換句話說,這個類內部最終還是要輸出一種能被瀏覽器直接渲染的東西的,我們暫時可以認為這個類內部有一個方法可以輸出 HTML+CSS 程式碼(但實際並不是這樣
  2. 它本身可以擁有一些屬性與方法來表達業務邏輯,這些屬性與方法可以在程式碼中用@屬性@onclick=方法這種語法被我們使用

假如上面這個程式碼檔案叫做Dialog.razor的話,按照元件應當被複用巢狀的要求,這個元件可以被巢狀在另外一個元件裡,比如我們下面將它巢狀在一個叫Index.razor的檔案中:

@page "/"

<h1>Hello, world!</h1>

<p>
    Welcome to your new app.
</p>

<Dialog Title="Learn More">
    Do you want to <i>learn more</i> about Blazor?
</Dialog>

這時,我們就可以理解為,在最終的編譯出來的.NET 類中,當Index類要輸出渲染時,它會例項化一個Dialog類的例項,並且將dialog.Title的值設定為Learn More, 將diaglog.ChildContent的值設定為Do you want to <i>learn more</i> about Blazor?,然後在必要的位置將兩個diaglog渲染出來

如果 DOM 是渲染的目標的話,那麼這個Index的例項,會被渲染成對應的一顆 DOM 樹,而其中的一顆子樹,其實是Dialog的樣子

2. Hello Blazor

這一節,我們將建立兩個專案,分別先快速體驗一下 Blazor 技術。到今天為止(2022-03-15),.NET 的版本號已經到 6 了,但我們這裡依然使用 5.0 版本做演示,原因嘛,沒有原因,就是倔強。

另外,顯然,你需要在電腦上安裝 .Net Core SDK,建議你先安裝個版本號為 5 開頭的與本文同步。。不要緊張,.NetCore SDK 可以同時安裝 N 多個版本,可以安全共存,最終,在命令列中輸入dotnet --list-sdks時,你要能找到一個以 5 開頭的版本,如下:

2.1 Hello Blazor Server

命令: dotnet new blazorserver -o HelloBlazorServer --framework 'net5.0'

預設創建出了這樣的一個專案:

這其實是一個非常典型的 ASP 專案,如果你點開Program.csStartup.cs看的話,它與普通的 ASP 專案基本沒什麼區別,核心的點在Start.cs中的ConfigureServicesConfigure兩個方法中:

再介紹一下其它目錄:

  1. Program.cs, Startup.cs : 經典的 ASP .Net Core 專案入口
  2. appsettings.jsonappsettings.Development.json : 專案配置檔案
  3. HelloBlazorServer.csproj : 專案編譯指令碼
  4. _Imports.razor : 相當於所有*.razor檔案的公共頭,點開看,裡面全是@using指令
  5. App.razor : 這是整個專案的 UI 根元件,它內部其實只寫了一個<Router>元素,邏輯是:如果路由匹配成功了,則去把匹配 UI 元件包在MainLayout元件裡面進行渲染,否則將一個提示字串包在MainLayout元件裡渲染出來
  6. wwwroot 目錄:裡面包含了靜態資源,與普通 的 ASP .Net Core 專案一樣。需要注意的是,預設建立的專案中,這裡面給你自帶了bootstrap樣式庫和open-iconic圖示庫
  7. Properties 目錄:與經典的 ASP 專案一樣,裡面一個launchSettings.json描述了一些配置項
  8. Data 目錄:這裡麵包含兩個類,一個是預設自帶的WeatherForecast.cs,是一個 Model 類,另外一個是WeatherForecastService.cs類,裡面寫著真正的“業務邏輯”,並且這個SeatherForecastService還被在Startup.cs中註冊成了一個全域性單例的服務
  9. Shared目錄:這裡麵包含了一些自帶的 Blazor 元件,這些元件都是被廣泛使用的公共元件,所以被放在這個目錄中。你會注意到MainLayoutNavMenu元件除了本身的*.razor定義檔案,還有單獨的描述樣式的css檔案
  10. Pages目錄:這裡就包含了所有的頁面 Blazor 元件,如果你要開發一個網站的話,這裡應該是你最常工作的地方。這個目錄中除了Counter, FetchDataIndex三個 Blazor 元件,還有_Host.cshtml, Error.cshtml, Error.cshtml.cs三個檔案,你可以暫時理解為,*.cshtml*.cshtml.cs是 Blazor 元件的傳統寫法

我們再打包一下,觀察一個 Blazor Server App 在打包後會變成什麼,我們將當前這個專案打包到 Linux 平臺上,然後觀察一下輸出:

可以看到,整個應用其實被打包成了一個典型的 ASP 應用:你只需要將這個目錄部署在某臺 Linux 機器上,然後執行./HelloBlazorServer就行了。。你所定義的所有 Blazor 元件都被編譯進了HelloBlazorServer.dll這個 Assembly 中

接下來我們再來驗證一下東西:我們要來驗證,在 Blazor Server App 中,所有的計算都是在服務端完成的。預設建立的這個 App 有個頁面,在本地啟動的話地址是https://localhost:5001/counter,這個頁面上有一個計數器,按一下按鈕,計數器就+1

通過檢視Pages/Counter.razor我們得知,這個計數器中的數值,其實就是Counter類中的一個屬性,每次點選按鈕,其實呼叫的是Counter例項下的IncrementCount()方法

按照上面我們介紹 Blazor Server App 執行模式的說法,那麼每次客戶端使用者在瀏覽器中點選這個按鈕時,其實都會發送一個網路訊息給服務端,然後服務端去執行counter.IncrementCount(),執行結束後,服務端的counter例項去分析 UI 上有哪些地方需要更新,然後再通過網路訊息,指揮瀏覽器更新頁面上渲染的數字。

如果這套理論是正確的,那意味著每次使用者點選這個按鈕,背後都會有資料在瀏覽器與服務端之間來回傳遞。我們用dotnet run命令將整個專案在本地啟動起來,然後開啟瀏覽器,按 F12 開啟除錯控制檯,切換到網路選項卡,然後重新整理地址去訪問這個計數器頁面,我們會看到一個奇怪的網路請求:

這其實是一個全雙工的 WebSocket 連線,點進去,切換到訊息選項卡,然後隨著你每一次點選頁面上的按鈕,你都會看到有二進位制資料在背後一來一回

2.2 Hello Blazor WebAssembly

命令: dotnet new blazorwasm -o HelloBlazorWASM --framework 'net5.0'

創建出這樣一個專案:

這個專案,就和 ASP 沒關係了,我們開啟它的Program.cs去看一眼,如果你對.NET 稍微熟悉的話,你就會立即明白:這個東西,不正常,這個東西,不尋常:

namespace HelloBlazorWASM
{
    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();
        }
    }
}

這個程式入口建立了一個WebAssemblyHost,然後把這個 Host 運行了起來。而其實呢,這個WebAssemblyHost就是個靜態的 WebServer 而已,跟你用create-react-app建立一個 react 專案,然後npm start起來的原理是差不多一樣的。

另外,在Blazor Server的例子中,FetchData頁面的資料,是通過在服務端隨機生成的,服務端的邏輯寫在WeatherForecastService.cs中。而現在在Blazor WebAssembly中,有兩個顯著的區別

  1. 沒有了Data目錄,也沒有了WeatherForecast.csWeatherForecastService.cs
  2. FetchData.razor
    1. 就地定義了結構體WeatherForecast
    2. 資料是從一個靜態的,託管在服務端上的json檔案中獲取的
@page "/fetchdata"
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
    }

    public class WeatherForecast
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public string Summary { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

再結合我們上面講的Blazor WebAssembly的工作方式,我們就能得出一個結論:在這個樣例Blazor WebAssembly程式中,所有的計算負載,其實都是執行在客戶端的瀏覽器上的。

服務端純粹就是一個靜態檔案託管 Web Server,然後我們把這個專案打包一下,看看它長什麼樣子:

打包後這玩意就長這樣:

其實所有的東西都在那個wwwroot目錄裡,而外邊那個web.config,其實是寫給 IIS 看的。

看到沒?這玩意純粹就是個前端專案!跟webpack後的前端專案不能說一模一樣吧,但至少是有異曲同工之妙,把整個dist目錄現在隨便放在一個 Nginx 裡託管起來,它就能跑起來。而我們在Program.cs中看到的所謂WebAssemblyHost,其實就他媽是一個 Kestrel Server,用來託管靜態檔案而已

你不信?行,我下載一個 Windows 版本的 Nginx,給你跑一下你就信了,我把上面的wwwroot目錄拷到 Nginx 目錄下的HelloBlazorWASM目錄中,然後把 Nginx 的配置檔案改成下面這樣:

# ...
    server {
        listen       9438;
        server_name  localhost;

        location / {
            root HelloBlazorWASM/wwwroot;
            index  index.html index.htm;
        }
        # ...
    }
# ...

然後給你跑起來,你看,一點毛病沒有!

2.3 再看 Blazor 的優缺點

通過上面兩個例子,我們對 Blazor 到底是什麼有了一個更直觀的認識,無論是 Blazor Server 還是 Blazor Webassembly,其實都不會革現在流行的前端框架的命,整個 Blazor 技術的優勢和缺陷都相當激進,它的應用範圍也相當受限。

優勢主要來源於 C#和 .NET 工具鏈,如果你對 Azure 熟悉的話,也會知道微軟在雲這方面,真的是給 .NET 做了無縫對接。從感觀上來說,Blazor 的技術,適合於個人與小團隊創業者,做一些負載有限的 Web 應用。

缺陷處十分明顯,對於 Blazor Server 來說,一直需要客戶端瀏覽器通過SingalR與服務端建立一個 WebSocket 連線,所有計算都在服務端,長連線還一直得保持,並且,每新開一個 Tab,就相當於新開了一個連線,效能問題十分堪憂。甚至我們不能說這是一個 Web 應用,這本質上其實是一個以瀏覽器為客戶端的遠端應用

在 Blazor WebAssembly 這邊,你也看到了,即便是我們建立的樣例程式 ,在載入時都非常能明顯的看到一個Loading字樣,如果你再細心的去翻瀏覽器的除錯面板的話,你會看到這個網頁啥啥幹不幹,先給你載入個 70 多 KB 的blazor.webassembly.js,再給你載入一個 200 多 KB 的dotnet.5.0.12.js,你品,你細品。再網頁點開了,業務邏輯計算程式碼被編成了 WebAssembly,UI 更新還得脫褲子放屁間接調 JS 去操控 DOM,你品,你細品。

但是,話說回來,話說說說回來,各位,捫心自問一下,如果一個 Web 產品,做到了峰值併發上萬,規模得有多大?而這樣的資料併發規模,我不知道你的後臺業務邏輯得有多複雜,即使複雜的要死,說難聽點,也就是加幾臺機器的問題,瓶頸如果有,也一定是在儲存層。

Blazor 生態第二大的問題是:沒有 UI 庫,沒有一個類似於 ant-d 的庫可以用,這是一個非常大的問題。對於創業團隊或者個人開發者來說,這個問題要比什麼狗屁效能問題大一萬倍。