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

標籤: Blazor .Nethtml


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

飈車前

須要說明的一點是,由於我深刻接觸 Blazor 的時間也不是多長,頂多也就半年,因此這篇文章的內容我不能保證 100% 正確,但能夠保證大體原理正確java

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

  • 瞭解 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 源碼看這篇文章,我不能貼出全部源碼,源碼須要編譯過才能看,否則會很麻煩,但編譯這事比較難,編譯源碼比看源碼難多了,這兒是一位園友的源碼編譯教程:http://www.javashuo.com/article/p-enqpoaps-t.html
天底下沒有新鮮事兒,Blazor 看着神奇,其實也沒啥黑科技,它跑不掉 Http 協議,也跑不掉 Html數據庫

開始發車

Blazor 服務端渲染過程

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

瀏覽器 -->> 服務器: 創建 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 並無什麼不同

相關文章
相關標籤/搜索