1. 程式人生 > >Blazor 機制初探以及什麼是前後端分離,還不趕緊上車?

Blazor 機制初探以及什麼是前後端分離,還不趕緊上車?

標籤: Blazor .Net


上一篇文章發了一個 BlazAdmin 的嚐鮮版,這一次主要聊聊 Blazor 是如何做到用 C# 來寫前端的,傳送門:https://www.cnblogs.com/wzxinchen/p/12057171.html

飈車前

需要說明的一點是,因為我深入接觸 Blazor 的時間也不是多長,頂多也就半年,所以這篇文章的內容我不能保證 100% 正確,但可以保證大致原理正確

另外,具有以下條件的園友食用這篇文章會更舒服:

  • 瞭解 Http 請求響應模型及 Http 協議
  • 有足夠的微軟技術棧 Web 開發經驗,例如 MVC、WebApi 等
  • 有按照微軟的 Blazor 官方文件進行入門的實戰操作,傳送門:https://docs.microsoft.com/zh-cn/aspnet/core/blazor/get-started?view=aspnetcore-3.1&tabs=visual-studio
  • 有自己研究過 Blazor 生成的程式碼
  • 有過 SignalR 或 WebSocket 使用經驗

建議結合 AspNetCore 原始碼看這篇文章,我不能貼出所有原始碼,原始碼需要編譯過才能看,不然會很麻煩,但編譯這事比較難,編譯原始碼比看原始碼難多了,這兒是一位園友的原始碼編譯教程:https://www.cnblogs.com/ZaraNet/p/12001261.html
天底下沒有新鮮事兒,Blazor 看著神奇,其實也沒啥黑科技,它跑不掉 Http 協議,也跑不掉 Html

開始發車

Blazor 服務端渲染過程

當您開啟一個服務端渲染的 Blazor 應用時:

    瀏覽器 -->> 伺服器: 建立 WebSocket 連線
    伺服器 -->> 瀏覽器: 傳送首頁 HTML 程式碼
    loop 連線未斷開
        Note left of 瀏覽器: 瀏覽器JS捕獲使用者輸入事件
        瀏覽器 -->> 伺服器: 通知伺服器發生了該事件
        Note right of 伺服器: 伺服器 .Net 處理事件
        伺服器-->>瀏覽器: 傳送有變動的 HTML 程式碼
        Note left of 瀏覽器: 瀏覽器JS渲染變動的 HTML 程式碼
    end

有以下幾點需要注意:

  • WebSocket 連線採用 SignalR 來建立,如果瀏覽器不支援 WebSocket,SignalR 會採用其他技術建立
  • 瀏覽器捕獲使用者輸入是使用 Javascript進行捕獲的
  • 伺服器處理客戶端事件完成後,會生成新的 HTML 結構,然後將這個結構與老的結構進行對比,得到有變動的 HTML 程式碼
  • Blazor 服務端渲染版採用在伺服器端維護一個虛擬 DOM 樹來實現上述操作
  • “通知伺服器發生了該事件”這一步裡,從原理上來說類似於 WebForm 的 PostBack 機制,不同點在於,Blazor 只告訴伺服器是哪個 DOM 節點發生了什麼事件,這個傳輸量是極小的。

服務端渲染的基本原理就是這樣,下面我們詳細討論

Blazor 路由渲染過程

當我們通過 NavigationManager 去改變路由地址時,大概流程如下

st=>start: 伺服器啟動
rt=>operation: 初始化 Router 元件,Router 內部註冊 LocationChanged 事件
op1=>operation: LocationChanged 事件中根據路由查詢對應的元件,預設觸發首頁元件
queue=>operation: 加入渲染佇列
render=>operation: 一直進行渲染及比對,直到佇列中所有的元件全部渲染完
diff=>operation: 將比對的差異結果更新至瀏覽器
e=>end: 等待下一次路由改變,繼續觸發 LocationChanged 事件

st->rt->op1->queue->render->diff->e

這裡的 Router 元件,就是我們經常用到的,看看下面的程式碼,是不是很熟悉?

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Router 元件部分程式碼

public class Router : IComponent, IHandleAfterRender, IDisposable
{
     public void Attach(RenderHandle renderHandle)
        {
            _logger = LoggerFactory.CreateLogger<Router>();
            _renderHandle = renderHandle;
            _baseUri = NavigationManager.BaseUri;
            _locationAbsolute = NavigationManager.Uri;
            //註冊 LocationChanged 事件
            NavigationManager.LocationChanged += OnLocationChanged;
        }
    private void OnLocationChanged(object sender, LocationChangedEventArgs args)
        {
            _locationAbsolute = args.Location;
            if (_renderHandle.IsInitialized && Routes != null)
            {
                Refresh(args.IsNavigationIntercepted);
            }
        }
    private void Refresh(bool isNavigationIntercepted)
        {
            var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
            locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
            var context = new RouteContext(locationPath);
            Routes.Route(context);
            
            ..........
            
            var routeData = new RouteData(
                context.Handler,
                context.Parameters ?? _emptyParametersDictionary);
            //此處開始渲染,Found 是一個 RenderFragment<RouteData> 委託,是我們在呼叫的時候指定的那個
            _renderHandle.Render(Found(routeData));
            ..........
        }
}

Blazor 元件渲染過程

要開始飈車了,握緊方向盤,不要翻車。
這部分可能會比較難,如果你發現你看不懂的話就先嚐試自己寫個元件玩玩。
在 Blazor 中,幾乎一切皆元件。首先我們得提到一個 Blazor 元件的幾個關鍵方法,部分方法也是它的生命週期

  • OnInitialized、OnInitializedAsync:僅在第一次例項化元件時,才會呼叫這些方法一次。注意,該方法呼叫時引數已經設定,但沒有渲染。
  • SetParametersAsync:該方法可以讓您在設定引數之前做一些事
  • OnParametersSetAsync、OnParametersSet:每一次引數設定完成之後都會呼叫
  • OnAfterRender、OnAfterRenderAsync:在元件渲染完成之後觸發
  • ShouldRender:如果該方法返回 false,則元件在第一次渲染完成後不會執行二次渲染
  • StateHasChanged:強制渲染當前元件,如果 ShouldRender 返回的是 false,則不會強制渲染
  • BuildRenderTree: 該方法一般情況下我們用不到,它的作用是拼接 HTML 程式碼,由 VS 自動生成的程式碼去呼叫它

另有一個關鍵的結構體 EventCallBack,還有一個關鍵的委託RenderFragment,它倆非常重要,前者可能見得比較少,後者基本上玩過 Blazor 的園友都知道。

上面提到的關鍵點,有個印象即可,下面將開始飈車,我們將重點討論那個流程圖中渲染對比的那部分,但將忽略瀏覽器捕獲事件這一步,我不能貼太多的原始碼,儘可能用流程圖表示

主要生命週期過程

st=>start: 開始渲染
isfirst=>condition: 是否首次渲染
init=>operation: 呼叫 OnInitialized 方法
initAsync=>operation: 呼叫 OnInitializedAsync 方法
onSetParameter=>operation: 呼叫 OnParametersSet 方法
setParameter=>operation: 呼叫 SetParametersAsync 方法
stateHasChanged=>operation: 呼叫 StateHasChanged 方法
st->setParameter->isfirst->init->initAsync->onSetParameter
onSetParameter->stateHasChanged
isfirst(yes)->init
isfirst(no)->onSetParameter

需要注意的是這個流程中沒有 OnAfterRender 方法的呼叫,這個將在下面討論

StateHasChanged 方法

這個方法至關重要,就比如上圖中最終只到了 StateHasChanged 方法,就沒了下文,我們來看看這個方法裡面有什麼

st=>start: 開始
isfirst=>condition: 是否首次渲染
should=>condition: ShouldRender 為True?
queue=>operation: 進入渲染佇列
render=>operation: 開始迴圈渲染佇列的資料
after=>operation: 觸發 OnAfterRender 方法
e=>end: 結束
st->isfirst
queue->render->after->e
isfirst(yes)->queue
isfirst(no)->should
should(yes)->queue
should(no)->e

至此,我們基本把一個元件的生命週期的那幾個方法討論完了,除了一些非同步版本的,邏輯都差不多,沒有寫進來

渲染佇列時都幹了啥?

嗯對,這是重點

st=>start: 開始渲染佇列
queue=>condition: 佇列還有元件?
read=>operation: 從佇列獲取元件
swap=>operation: 備份當前 DOM 樹及清空
render=>operation: 呼叫元件的 RenderFragment 委託獲取新的 DOM 樹
diff=>operation: 與備份的樹對比
append=>operation: 將對比結果存入列表
display=>operation: 將列表中的所有對比結果傳送至瀏覽器
e=>end: 結束
st->queue
read->swap->render->diff->append->queue
queue(yes)->read
queue(no)->display->e

為了圖好看點(好吧現在其實也不好看),我把流程縮短了一點,有以下幾點需要注意:

  • 渲染開始之前是將當前樹賦值成了舊的樹,然後再將當前樹清空
  • 元件的 RenderFragment 委託在大多數情況下就是元件的 ChildContent 屬性的值,玩過的都知道幾乎每個元件都有自己的 ChildContent
  • 同時 RenderFragment 也有可能是 ComponentBase類中的一個私有屬性,詳見下面的程式碼。當然也有可能是其他的,限於篇幅,不細說
  • RenderFragment 委託輸入的引數就是當前這顆樹
  • 如果您在元件中呼叫了子元件,並且這個子元件還有自己的內容,那麼 VS 會生成呼叫這個元件的程式碼,並且為這個元件新增 ChildContent 屬性,內容就是子元件自己的內容,詳見程式碼

下面是 ComponentBase 的部分程式碼,上文提到的私有屬性就是 _renderFragment,這個私有屬性僅在此處被賦值,可以看到這個屬性內部呼叫了 BuildRenderTree 方法

    public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
    {
        private readonly RenderFragment _renderFragment;

        /// <summary>
        /// Constructs an instance of <see cref="ComponentBase"/>.
        /// </summary>
        public ComponentBase()
        {
            _renderFragment = builder =>
            {
                _hasPendingQueuedRender = false;
                _hasNeverRendered = false;
                BuildRenderTree(builder);
            };
        }
    }

針對最後一點,舉個例子
下面是 NavMenu.razor 元件的 Razor 程式碼

<BMenu>
    <BMenuItem Route="button">Button 按鈕</BMenuItem>
</BMenu>

下面是 VS 生成的程式碼

public partial class NavMenu : Microsoft.AspNetCore.Components.ComponentBase
    {
        protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.OpenComponent<BMenu>(1);
            __builder.AddAttribute(4, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => {
                __builder2.OpenComponent<BMenuItem>(6);
                __builder2.AddAttribute(7, "Route", "button");
                __builder2.AddAttribute(8, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder3) => {
                    __builder3.AddMarkupContent(9, "Button 按鈕");
                }
                ));
                __builder2.CloseComponent();
            }
        }
    }

可以看到,NavMenu.razor 使用了 BMenu 這個元件,BMenu 又使用了 BMenuItem這個元件,共套了兩層,因此生成了兩個 ChildContent 的屬性,而且屬性型別都是 Microsoft.AspNetCore.Components.RenderFragment
到這兒為止,Blazor 的大概機制基本討論了一半,接下來討論上個流程圖中的對比那一步,看看 Blazor 是如何進行的對比
這裡不細說,因為確實太複雜我也沒搞清楚,只說個大概流程,需要說明的一點是 Blazor 的對比是基於序列號的,序列號是什麼?大家一定注意到上面程式碼中的 __builder.AddAttribute(4 中的這個 4 了,這個 4 就是序列號,然後每個序列號對應的內容稱為幀,簡而言之是通過判斷每個序列號對應的幀是否一致來對比是否有改動

st=>start: 開始對比
seq=>operation: 迴圈每幀
compare=>condition: 序列號是否一致?
isComponent=>condition: 該幀是否都為元件?
render=>operation: 渲染該元件
compareParameter=>condition: 兩邊元件的引數是否有變化?
skip=>operation: 跳過該幀
setParameter=>operation: 設定新元件的引數,進入該元件的生命週期流程
currentSkip=>operation: 機制過於複雜,不討論
e=>end: 對比結束
endSeq=>operation: 結束迴圈
st->seq->compare
compare(yes)->isComponent
compare(no)->currentSkip
isComponent(yes)->render->compareParameter
isComponent(no)->currentSkip
compareParameter(yes)->setParameter->endSeq->e
compareParameter(no)->skip

流程圖總算畫完了,大概有以下幾點需要注意:

  • 實際的對比過程是很複雜的,流程圖是簡化了再簡化的結果,這篇文章的幾個流程圖需要結合在一起理解才行
  • 當走到設定新元件的引數這一步時,繼續往下其實就是進入了新元件的生命週期流程,這個流程跟上面的生命週期流程是一樣的
  • 結合所有流程圖來看,如果只是元件本身重新渲染,那麼元件本身設定引數的方法不會被觸發,必須是它的父元件被渲染,才會觸發它自己的設定引數的方法
  • 對比元件引數這一步,流程圖比較籠統。我們可以簡單的認為,沒有元件的引數是不變化的,它的對比流程過於細節,我覺得沒必要寫進來。

渲染到此結束,下面就來談談 Blazor 會讓我們遇到的問題

Blazor 的不足

優勢我們就不談了,我們來談談一個比較隱藏但又不容易解決的不足,這個不足就是我們一不小心就讓我們的 Blazor 應用變得卡,而且還比較不容易解決,這個問題在服務端渲染的應用中尤其嚴重。

結合第一張流程圖,瀏覽器產生任何事件都會發送到伺服器端,想象一下你註冊了一個 onmousemove 事件的話,還要不要活了?所以,大規模觸發的事件儘量少註冊,這裡面的網路傳輸成本是很大的,而且也會給你的服務端造成很大的壓力。

Blazor 應用變卡一般有以下幾種情況,我們只討論服務端應用的情況

  • 伺服器端已經掛了,這種情況其實瀏覽器端會完全失去響應,除非你重新整理
  • 你的程式碼有問題或你引用的庫的程式碼有問題,導致進入死迴圈或迴圈次數非常多

第一點無所謂,第二點是要命的,至少對於我來說,一旦 Blazui 或 BlazAdmin 出現了卡的情況,會非常頭疼,但實際上大多數情況都是第二種中,原因在於:

結合所有流程圖來看,Blazor 完成渲染才會傳送至瀏覽器,那麼完成渲染的標準就是渲染佇列被清空,那如果一直無法清空呢?體現出來就是死迴圈,或者說發生了一次點選事件結果迴圈了十次,這明顯不科學(你故意的例外),而渲染佇列被加入新東西大多數情況下是因為呼叫了 StateHasChanged 並且 ShuoldRender 返回了 true,或者是因為使用了 EventCallBack,這些程式碼所在的地方你全都難以除錯
因為這些程式碼不是你的程式碼,所以你的斷點也沒處打,目前的 Blazor 不會告訴你到底是哪個元件哪行程式碼引起的死迴圈

還欠了點東西

還有一個關鍵的東西是 EventCallBack,一次寫太多了,不想寫了
園友如果有興趣的話可以繼續把這個寫了
有任何問題可進QQ群交流:74522853

什麼是前後端分離?

Blazor 出來的時候一堆人說什麼 WebForm 又來了,Silverlight 又來了,還有啥啥亂七八糟的,最讓我不能理解的是另一種說法:

前後端分離搞得好好的,微軟為什麼又要把前後端合在一起?

我不敢瞎說,我找了一篇文章:https://www.jianshu.com/p/bf3fa3ba2a8f
下面是摘抄的內容

1.首先要知道所有的程式都是一資料為基礎的,沒有資料的程式沒有實際意義,程式的本質就是對程式的增刪改查。

2.前後端分離就是把資料操作和顯示分離出來。前端專注做資料顯示,通過文字,圖片或者圖示等方式讓資料形象直觀的顯示出來。後端專注做資料的操作。前端把資料發給後端,有後端對資料進行修改。

3.後端一般用java,c#等語言,現在的node屬於JavaScript也能進行後端操作,此處不意義裂解語言。後端來進行資料庫的連結,並對資料進行操作。

4.後端提供介面給前端呼叫,來觸發後端對資料的操作。

基本原理就是這樣,可能語言上不準確,思想是沒有問題的。

作者:前端developer 連結:https://www.jianshu.com/p/bf3fa3ba2a8f 來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

重點在於第二點,前後端分離就是把資料操作和顯示分離出來,Blazor 並沒有有非要讓你用 .Net 寫後端
第三點也說了,前端一般是 JS,那現在把 JS 換成 .Net 並沒有什麼不一樣