標簽: 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 並沒有什么不一樣