主頁 > .NET開發 > Blazor 003 : Razor的基礎語法

Blazor 003 : Razor的基礎語法

2022-03-29 06:01:30 .NET開發

上文,我們通過剖析一個最簡單的 Blazor WASM 專案,講明白了 Razor 檔案是什么,以及它被轉譯成 C#后長什么樣子,也介紹了 Razor 中最簡單的一個語法:Razor Expression,也就是 Razor 運算式

本文將介紹兩個內容:

  • 首先我們將書接上文,再介紹一丁點 Razor 語法
  • 然后我們創建一個對等的 Blazor Server 專案看看事情有哪些變化,

另外請注意:上一篇文章中有很嚴重的錯誤,沒有及時看到訂正與更正的同學麻煩回去看一眼,

目錄

目錄
  • 目錄
  • 1. 基礎的 Razor 語法之二
    • 1.1 簡單的代碼塊 : 變數宣告與賦值
    • 1.2 惡心的代碼塊:面里加水水里加面無窮加下去
    • 1.3 在代碼塊中書寫函式
    • 1.4 代碼塊若是回圈和控制塊,可以有簡便寫法
    • 1.5 注釋
      • 關于注釋的無用知識點
    • 1.6 指令 Directives
      • 1.6.1 @namespace@using@attribute指令與@page指令
      • 1.6.2 @implements@inherits指令
      • 1.6.3 @code指令與@functions指令
      • 1.6.4 其它指令
    • 1.7 指令屬性 Directive Attributes
    • 1.8 小小的總結
  • 2. 徒手搓一個 Blazor Server 專案
    • 2.1 在創建專案之前需要進行的思考
    • 2.2 創建專案檔案與入口類
    • 2.3 創建pages/_Host.cshtml
    • 2.4 創建 Blazor 組件
    • 2.5 Blazor Server 真正的運行邏輯
      • 2.5.1 第一次 HTTP 請求
      • 2.5.2 第二次 HTTP 請求
      • 2.5.3 第三、四次 HTTP 請求
      • 2.5.4 websocket 連接
      • 2.5.5 blazor.server.js到底在哪里?

1. 基礎的 Razor 語法之二

這里我們接著上一篇文章的第三小節,繼續講一些 Razor 的基本語法,講解試驗程序依然使用上一篇文章創建的 Blazor WASM 專案演示,

這里再提前做個免責宣告:我們上一篇文章中提到過,Razor 作為一門標記語言,早在 Blazor 出現 之前就被 ASP .NET 使用,作為服務端渲染框架里用來描述 UI 的標記語言使用著,其歷史地位與 JSP 類似,如今又被 Blazor 框架拿來作 UI 描述語言,

但實際上,Razor 中會有一些使用細節,有些功能僅在 Blazor 下可用,有些功能又僅在 ASP .NET 場景下可用,也就是說,隨著檔案后綴從*.cshtml改成了*.razor,背后很多東西都發生了變化,最明顯的就是背后轉譯的 C#差異非常大:這很好理解,服務端渲染是要生成一個 HTML 檔案,而 Blazor 下是要生成一個類似于 V-Dom 的資料結構,

但我的系列文章并不會去介紹這些差異,我只保證:我所介紹的 Razor 語法、用法,一定在 Blazor 框架下是可用的,畢竟這是一個介紹 Blazor 框架的系列文章,

1.1 簡單的代碼塊 : 變數宣告與賦值

上篇文章我們介紹了Razor Expression,也就是運算式,那么再漸進一點:我們這次從運算式,升級到多行代碼,也就是代碼塊上.

代碼塊的語法非常簡單,就是把一行或多行代碼放在一個@{ }中去即可,比如我們把Index.razor改成下面這樣:

@page "/"

<h1>Hello, Razor!</h1>

@{
    var quote = "Today a reader, tomorrow a leader";
}

<h2>@quote</h2>

@{
    quote = "Always desire to learn something useful";
}

<h2>@quote</h2>

效果如下:

image

它背后的 C#類則變成了這樣:

public class Index : ComponentBase
{
  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>");
    string quote = "Today a reader, tomorrow a leader";     // !!!!
    __builder.OpenElement(1, "h2");
    __builder.AddContent(2, quote);
    __builder.CloseElement();
    quote = "Always desire to learn something useful";      // !!!!
    __builder.OpenElement(3, "h2");
    __builder.AddContent(4, quote);
    __builder.CloseElement();
  }
}

這非常明顯:我們通過兩個代碼塊在Index.razor中摻入的 C#代碼,是被原封不動的插在了Index類的BuildRenderTree方法中去了,

1.2 惡心的代碼塊:面里加水水里加面無窮加下去

我在上一篇文章開篇的時候就噴過 Razor 是一門丑陋的語言,那么現在,就請允許我向你們介紹第一個屎點:在 Razor 的代碼塊中,可以直接寫標記語言!比如我們把Index.razor寫成下面這樣:

@page "/"

<h1>Hello, Razor!</h1>

@{
    var quote = "Today a reader, tomorrow a leader";

    <h1>Here is a famous quote</h1>
}

<h2>@quote</h2>

@{
    quote = "Always desire to learn something useful";

    <h1>Here is another famous quote</h1>
}

<h2>@quote</h2>

它的效果如下:

image

是不是很震驚?上面的代碼中,我們先是在標記語言中通過@{ }的方式向其中摻了 C#,然后在本應當全是 C#的地方,又摻了兩句標記語言,,相當于水里加面再加水了屬于是,

雖然看著很惡心,但這個語法規則非常簡單:

  1. 標記語言內部要摻 C#,必須用@{}括起來
  2. C#內部要摻標記語言,直接寫就行了,Parser 主要是靠檢測 HTML 標簽判斷這是 C#代碼還是 HTML 代碼的
  3. 以上是可以嵌套的

在展示嵌套,也就是水里加面再加水再加面無窮盡也之前,我們先看一下上面的 Razor 代碼被轉譯成了什么樣子:

public class Index : ComponentBase
{
  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>");
    string quote = "Today a reader, tomorrow a leader";                     // !!!
    __builder.AddMarkupContent(1, "<h1>Here is a famous quote</h1>");       // !!!
    __builder.OpenElement(2, "h2");
    __builder.AddContent(3, quote);
    __builder.CloseElement();
    quote = "Always desire to learn something useful";                      // !!!
    __builder.AddMarkupContent(4, "<h1>Here is another famous quote</h1>"); // !!!
    __builder.OpenElement(5, "h2");
    __builder.AddContent(6, quote);
    __builder.CloseElement();
  }
}

也就是說,在代碼塊中摻入的標記語言,在轉譯時其實整行是被轉譯成了AddMarkupContent

現在!!請全體起立,觀看下面的代碼:

@page "/"

<h1>Hello, Razor!</h1>

@{
    var quote = "Today a reader, tomorrow a leader";

    <h1>
        Here is a famous quote:
        @{
            quote = "Always desire to learn something useful";
            <p>@quote</p>
        }
    </h1>
}

這就很讓人無語,像 JSX 和 TSX 雖然大家也是摻著寫,但能這樣摻著寫的,也就 Razor 獨一家了,

它的效果如下:

image

上面的代碼被轉譯成了下面這樣:

public class Index : ComponentBase
{
  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>");
    string quote = "Today a reader, tomorrow a leader";
    __builder.OpenElement(1, "h1");
    __builder.AddMarkupContent(2, "\r\n        Here is a famous quote: \r\n");
    quote = "Always desire to learn something useful";
    __builder.OpenElement(3, "p");
    __builder.AddContent(4, quote);
    __builder.CloseElement();
    __builder.CloseElement();
  }
}

我為什么之前在上一篇文章里說,要理解 Razor 的語法,就需要看一眼轉譯后的 C#代碼,原因就在這里:通過對比 Razor 和 C#代碼,我們能觀察出一個非常有意思的現象:在 Razor 代碼中,我們書寫上寫出了一種嵌套的效果,但實際上轉譯后的 C#代碼中,并沒有這種嵌套 的效果,

也就是說,@{xxx}這種語法形式,雖然寫出來有一對大括號,但這個大括號并不代表程式邏輯上有任何嵌套或者遞回呼叫,這種語法形式僅僅起到了提醒 Parser 的作用

你仔細想這個道理,如果你是 Razor 語言 Parser 的作者,要挑出摻在 C#代碼中的標記語言是非常簡單的:

  • 決議程序中碰到<xxx>,然后其中的xxx正好是一個合法的 HTML 元素,后續看下去還存在一個對等的</xxx>,那么肯定沒跑了,這一段就是標記語言,直接轉換成AddMarkupContent()就行了

但想要挑出摻在標記語言中的 C#代碼,在沒有特殊標記的前提下,是不可能的事情,而@{ }這種語法,這一對括號,僅僅就是提供給 Parser 看的一個記號、一個標記,用來提示 Razor 引擎:這里寫的是 C#代碼,

再強調一點:@{ }這一對括號,并不意味著代碼邏輯上有任何嵌套呼叫關系,它僅僅是一個標記而已,

另外再有一個小知識點:我們上面說了,從 C#代碼里分辨摻進去的標記語言代碼,不依靠任何外部標記,僅憑借標記語言自身的 XML Tag 就可以分辨,但,如果,你就真的想單純的想讓一行字串以標記語言被 Parser 識別的話,怎么做?

這時候就必須引入一個外部標記了,Razor 提出的解決辦法是:

  • @:為標記,從這個標記開始直到這一行行尾,Parser 將會強行將這部分內容識別為標記語言
  • 如果需要標識多行,則每一行都必須添加@:這個標記

作為一門 UI 描述語言,Razor 這樣設計好不好,輪不到我評價,我也沒這個資格,但是,對于 Razor 的學習者而言,如果理解不到上面幾段話表達的觀點,那么,他將很難理解、閱讀這種互相摻來摻去的代碼,,而很不幸的是,Razor 的學習者中,能去觀察轉譯后 C#代碼的人,非常少,

1.3 在代碼塊中書寫函式

在介紹這個 Razor 特性之前,我先要介紹一下 C#中的一個語言特性,叫local function,簡單來說,C#允許你在方法或函式內部創建一個臨時函式,一個簡單的例子如下:

public class Program
{
    public static void Main(string[] args)
    {
        void Print(string str)
        {
            Console.WriteLine(str);
        }

        Print("Foo");
        Print("Bar");
    }
}

這段代碼會在控制臺輸出兩行:

Foo
Bar

看到這里你可能覺得沒什么神奇的,還不如寫成var Print = (string str) => {Console.WriteLine(str);};這樣來得方便,但 local function 出彩的地方在于,它的宣告和定義是會被自動提前的,所以下面的代碼也是合法的:

public class Program
{
    public static void Main(string[] args)
    {
        Print("Foo");
        Print("Bar");

        return;

        void Print(string str)
        {
            Console.WriteLine(str);
        }
    }
}

你甚至可以把 local function 定義在return陳述句之后,

但其實說穿了,截至現在,依然沒什么特別神奇的地方,你可能會想:哈,這有什么卵用?純純的語法糖?就提前宣告嗎?那不還是 lambda 運算式嘛!

你的想法是正確的,普通的 local function 也可以捕獲變數,確實就是 Lambda 運算式+提前宣告,

但是,local function 可以添加 static 關鍵字,加了 static 關鍵字之后,local function 就不會捕獲任何變數了,就變成了一個純純的函式,純的像你大二學 C 語言時寫的函式一樣,

常用 Lambda 的人基本都遇到過,Lambda 函式體內因為不小心捕獲了一個不該被捕獲的變數,從而寫出了一個非常難以排查的 Bug,這種情況下,你就應當使用 static local function

現在聊回 Razor 來:在 Razor 的@{}代碼塊中,你也可以定義函式,有時候你定義的函式會被轉譯成普通的 local function,有時會被轉譯成 static local function,

當你寫的函式就單純的是一個函式時,它會被轉譯成 static local function,比如下例

@page "/"

<h1>Hello, Razor!</h1>

@{
    string ConvertToUpperCase(string str)
    {
        return str.ToUpper();
    }

    var quote = "Today a reader, tomorrow a leader";
    quote = ConvertToUpperCase(quote);
}

<p>@quote</p>

轉譯后長這樣:

public class Index : ComponentBase
{
  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>");
    string quote = "Today a reader, tomorrow a leader";
    quote = ConvertToUpperCase(quote);
    __builder.OpenElement(1, "p");
    __builder.AddContent(2, quote);
    __builder.CloseElement();
    static string ConvertToUpperCase(string str)
    {
      return str.ToUpper();
    }
  }
}

而如果你寫的函式,內部摻了標記語言的話,就會被轉譯成一個普通的函式,因為這種情況下你寫的函式需要捕獲外部變數__builder,比如下面這個例子:

@page "/"

<h1>Hello, Razor!</h1>

@{
    void ConvertToUpperCase(string str)
    {
        <p>@str.ToUpper()</p>
    }

    var quote = "Today a reader, tomorrow a leader";
    ConvertToUpperCase(quote);
    quote = "Always desire to learn something useful";
    ConvertToUpperCase(quote);
}

它被轉譯后變是一個普通的 local function,如下:

public class Index : ComponentBase
{
  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>");
    string quote = "Today a reader, tomorrow a leader";
    ConvertToUpperCase(quote);
    quote = "Always desire to learn something useful";
    ConvertToUpperCase(quote);
    void ConvertToUpperCase(string str)
    {
      __builder.OpenElement(1, "p");
      __builder.AddContent(2, str);
      __builder.CloseElement();
    }
  }
}

local function 和 Razor 結合在一起,可以允許我們在區域小范圍內創建出一些 util 函式,來非常容易的描述諸如串列、表格這種視覺效果,這個知識點在正式開發生產中會非常頻繁的被用到,

要真正理解掌握這個知識點,前提是要理解 local function,再就是要理解標記語言和 C#互相摻著寫的真相,

1.4 代碼塊若是回圈和控制塊,可以有簡便寫法

我們上面講了,代碼塊的起止標記是@{},也理解了這一對括號其實僅起個標記作用,也就是說要給 Parser 一種能方便的分辨當前代碼到底是標記語言還是 C#代碼的辦法,

明確以上這個關鍵知識點,我們現在來看一個for回圈塊,假如我們把Index.razor改寫為如下:

@page "/"

<h1>Hello, Razor!</h1>

@{
    void ConvertToUpperCase(string str)
    {
        <p>@str.ToUpper()</p>
    }

    string[] quotes = {
        "Today a reader, tomorrow a leader",
        "Always desire to learn something useful"
    };
}

<h2>Here are some quotes:</h2>

@{
    for(int i = 0; i < quotes.Length; ++i)
    {
        ConvertToUpperCase(quotes[i]);
    }
}

具體什么效果就不用我多說了,代碼非常簡單易懂,我們主要來看上面代碼中的for回圈塊:它是一個由@{}圍起來的代碼塊,它內部僅包含一個for回圈,

對于這種內部僅包含一個回圈、或邏輯判斷代碼的代碼塊而言,它們有簡便寫法,如下所示:

@page "/"

<h1>Hello, Razor!</h1>

@{
    void ConvertToUpperCase(string str)
    {
        <p>@str.ToUpper()</p>
    }

    string[] quotes = {
        "Today a reader, tomorrow a leader",
        "Always desire to learn something useful"
    };
}

<h2>Here are some quotes:</h2>

@for(int i = 0; i < quotes.Length; ++i)
{
    ConvertToUpperCase(quotes[i]);
}

之所以這樣行得通,是因為 Parser 可以通過@for, @if, @switch, @foreach這些帶了標記的關鍵詞,以及這些回圈、或邏輯判斷代碼自身的大括號,就足以分辨出,這是一坨 C#代碼了,

同理,下面都是合法的代碼塊

@page "/"

@foreach(...)
{

}

@for(...)
{

}

@if(...)
{

}
else if(...)
{

}
else
{

}

@switch()
{
  case xx:
   ...
   break;
  case yy:
   ...
   break;
  default:
   ...
   break;
}

實際上,除了回圈、邏輯判斷代碼塊可以有這種簡便寫法,還包括using 塊, try-catch塊,lock塊等,滿足一個關鍵字+一對大括號形式的 C#代碼塊,均可以使用上述簡便寫法

1.5 注釋

標記語言有注釋,格式是<!-- comment -->,C#也有注釋,要么是// comment形式的單行注釋,要么是/* comments */形式的多行注釋,

Razor 則是標記語言和 C#的結合體,它依然符合直覺:

  • 在標記語言區域寫<!-- comment -->注釋,沒毛病
  • 在 C#語言區域寫 C#注釋,也沒毛病

這看起來沒什么問題,但有一個挺蛋疼的問題:假如你在開發程序中,想通過注釋的方式臨時洗掉一大塊代碼區域的話,而又恰巧這一大塊代碼區域既包括標記語言,也包括 C#代碼的話,

就不太好做,

所以 Razor 提供了第三種注釋方式:使用@* comments *@形式的多行注釋,

雖然我們并不知道 Razor 引擎內部的實作細節,但我建議你以下面的邏輯來理解這三種注釋:

  1. Razor 引擎在 Parser 作業之前,會先把@* comments *@形式的注釋移除掉
  2. 在 Parser 作業中,當決議到標記語言時,再去移除標記語言注釋,當決議到 C#代碼時,再去移除 C#代碼注釋

上面的描述也可能是錯的,上面的理解也其實沒有任何實際意義,,上面也僅是我的個人建議,,

關于注釋的無用知識點

  1. 在傳統的 ASP .NET Core 應用中,Razor Page 經服務端渲染后會攜帶標記語言注釋,也就是說標記語言注釋并不會被 Razor 引擎移除
  2. 無論是服務端渲染的 Razor Page,還是 Blazor 應用中的 Razor Page,其實 C#代碼注釋也不會被 Razor 引擎移除 -- 記得我們之前說的,Razor Page 檔案會被轉譯成一個 C#檔案嗎?如果你將.Net 版本降低到 5.0 及其以下,你會在obj\Debug\net5.0\Razor目錄下看到轉譯后的 C#檔案,里面完整的保留了所有 C#注釋
  3. 以上兩種行為,隨著.NET 版本的變遷,都有可能發生變化,畢竟,無論是 HTML 檔案中保留著標記語言注釋,還是轉譯后的 C#代碼檔案攜帶注釋,這些注釋都最終不會出現在瀏覽器的視覺渲染效果里,
  4. 但可以非常確定的是,@* comments *@這種注釋,是一定會被 Razor 引擎移除掉的,你絕對不會在除了源代碼檔案之外的任何地方再看到它

1.6 指令 Directives

前面我們介紹了代碼塊和運算式,它們的語法分別是@{...}@(...)(括號在無歧義的情況下可省略),現在我們要介紹另外一種特殊的東西,它被稱為指令,指令通常用會改變 Razor 引擎對整個*.razor檔案的處理方式或細節

換句話說,*.razor本質上是一個 C#類,指令則是對這整個 C#類的一些額外補充:這里要特別強調,它作用的受體,是整個 C#類,是整個*.razor檔案

指令的語法也比較簡單,還是由猴頭符號@開頭,然后跟著一個特定的關鍵字,比如attributecode,不同的關鍵字代表不同的指令,再然后,按不同的指令,可能會附加一個由{}括起來的多行代碼塊,或直接附加單行內容

1.6.1 @namespace@using@attribute指令與@page指令

這四個都是單行指令,

@namespace指令很容易理解,就是給 C#類宣告名稱空間,默認情況下*.razor轉譯后的類會被存放在專案的<RootNamespace>下,

  • 額外知識:
    我們上節課說過了*.csproj檔案是.Net 專案的編譯腳本檔案,在這個檔案中有個 XML 元素叫做<PropertyGroup>,這個元素下會定義形形色色的值來向編譯器或后續的工具鏈傳遞一些資訊,

    比如我們的示例程式中,就定義了一個<TargetFramework>屬性,其值net6.0就是在告訴工具鏈:這是一個面向.NET 6.0 版本的程式,請在編譯、鏈接時使用 6.0 版本的 SDK

    這些值被稱為專案的Properties,除了我們顯式寫出來的<TargetFramework>這個 Property,工具鏈還會自動的宣告一些其它的 Property 并賦予它們一些默認值,最重要的值有兩個:<AssemblyName><RootNamespace>

    根據名字很容易理解:前者是編譯出來的 dll 的名字,后者是該 dll 中的根 namespace 的值,

    通常情況下,這兩個值都默認為*.csproj檔案的檔案名,比如我們用到的HelloRazor.csproj,這兩個 Property 的值均為HelloRazor,這意味著:

    1. 編譯出來的產物的名字叫HelloRazor.dll
    2. 工具鏈自動生成的一些類,將放置于HelloRazor這個 namespace 下

    注意:對于顯式的,寫在*.cs檔案中的 C#代碼直接定義的類,<RootNamespace>并不起任何作用,

@using指令很容易理解,就是一個 C#中的using陳述句,它的作用也和 C#中位于腦門上的using陳述句一樣:參考一個 namespace

@attribute指令也很簡單,它的作用和 C#類定義腦門上的[xxx]是一樣的:給整個類附加一個 Attribute,

@page指令其實是一個特殊的@attribute指令,它相當于在 C#類腦門上添加了一個[Route("...")],下面這個例子一次性的把三個指令都給大家展示了出來

看下面這個例子,我們把Index.razor改寫成下面這樣:

@namespace HelloRazorAgain
@using Microsoft.AspNetCore.Authorization

@page "/"
@attribute [Authorize]

<h1>Hello, Razor!</h1>

它轉譯后的 C#類就會長這樣:

image

注意:雖然專案名稱是HelloRazor,但我們通過@namespace指令將Index類放在了HelloRazorAgain這個 namespace 下,注意看AppProgram還處于HelloRazor namespace 下,而其中:

  1. Program處于HelloRazor下是因為 C#代碼中顯式的寫了namespace HelloRazor;
  2. App處于HelloRazor下是因為 Blazor 引擎默認使用<RootNamespace>作為 namespace,即是HelloRazor
  • 額外知識點:輸出查看專案中的 Property 的值

    *.csproj中定義一個Target,然后使用Message這個 Task 來輸出就可以了,如下:

    <Target Name="Log">
      <Message Text="RootNamespace = $(RootNamespace)" />
      <Message Text="AssemblyName = $(AssemblyName)" />
    </Target>
    

    然后在控制臺使用dotnet build -t:Log就可以執行上面定義的名為Log的 Target,不過由于 dotnet 包裹的 msbuild 默認情況下并不顯示普通日志資訊,所以需要顯式的將輸出的 verbosity 指定為 normal 才可見,所以需要使用dotnet build -t:Log -v:normal,你才會看到如下的輸出 :

    image

    如果你對上面這個額外知識點中描寫的內容一頭霧水,你有兩個選擇:忽略它,或者去學習一下有關MSbuild的相關知識

1.6.2 @implements@inherits指令

這兩個指令也是單行指令,一個用來表達繼承介面,一個用來表達繼承類,如下例所示:

@implements System.Runtime.Serialization.ISerializable
@implements System.IDisposable
@inherits Microsoft.AspNetCore.Components.ComponentBase

@page "/"

<h1>Hello, Razor!</h1>

@code {
    public void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        throw new NotImplementedException();
    }
}

我們先忽略掉那個@ code { ... }代碼塊,先看指令,上面的代碼轉譯后會變成:

[Route("/")]
public class Index : ComponentBase, ISerializable, IDisposable
{
  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor!</h1>");
  }

  public void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    throw new NotImplementedException();
  }

  public void Dispose()
  {
    throw new NotImplementedException();
  }
}

需要注意的是:

  1. 這兩個指令都是單行指令,且每一行指令后面只能跟一個類,或介面名
  2. 在一個*.razor檔案中,@inherits指令至多只能出現一次,@implements指令可以出現多次:這很容易理解,因為 C#里一個類只能繼承自一個父類,但可以同時實作多個介面

但是這里面有個非常怪的問題:對于 Blazor 組件類來說,必須繼承自Microsoft.AspNetCore.Componenets.ComponentBase,這就意味著正常情況下,@inherits指令是一句沒用的指令,你只有兩種選擇:

  1. 不寫 @inherits指令,這樣 Razor 引擎會自動讓這個類繼承于ComponentBase
  2. @inhertis指令,這樣就只能寫@inherits ComponentBase

1.6.3 @code指令與@functions指令

@code@functions兩個指令在 Blazor 語境下,是完全相同的一個東西,你可以理解為同一個指令,有兩個不同的名字,當然這背后有一定的歷史原因,

  • @code/functions是多行指令,它后面跟一對大括號,大括號里寫一些欄位、屬性和方法定義
  • 這些欄位、屬性和方法將變成 C#類的成員

這里一定要注意區分@code/functions代碼塊運算式的區別,最大的區別是:前者是在向類添加成員,后者是在向類的BuildRenderTree方法中添加內容

我們在上面其實已經展示 了@code的用法與實際效果,這里就不再重復了

1.6.4 其它指令

以上就是開發中最常用到的一些指令,當然不包括全部,我們沒有介紹到的指令還包括@preservewhitespace, @layout, @inject等,其中@layout還是一個非常重要的指令,但我們不在這里介紹這些指令,而是會在后續文章,介紹到相應的知識點時,再去做介紹,

1.7 指令屬性 Directive Attributes

上一小節說過了,指令是對整個 C#類進行修飾、變更的一些手段,Razor 引擎會按照指令的指引去處理背后的 C#類,

而這一小節我們要介紹的,另外一個新的語法內容,叫指令屬性: directive attributes,其中屬性 attribute是本意,指的是標記語言中的attribute,就像<a>標簽的href 屬性指令 directive是名詞性形容詞,

也就是說,指令屬性是一些要應用在 HTML 元素上的特殊屬性: attribute,但這些屬性在原生 HTML 規范中是不存在的,并且為了區別于自定義屬性,這些特殊的指令屬性也是以猴頭符號@做標記的,

指令屬性是 Razor Page 在 Blazor 場景下獨有的語法特性,今天在這里我們僅介紹一個指令屬性:@on{EVENT},它幾乎是所有指令屬性中最重要的一個,而其它的指令屬性,我們將在后續碰到的時候再去做介紹,

越說越亂,我們直接來看一個例子:我們先將Index.razor改寫成下面這樣:

@page "/"

<h1>Hello, Razor! Below is a simple click counter</h1>

@code {
    public int Count{get;set;} = 0;
}

<button>Increase Count</button>

<h2>Count: @Count</h2>

有了前面的知識,我們很容易能看懂上面的代碼在干什么:

  1. 我們通過@code{}指令給背后的 C#類宣告了一個int型別的屬性Count,并置其初始值為 0
  2. 我們用標記語言寫了一個按鈕,但目前這個按鈕沒有任何互動功能
  3. 我們通過隱式運算式,將Count的值展示在了頁面上,顯然,這個值在目前恒定的會顯示為 0

接下來,我們希望讓用戶每點擊一次按鈕,就讓屬性Count自增 1,那么就需要做兩件事:

  1. 我們寫一個成員方法,每次該成員方法被呼叫,屬性Count都會自增 1
  2. 最關鍵的是:我們要把這個成員方法,與按鈕的點擊事件關聯起來:這里就是指令屬性發光發熱的地方

那么,我們把代碼改寫成下面這樣,應該就可以了嗎?

@page "/"

<h1>Hello, Razor! Below is a simple click counter</h1>

@code {
    public int Count{get;set;} = 0;

    private void IncreaseCount()
    {
        Count++;
    }
}

<button @onclick="IncreaseCount">Increase Count</button>

<h2>Count: @Count</h2>
  1. 我們首先是創建了一個方法
  2. 其次,我們在<button>標簽內,使用@onclick指令屬性,將回呼方法與按鈕的點擊事件系結在一起

看起來沒有任何問題,但編譯、運行,你會發現:點擊按鈕后,計數依然是 0,沒有任何變化

這是怎么回事?我們來翻看一下轉譯后的 C#代碼,如下:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

[Route("/")]
public class Index : ComponentBase
{
  public int Count { get; set; } = 0;


  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor! Below is a simple click counter</h1>\r\n\r\n\r\n");
    __builder.AddMarkupContent(1, "<button @onclick=\"IncreaseCount\">Increase Count</button>\r\n\r\n");
    __builder.OpenElement(2, "h2");
    __builder.AddContent(3, "Count: ");
    __builder.AddContent(4, Count);
    __builder.CloseElement();
  }

  private void IncreaseCount()
  {
    Count++;
  }
}

看來,指令屬性 @onclick并沒有被正確決議處理,而是直接以文本形式輸出到了渲染結果中去,這是怎么回事呢?

原因在于:Blazor 引擎雖然知道@onclick長的像一個指令屬性,但它找不到這個指令屬性的定義,于是降級將其視為普通的自定義屬性去處理了,那么怎么樣才能讓 Blazor 引擎找到@onclick的定義呢?

答案是:要使用@using指令引入正確的 namespace,下面是正確答案

@using Microsoft.AspNetCore.Components.Web
@page "/"

<h1>Hello, Razor! Below is a simple click counter</h1>

@code {
    public int Count{get;set;} = 0;

    private void IncreaseCount()
    {
        Count++;
    }
}

<button @onclick="IncreaseCount">Increase Count</button>

<h2>Count: @Count</h2>

通過引入Microsoft.AspNetCore.Components.Web這個名稱空間,Blazor 引擎才能找到@onclick的真身,從而轉譯后的 C#代碼將長下面這樣:

using System;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;

[Route("/")]
public class Index : ComponentBase
{
  public int Count { get; set; } = 0;


  protected override void BuildRenderTree(RenderTreeBuilder __builder)
  {
    __builder.AddMarkupContent(0, "<h1>Hello, Razor! Below is a simple click counter</h1>\r\n\r\n\r\n");
    __builder.OpenElement(1, "button");
    __builder.AddAttribute(2, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, new Action(IncreaseCount)));
    __builder.AddContent(3, "Increase Count");
    __builder.CloseElement();
    __builder.AddMarkupContent(4, "\r\n\r\n");
    __builder.OpenElement(5, "h2");
    __builder.AddContent(6, "Count: ");
    __builder.AddContent(7, Count);
    __builder.CloseElement();
  }

  private void IncreaseCount()
  {
    Count++;
  }
}

引入名稱空間前后,轉譯的 C#代碼有了兩點變化

  1. 首先就是引入 了Microsoft.AspNetCore.Components.Web 這個 namespace
  2. 其次是@onclick屬性被正確的轉譯成了__builder.AddAttribute(2, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this.new Action(IncreaseCount)))

現在,表面問題解決了,但有一個疑惑:為什么非要參考M.A.Components.Web

我無法向你提供一個完美的解釋,我有一個蹩腳的解釋,很有可能是錯誤的:因為click是一個滑鼠事件,而MouseEventArgs是定義在M.A.Components.Web這個 namespace 下的,

事件系結與資料系結是另外兩個非常重要的話題,在這個小節里,我們僅僅是向大家展示了什么叫指令屬性,重點在于指令屬性,而不在事件系結上,

后續我們會在介紹事件系結時介紹更多的細節,包括如何將滑鼠事件的引數傳遞給回呼方法等內容

1.8 小小的總結

下表小小的總結回顧了 Razor 的基礎語法內容:

語法名稱 語法 備注
運算式 @xxx@(xxx) 轉譯后位于BuildRenderTree方法內部
簡單代碼塊 @{ ... } BuildRenderTree方法內部宣告變數、執行邏輯
代碼塊中摻標記語言 @{ <tag>...</tag>}@{ @:xxx } 被轉譯成__builder.AddMarkupContent()
要特別理解代碼塊的括號并不代表嵌套遞回呼叫,它只是個標記
代碼塊中書寫函式 @{ xxx method(xxx xxx) { ... }} 被轉譯成BuildRenderTree方法內部的 local function
代碼塊的簡寫形式 @for(xxx){}@if() {...} 只是個語法糖而已,轉譯后依然處于BuildRenderTree方法內部
注釋 @* xxx *@ 標記語言注釋與 C#注釋到底是怎么被處理的,不同版本的.NET 可能行為不一樣,但@* xxx *@式的注釋,是一定會被移除的
單行指令 @namespace, @using, @attribute, @page, @implements, @inherits 用來修飾整個類
多行指令 @code, @functions 用來給類中添加成員定義
指令屬性 @onxxx 是一種特殊的屬性 attribute@onxxx等事件指令屬性在使用時需要引入 namespace Microsoft.AspNetCore.Components.Web,否則 Blazor 引擎不能正確識別指令屬性

2. 徒手搓一個 Blazor Server 專案

2.1 在創建專案之前需要進行的思考

在創建專案之前,先在心里回顧一下 Blazor Server 的作業方式:

  • 服務端渲染,瀏覽器只是個視覺樣式渲染器,Blazor Server 其實是一個 C/S 架構的遠程 App

也就是說,Blazor Server App 是一個運行在服務端的 App,這個 App run 起來之后,要做兩件事:

  1. 對于首次用戶瀏覽器訪問的 HTTP 請求:Blazor Server App 要處理這個 HTTP 請求,然后回傳一些相關的 HTML&JS(或可能還包含其它)檔案,而用戶的瀏覽器收到這個回應后,會執行其回傳的一些 JS 代碼,這些 JS 代碼最重要的一個功能是:使瀏覽器與 Blazor Server App 之間建立一個 SingnalR 長連接,
  2. 后續用戶在瀏覽器頁面中點點按按,凡事涉及互動更新,瀏覽器中的 JS 代碼都會把用戶的動作通過 SignalR 傳遞給服務器,然后服務器會做相應的運算,再把視覺更新的訊息傳遞給瀏覽器,瀏覽器按命令更新渲染就可以

這里我們再把這張圖貼出來一次:

blazor_server

所以,有以下思路

  1. 創建一個 Asp .Net Core 應用:
    既然用戶的首次請求還是傳統的 HTTP,并且服務端是要回傳一些靜態資源的(首次需要回傳的 HTML 檔案、JS 代碼與 CSS 檔案),那么在.NET 技術堆疊中,顯然這就是一個典型的 Asp .Net Core 專案應該做的事:
  2. 沿著上面的思路,我們能想出,這個 Asp .Net Core 應用至少應當做兩件事:
    • 用傳統的 Asp .Net 技術給用戶的第一次訪問回傳 HTML 檔案與 JS 代碼
    • 用新的技術接管后續的 SignalR 連接,與處理連接中發送的資料

注意,SignalR 是一個特別面向 Web 前后端的網路連接庫,旨在為前后端提供一個可靠的全雙工通信鏈路,它優先使用WebSocket協議,但當 WebSocket 由于種種原因不可用時,也會切換到其它通信協議上去,簡而言之,它其實是一個六層協議(通常我們把以太網稱為二層網路,IP 稱為三層網路,TCP/UDP 稱為四層網路協議,HTTP/SMTP/WebSocket 稱為五層協議)

為了后面描述方面,也為了降低大家的心智負擔,我在這里建議大家,直接將 SignalR 理解為一個平行于 HTTP 協議的網路通信協議,它是全雙工的長連接,這個理解是錯誤的,槽點很多,但還是那句話:是,我知道,我這是在簡化問題,先隱去一些細節,目的是之后展示更大的圖景,

現在思考的差不多了,我們創建一個名為HelloRazorAgain的目錄,然后開干吧!

2.2 創建專案檔案與入口類

有了上面的思路,那么這次的HelloRazorAgain.csproj就很好寫了

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>

是的,就這么簡潔,沒有宣告任何一個包的依賴,是因為第一行<Project Sdk="Microsoft.NET.Sdk.Web">已經把 Web 開發全家桶里所有可能涉及到的包都給你引進來了,

然后我們再來創建入口類Program.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace HelloRazorAgain;


public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddRazorPages();
        builder.Services.AddServerSideBlazor();

        var app = builder.Build();

        // configure HTTP request pipeline
        app.UseStaticFiles();
        app.UseRouting();
        app.MapBlazorHub();
        app.MapFallbackToPage("/_Host");

        app.Run();
    }
}

這個入口函式較 Blazor WASM 來說稍復雜一些,我們一句一句過:

  1. var builder = WebApplication.CreateBuilder(args)

    創建了一個WebApplicationBuilder實體:這個很好理解,即便我們只需要處理第一次的 HTTP 請求,我們還是需要寫一個 Web 應用

    對于不熟悉.Net 平臺的同學來說,這里補充一點額外知識

    • 這種創建 HostBuilder,再添加各種 Services,再通過hostBuilder.build()創建 Host,最后再將Host Run 起來的寫法,是.Net 平臺創建復雜應用的一個標準寫法

    • Host 是一個高級抽象概念,簡單來說,就是業務邏輯代碼的運行環境與周邊配套,以開發一個 MVC Web 應用為例來闡述的話,業務邏輯代碼指的是各種Controller里的方法,以及它們呼叫的各種其它類中的方法,而運行環境與配套,指的東西就多了去了,包括但不限于:

      • 處理網路連接、請求的相關代碼
      • 日志管理模塊
      • 配置管理模塊
      • 靜態檔案管理模塊
      • 用戶登錄鑒權的相關模塊代碼
      • 甚至于,將用戶 HTTP 請求中的路徑,映射到某個 Controller 中某個 Action 的這部分代碼,也就是路由,也算是運行環境與配套
    • 所以,當我們要寫 Web 應用時,我們需要創建一個“WebHost”,當我們需要寫一個 7*24 小時運行的后臺監控程式時,我們需要創建一個“DaemonHost”,

    • .NET 官方為了廣大的程式員不要總重復性的作業,就專門為 Web 開發領域,定義且實作了一個IHost的具體類:WebApplication,而這個類的實體化程序使用了設計模式中的 Builder 模式,所以它配套也有一個WebApplicationBuilder的定義與實作,隨著.Net 版本的變遷,這個特定于 Web 開發場景的IHost具體類,與之配套的Builder類的名字也有變化,WebApplicationWebApplicationBuilder是進化到.Net 6.0 后官方推薦使用的套件,,我們用腳趾頭也能想到,這套官方實作的套件中,默認至少包含三個東西:

      • 處理網路的相關代碼,這里就藏著對 Kestrel 庫的使用
      • 配置管理相關代碼:這就是為什么默認情況下我們能從app.settings.json讀取配置的原因
      • 日志管理相關代碼:這就是為什么你一行日志代碼都沒寫,但控制臺會輸出日志的原因
        這些一個個的功能,就是所謂的Service,上面提到的三個功能,是.NET 官方認為“但凡你做 Web 開發就肯定會用到,所以我就給你默認添加進WebApplication中的功能”,但還有更多的功能,.NET 官方只提供了,但沒有默認塞進WebApplication

      顯然,我們也可以在官方提供的各種 Service 不能滿足我們需求時,自己寫一些個性化Service添加進去

    • 所以,我們需要先按 Builder 模式,創建一個WebApplicationBuilder,然后再按需求添加我們用得到的各種 Services,最后再呼叫WebApplicationBuilder.Build()把這個 Host 創建出來:也就是WebApplication實體

  • AddRazorPages()

    這是一個歷史很悠久的 Service:RazorPage,在 ASP .NET 服務端渲染時代,我們前兩篇文章也提到了,那時候.NET 技術堆疊就在使用 Razor Page,這個 Service 的作用就是在向當前WebApplication添加 Razor 引擎相關能力

    但請注意:這和 Blazor 是沒什么關系的,所謂的添加 Razor 引擎相關能力,指的是老式的 ASP .NET 的服務端渲染作業方式:當用戶的請求能被匹配到某個*.cshtml檔案時,服務端要根據*.cshtml檔案中的內容,給客戶端生成一個 HTML 檔案作為 HTTP 回應,

    添加這個功能,是為了處理用戶的第一次 HTTP 請求

  • AddServerSideBlazor()

    這句代碼,才是 Blazor Server 的靈魂,,,的 7 成,

    這句代碼的作用,是在向當前的WebApplication添加:接收、管理客戶端通過 JS 代碼發起的 SignalR 連接的能力,添加這個功能,是為了處理用戶的后續通過 SignalR 發送的資料,并在服務端互動邏輯計算完畢后,再通過 SignalR 連接把 UI 更新的訊息傳遞給用戶瀏覽器

  • var app = builder.Build()

    創建了WebApplication實體,

    • 這里再補充一些額外知識,對 ASP .Net Core 基礎知識不了解的可以看一看:

      我們在上面講了Host模式,那么截至目前,我們已經創建好了一個IHost實體,它的型別是WebApplication,那么下一步是不是就可以直接調這個實體的Run方法了呢?

      不,別著急,對于 Web 應用來說,創建 Host 的整個程序,只不過是注冊了可能用到的各種 Services而已,但對 Web 應用來說,更重要的問題是:當一個網路請求來臨時,這些 Service 應當在何種組織與調度下去執行,并最終生成一個 HTTP 回應?

      你可能脫口而出:那我制定一個順序就行了,比如先記錄日志,再做認證鑒權,再做路由,再處理請求等等,

      但你還是想簡單了,.NET 的設計者想的比你更深入一層,他們發明了一個概念:中間件(Middleware)

      之所以如此設計,是因為Service這個概念其實是一個更細粒度的概念,將 Service 看作是函式的話,簡單來說,一個個Service的輸入與輸入并不是 HTTP 請求和 HTTP 回應,比如日志管理的 Service,它的輸入就是字串,它的“輸出”就是把日志寫到你配置好的某個地方,

      而中間件其實是在這些 Service 之上,一個個更復雜的“函式”,中間件的輸入有兩種:要么是 HTTP 請求,要么是另外一個中間件的輸出結果,中間件的輸出有兩種:要么是 HTTP 回應,要么是另外一個中間件的輸入,

      我們要制定的順序,其實是中間件的執行順序,如下圖所示:

      image

      像上圖那樣,配置好的中間件的順序,串起來像一條流水線,就叫 pipeline,HTTP 請求嚴格按照順序從 pipeline 一個一個中間件的走,如果中途沒有錯誤發生,那么它們將最終走到一個叫endpiont的中間件上去,endpoint中間件將根據 App 的配置,要么去執行某個 Controller 中的某個方法(經典 MVC),要么去渲染某個*.cshtml(如我們上面的Program.cs所示)

      image

      也就是說,下面我們要配置 pipeline 了

  • app.UseStaticFiles(); : 處理靜態檔案請求的中間件

  • app.UseRouting(); : 路由中間件

  • app.MapBlazorHub(); : 可以簡單的理解為,建立 SignalR 連接的中間件,這,就是 Blazor Server 的靈魂剩下的那三成,

  • app.MapFallbackToPage("/_Host"); : 渲染pages/_Host.cshtml

    這就是一個特殊的endpoint中間件,它固定的渲染唯一的一個頁面:pages/_Host.cshtml

    好了,pipeline 配置結束了,最后,Run 起來

  • app.Run();

現在把代碼讀完,整體邏輯就非常清晰了:

  1. 用戶的第一次請求,是一個 HTTP 請求,將走完整個 pipeline,干了兩件重要的事
    1. 向瀏覽器回傳了渲染好的pages/_Host.cshtml
    2. 服務端與瀏覽器之間建立了 SignalR 連接
  2. 用戶的后續點、按、輸入,都將通過上面建立好的 Signal 連接與服務端互動資料

這里我先告訴你,上面的描述,是錯的,事實沒有那么簡單,后面介紹完代碼后,我們會再捋一次流程,不過就目前為止,請先按上面的簡化描述理解著,

2.3 創建pages/_Host.cshtml

我們上面說了,初次要給用戶回傳一個初始頁面,并且在閱讀Program.cs代碼時,我們也知道了這個初始頁面其實是一個 Razor Page 的服務端渲染結果,下面就是這個pages/_Host.cshtml的內容:

@page "/ThisIsAMeanlessDirective"

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Hello Razor Again</title>
    <base href="https://www.cnblogs.com/" />
</head>
<body>
    <component type="typeof(HelloRazorAgain.App)" render-mode="ServerPrerendered" />

    <script src="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/28/_framework/blazor.server.js"></script>
</body>
</html>

我再次重申一遍:這個檔案雖然也是 Razor Page,但它和 Blazor 是沒有任何關系的,這里是傳統的 ASP .NET 服務端渲染場景下的 Razor Page,

上面的代碼中有四行需要注意:

  • @page "/ThisIsAMeanlessDirective"

    Razor Page 腦門上按規定都得有個@page指令,來說明它的路由路徑,但鑒于這個檔案是在Program.cs中通過app.MapFallbackToPage("/_Host")直接指定的,所以這個指令是沒有實際意義的,但按規定不寫又不行,

  • @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

    TagHelper 是我們還沒有介紹到的一個 Razor 語法中的內容,我們這里先不介紹,可能要等到兩三篇文章后我們才會去介紹這個高級特性,但在這里,它的功能是可以讓我們使用一個名為<component>的元素:它不是 HTML 元素,也不是 Blazor 組件,它是魔法

  • <component type="typeof(HelloRazorAgain.App)" render-mode="ServerPrerendered" />

    這是一行魔法,它的作用是在一個服務端渲染的 Razor Page 中,去參考一個 Blazor 組件,被參考的 Blazor 組件是HelloRazorAgain.App類,

    屬性render-mode指出了,這個被參考的 Blazor 組件,將在服務端進行渲染成 HTML 檔案,再被塞到 HTTP 回包中去

  • <script src="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/28/_framework/blazor.server.js"></script>

    參考了一個特殊的 js 檔案,這個名為blazor.server.js的 js 檔案是 Blazor Server 框架自帶的一個檔案,它里面最重要的內容,是寫著瀏覽器如何向服務端發起一個 SignalR 連接

也就是說,整個檔案其實做了兩件事:

  1. 參考了一個 Blazor 組件,我們很快就會看到,這個 Blazor 組件正是上一篇文章中我們遇到過的根組件,做客戶端路由的那個組件
  2. 參考了一個blazor.server.js的特殊 JS 檔案,這是一個 Blazor Server 框架自帶的檔案,它用于運行在用戶的瀏覽器上,發起向服務端的 SignalR 連接

2.4 創建 Blazor 組件

和上一篇文章一樣,我們要寫兩個 Blazor 組件,一個是客戶端路由組件 App.razor,一個是有一點點內容的Hello.razor,為了展示 Blazor Server 渲染的特性(即 UI 要有隨著用戶輸入、點擊而重繪的程序),我們在Hello.razor中加入了一丁點額外內容

首先是App.razor,沒什么可說的,這個檔案和上一篇文章中提到的App.razor一模一樣

@using Microsoft.AspNetCore.Components.Routing

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/28/@routeData" />
    </Found>
    <NotFound>
        <h1>Page Not Found</h1>
    </NotFound>
</Router>

再次是Index.razor

@using Microsoft.AspNetCore.Components.Web

@page "/"

<h1>Hello, Razor!</h1>

<p>This is a Razor page with a click counter.</p>

<button @onclick="IncrementCount" >Click to Increase Count</button>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

有了前面 Razor 基礎語法的鋪墊,這里我們就無需再逐行解釋代碼了,

現在,所有檔案都創建完畢,所有檔案與目錄結構應當如下圖所示:

image

使用dotnet restore, dotnet build, dotnet run三連,運行起來吧!

image

2.5 Blazor Server 真正的運行邏輯

我們上面有提到一個錯誤說法:

1. 用戶的第一次請求,是一個 HTTP 請求,將走完整個 pipeline,干了兩件重要的事
   1. 向瀏覽器回傳了渲染好的`pages/_Host.cshtml`
   2. 服務端與瀏覽器之間建立了 SignalR 連接
2. 用戶的后續點、按、輸入,都將通過上面建立好的 Signal 連接與服務端互動資料

事實上的邏輯沒有那么簡單,現在我們來仔細的捋一遍,將專案運行起來,然后點開瀏覽器的除錯視窗,切換到網路選項卡,如下:

image

可以看到,按時間順序,瀏覽器先后發送了四個 HTTP 請求,并建立了一個 WebSocket 連接,我們從頭開始看

2.5.1 第一次 HTTP 請求

作為瀏覽器,用戶在地址欄敲入http://localhost:5000時,第一件要干的是就是向localhost:5000發送一個 GET 請求,

這個請求被服務端接收到之后,進入了 Asp .Net Core 的 middleware pipeline,按上文的描述,最終 hit 到app.MapFallbackToPage("/_Host")這一行代碼所注冊的 middleware 上去

而這行代碼,指導著服務端去讀取pages/_Host.cshtml,并使用ASP 場景下的 Razor 引擎,將其渲染成 HTML 檔案,

而由于pages/_Host.cshtml中使用<component>參考了我們書寫的 Blazor 組件,也就是App.razor,所以服務端會遞回的去渲染App.razor

App.razor其實只是一個路由頁面,并沒有任何視覺內容,具體要在內部再渲染什么內容,取決于用戶訪問的 URL 路徑,,在本例中,是根目錄"/",,而又恰巧,這個路徑有對應的頁面存在,所以 Hit 到了<Found>分支

@using Microsoft.AspNetCore.Components.Routing

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">   <!-- Hit到了這里 -->
        <RouteView RouteData="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/28/@routeData" />
    </Found>
    <NotFound>
        <h1>Page Not Found</h1>
    </NotFound>
</Router>

所以,服務端再去遞回的渲染Index.Razor:因為在Index.Razor腦門上寫著:@page "/"

并最終,渲染出了下面的結果:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Hello Razor Again</title>
    <base href="https://www.cnblogs.com/" />
  </head>
  <body>
    <!--Blazor:{"sequence":0,"type":"server","prerenderId":"78805629b3074d71b41621d864452c45","descriptor":"CfDJ8AcZjzqSCIlPjImWeXvfgvlW/O0dYX5WiF1HEjp3wnxX/BkueyBXrtymzLPIbZc3vdyUADpIR\u002Bx/wo6ZKqas8Mjy2puxTmZJHOxG1wwn/18E7\u002BloFQlW3/GyhZT2UQW47UHISsIvZsovrJ5VAAb69cA9Lv2M9mS\u002BsbXAH6uFqHPVDAJXEMBKR2tSrjCHlAYnLz\u002B0Yh0/ZhRYQP8mWvgR5pgQdjp6lUHznNgQ53C8b1DGeoJgzA5TbtwIcA1g6NVQy6JaQmb1TK0DNX0LBOd5c0E5Ilsf4HWHXc1JxQUI3DeGTcnA8PNTuTarVnsDeDsF\u002B6WqcEzc\u002BAAe/V/woXRg0\u002BpicpzMfaCS832wksRg/5g6"}-->
    <h1>Hello, Razor!</h1>

    <p>This is a Razor page with a click counter.</p>

    <button>Click to Increase Count</button>

    <p>Current count: 0</p>
    <!--Blazor:{"prerenderId":"78805629b3074d71b41621d864452c45"}-->

    <script src="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/28/_framework/blazor.server.js"></script>
  </body>
</html>

這樣一份渲染結果被作為回包,發回了瀏覽器,如下所示:

image

而這份渲染結果中最核心的部分,其實全在_framework/blazor.server.js

2.5.2 第二次 HTTP 請求

瀏覽器在收到 HTML 檔案后,對檔案進行渲染展示 ,除了視覺元素外,瀏覽器發現了檔案中書寫的特殊的一行:

<script src="https://www.cnblogs.com/ZhangShaobo/archive/2022/03/28/_framework/blazor.server.js"></script>

于是,自然的,瀏覽器發起了第二次 HTTP 請求,而請求的就是這個 JS 檔案

在服務端回傳了這個 JS 檔案后,按照慣例,瀏覽器開始執行 JS 檔案中的代碼,在執行程序中,瀏覽器發起了第三次與第四次 HTTP 請求,

這兩次請求都是 JS 代碼引導發起的請求

2.5.3 第三、四次 HTTP 請求

第三次 HTTP 請求是 JS 發起的fetch請求,請求路徑為/_blazor/initializers,請求方法為 GET,而服務端的回應,是一個空的陣列,我目前尚不得知這次請求的具體意義是什么,

image

image

在第三次請求結束后,JS 繼續以fetch方式發起了第四次 HTTP 請求,本次請求為 POST 請求,請求地址是/_blazor/negotiate,并通過 URL 攜帶了一個引數?negotiateVersion=1

image

服務端的回應是一個 JSON 物件,如下:

image

雖然我們也不知道具體細節,但從回包的內容上來看,顯然這是 SignalR 庫在建立連接時的協商,

2.5.4 websocket 連接

在以上四次 HTTP 請求后,瀏覽器與服務端建立起了一個 websocket 連接,顯然,其上就是 SignalR 連接,

image

這個 websocket 連接建立起來后,通過 Messages 選項卡可以看到,初期雙方進行了一些資料互動,具體干了什么我們也不清楚,應該是一些初始化作業,在這部分作業結束后,即使用戶在瀏覽器這邊沒有執行任何操作,瀏覽器也會每 5 分鐘與服務端通一下心跳,以維持網路連接,

image

我們可以認為在 websocket 連接完全建立之后,瀏覽器與服務端的互動,就再和 HTTP 協議沒什么關系了:換句話說,和服務端的整條 middleware pipeline 已經沒什么關系了,隨后在頁面上點擊按鈕,也只會看到 websocket messages 來回互動,

這一切背后的邏輯,在瀏覽器這邊,隱藏在神秘的/_framework/blazor.server.js中,在服務端那邊,隱藏在那句神奇的builder.Services.AddServerSideBlazor();

2.5.5 blazor.server.js到底在哪里?

我們從未寫過任何 JS 代碼,但服務端顯然托管了一個靜態檔案叫blazor.server.js中,那么就只有一種可能:這個檔案是工具鏈幫我們生成的,是 Blazor Server 框架的一部分,這很容易求證,我們可以使用下面的命令將整個專案發布在本地目錄中

> dotnet publish --output ./dist --configuration Release --runtime win-x64 --self-contained

通過這行命令可以把整個專案“部署”到目錄dist中去,dist目錄中,有一個 dll 名叫Microsoft.AspNetCore.Components.Server.dll,這個 dll 內部就以 Resource 的形式持有著這個神秘的blazor.server.js,如下所示:

image

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

標籤:.NET技术

上一篇:如何使用ViewProperty影片器按順序應用多個影片?

下一篇:推送到heroku容器時-生成大量資料

標籤雲
其他(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