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

上一篇文章發了一個 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 應用時:

<code>Copy瀏覽器服務器建立 WebSocket 連接發送首頁 HTML 代碼瀏覽器JS捕獲用戶輸入事件通知服務器發生了該事件服務器 .Net 處理事件發送有變動的 HTML 代碼瀏覽器JS渲染變動的 HTML 代碼loop[ 連接未斷開 ]瀏覽器服務器/<code> 

有以下幾點需要注意:

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

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

Blazor 路由渲染過程#

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

<code>Copy服務器啟動初始化 Router 組件,Router 內部註冊 LocationChanged 事件LocationChanged 事件中根據路由查找對應的組件,默認觸發首頁組件加入渲染隊列一直進行渲染及比對,直到隊列中所有的組件全部渲染完將比對的差異結果更新至瀏覽器等待下一次路由改變,繼續觸發 LocationChanged 事件/<code>

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

<code>Copy<router>
<found>
<routeview>
/<found>
<notfound>
<layoutview>

Sorry, there's nothing at this address.


/<layoutview>
/<notfound>
/<router>
/<code>

Router 組件部分代碼#

<code>Copypublic 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));
..........
}
}/<routedata>/<router>/<code>

Blazor 組件渲染過程#

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

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

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

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

主要生命週期過程#

<code>Copy開始渲染調用 SetParametersAsync 方法是否首次渲染調用 OnInitialized 方法調用 OnInitializedAsync 方法調用 OnParametersSet 方法調用 StateHasChanged 方法yesno/<code>

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

StateHasChanged 方法#

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

<code>Copy開始是否首次渲染進入渲染隊列開始循環渲染隊列的數據觸發 OnAfterRender 方法結束ShouldRender 為True?yesnoyesno/<code>

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

渲染隊列時都幹了啥?#

嗯對,這是重點

<code>Copy開始渲染隊列隊列還有組件?從隊列獲取組件備份當前 DOM 樹及清空調用組件的 RenderFragment 委託獲取新的 DOM 樹與備份的樹對比將對比結果存入列表將列表中的所有對比結果發送至瀏覽器結束yesno/<code>

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

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

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

<code>Copy    public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
private readonly RenderFragment _renderFragment;

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

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

<code>Copy<bmenu>
<bmenuitem>Button 按鈕/<bmenuitem>
/<bmenu>/<code>

下面是 VS 生成的代碼

<code>Copypublic 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();
}
}
}/<bmenuitem>/<bmenu>/<code>

可以看到,NavMenu.razor 使用了 BMenu 這個組件,BMenu 又使用了 BMenuItem這個組件,共套了兩層,因此生成了兩個 ChildContent 的屬性,而且屬性類型都是 Microsoft.AspNetCore.Components.RenderFragment
到這兒為止,Blazor 的大概機制基本討論了一半,接下來討論上個流程圖中的對比那一步,看看 Blazor 是如何進行的對比


這裡不細說,因為確實太複雜我也沒搞清楚,只說個大概流程,需要說明的一點是 Blazor 的對比是基於序列號的,序列號是什麼?大家一定注意到上面代碼中的 __builder.AddAttribute(4 中的這個 4 了,這個 4 就是序列號,然後每個序列號對應的內容稱為幀,簡而言之是通過判斷每個序列號對應的幀是否一致來對比是否有改動

<code>Copy開始對比循環每幀序列號是否一致?該幀是否都為組件?渲染該組件兩邊組件的參數是否有變化?設置新組件的參數,進入該組件的生命週期流程結束循環對比結束跳過該幀機制過於複雜,不討論yesnoyesnoyesno/<code>

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

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

的認為,沒有組件的參數是不變化的,它的對比流程過於細節,我覺得沒必要寫進來。

渲染到此結束,下面就來談談 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 並沒有什麼不一樣

出處:https://www.cnblogs.com/wzxinchen/p/12082136.html

版權:本文采用「署名-非商業性使用-相同方式共享 4.0 國際」知識共享許可協議進行許可。


分享到:


相關文章: