Blazor和Vue對比學習(進階2.2.3):狀態管理之狀態共享,Blazor的依賴注入和第三方庫Fluxor
- 2022 年 8 月 3 日
- 筆記
- Blazor, MAUI/WPF/Blazor/Vue/ArkUI, VUE
Blazor沒有提供狀態共享的方案,雖然依賴注入可以實現一個全局對象,這個對象可以擁有狀態、計算屬性、方法等特徵,但並不具備響應式。比如,組件A和組件B,都注入了這個全局對象,並引用了全局對象上的數據。我們通過組件A,修改全局對象的數據,全局對象上的數據更新,但引用了這個數據的組件B,並不會自動更新。如果要實現真正的狀態共享,需要藉助第三方庫Fluxor。
一、通過依賴注入,實現全局狀態
打開官方預製的Counter模板,無論是WASM模式,還是Server模式,組件切換/URL地址變更/頁面刷新等情況下,組件的狀態CurrenCount數據,都會恢復為初始值,狀態無法保持。依賴注入有三種生命周期,我們可以利用單例AddSingleton(WASM和Serve的注入生命周期有差異,此處不展開)。在應用啟動時,創建一個對象(實現類和服務類一致),在組件中注入這個對象後,就可以使用。這個對象,與Pinia相似,獨立於組件樹,所有組件都可以訪問,同時,它位於應用進程的記憶體中,組件切換時,它不會消失。但是,它不具備響應式。全局對象數據的更新,並不會響應式的更新所有引用這個數據的組件。WASM和Server的實現差不多,但兩者表現有一點差異,後文詳述,先來看實現程式碼。
//先創建一個存儲庫類 public class CountState { public int Count { get; set; } = 0; public void AddCount() { Count++; } } //在服務容器中注入 builder.Services.AddSingleton<CountState, CountState>(); //在組件中注入服務,並使用 @page "/counter" @inject CountState countState <PageTitle>Counter</PageTitle> <h1>Counter</h1> <p">Current count: @countState.Count</p> <button @onclick="IncrementCount">點擊增加</button> <CounterChild></CounterChild> @code { private void IncrementCount() { countState.AddCount(); } } //子組件CounterChild,用於測試存儲庫對象數據更新時,其它引用組件是否可以響應式更新 //結論:不能響應式更新 @inject CountState countState <h3>@countState.Count</h3>
<button @onclick="()=>{countState.Count++;}">在Child中點擊增加/button>
通過以上方式,我們實現了一個獨立於組件樹的存儲庫,任何一個組件,都可以通過注入這個存儲庫對象的方式,來綁定或修改存儲庫中的數據,或調用存儲庫中的方法。我們再具體看一下,綁定了存儲庫的兩個父子組件,都有哪些表現:
- 無論在父組件中,還是在子組件中,點擊增加按鈕,都只可以更新本組件中綁定的存儲庫對象。實際上存儲庫的狀態已經更新了,但沒有通知其它組件更新
- 組件切換/頁面跳轉後,再回到頁面時,父組件和子組件綁定的存儲庫數據,都更新為最新數據
- WASM模式下,刷新頁面時,重新載入整個應用,保存在記憶體中存儲庫清空,所以父子組件綁定的存儲庫數據,都恢復為初始值。但Server模式下刷新,因為存儲庫對象保存在SignalR連接的上下文中(伺服器記憶體),只要和伺服器的連接沒有斷開,狀態會一直保存。(即使斷開,Signal可以設置讓伺服器保存一段時間,在這段時間內,如果重連成功,狀態依然能夠保持)
總結:依賴注入是實現全局狀態的首先方案,使用便捷、操作簡單。但如果要實現響應式更新,我們還是需要藉助第三方庫Fluxor
二、Fluxor的使用
1、一個最簡單的案例
Blazor的入門學習,有一個非常有名的教程《blazor university》。這個教程的作者叫Peter Morris,Fluxor正是出自他手,最近的更新也是比較頻繁,值得一試。相比於Vue的Pinia和Vuex,使用上會比較繁瑣,主要原因是多了一個action機制,中間轉了一下,後面會詳細解讀,我們先上手,擼一個簡單的案例:
第一步:安裝依賴
Fluxor.Blazor.Web
第二步:入口程式Program.cs,註冊Fluxor服務
var currentAssembly = typeof(Program).Assembly;
builder.Services.AddFluxor(options => options.ScanAssemblies(currentAssembly));
第三步:根組件App.razor中,初始化年有存儲庫
<Fluxor.Blazor.Web.StoreInitializer/>
<Router AppAssembly=”@typeof(App).Assembly”>
……
</Router>
第四步:創建存儲庫的狀態類State、操作類Reducer和事件類Action(先稱它為信使),建議將這三個類統一放到一個文件夾中。文件結構如下圖所示:
//=========================================================================== //①狀態類CounterState using Fluxor; namespace StateManageFluxor.Store.Counter { //狀態State類,需要標註FeatureState特性 [FeatureState] public class CounterState { //定義了一個Count狀態數據,必須為只讀 public int Count { get; } public CounterState(int count) { Count = count; } //初始化Store時,系統調用,建議私有,必須有 private CounterState() { Count = 0; } } } //================================================================================================== //②操作類Reducer,類似於Pinia中的Action,用於操作狀態State //建議為靜態類和靜態方法 //可以寫多個Reducer,每個操作方法標註ReducerMethod特性 using Fluxor; namespace StateManageFluxor.Store.Counter { public static class CounterReducer { //狀態count遞增1操作 //接收兩個參數,一個是原state,一個是信使action [ReducerMethod] public static CounterState ReduceIncrCountAction(CounterState state, IncrCountAction action) { return new CounterState(count: state.Count + 1); } //狀態count遞減1操作 [ReducerMethod] public static CounterState ReduceDecrCountAction(CounterState state, DecrCountAction action) { return new CounterState(count: state.Count - action.DecrNum); } //如果信使不傳遞參數,還可以寫成如下格式: //[ReducerMethod(typeof(IncrCountAction))] //public static CounterState ReduceIncrCountAction(CounterState state) //{ // return new CounterState(count: state.Count + 1); //} } } //================================================================================================ //③事件類Action(稱它為信使) //一個Reducer對應一個Action //在組件中,通過Fluxor提供的Dispatcher/調度者,釋放信使Action //信使傳遞訊號給相應的Reducer,通知它執行,並根據需要傳遞參數 //信使IncrCountAction,一個空類,不傳遞參數 namespace StateManageFluxor.Store.Counter { public class IncrCountAction { } } //信使DecrCountAction,定義了一個DecrNum屬性 //調度者釋放信使時,可以定義DecrNum值,傳遞資訊 namespace StateManageFluxor.Store.Counter { public class DecrCountAction { public int DecrNum { get; set; } public DecrCountAction(int decrNum) { DecrNum = decrNum; } } }
第五步:Counter.razor組件,在組件中使用①綁定狀態;②通過調度者,釋放信使,從而觸發Reducer操作狀態
//引用需要的三個命名空間,可以統一放到_Imports.razor中 @using Fluxor @using Microsoft.AspNetCore.Components @using StateManageFluxor.Store.Counter //注入存儲庫的State,CounterState @inject IState<CounterState> CounterState //注入Fluxor提供的調度者對象Dispatcher //用於釋放信使Action @inject IDispatcher Dispatcher //繼承Fluxor提供的一個組件內 //「只有」繼續了這個類,組件才能實現響應式更新 @inherits Fluxor.Blazor.Web.Components.FluxorComponent @page "/counter" <p>Current count: @CounterState.Value.Count</p> <button @onclick="IncrCount">增加</button> <button @onclick="DecrCount">減少</button> @code { //IncrCount方法中,調度者釋放一個空的信使IncrCountAction private void IncrCount() { var action = new IncrCountAction(); Dispatcher.Dispatch(action); } //DecrCount方法中,調度者釋放一個攜帶資訊的信使DecrCountAction private void DecrCount() { var action = new DecrCountAction(2); Dispatcher.Dispatch(action); } }
第六步:完成以上五步,即實現了一個共享存儲庫的簡單應用。我們可以在另外一個組件中(選左側導航欄的NavMenu.razor),也綁定存儲庫的狀態,驗證一下是否能夠響應式的更新
//注入存儲庫的State @inject IState<CounterState> CounterState //繼承Fluxor提供的一個組件類,這樣才可以實現響應式更新 @inherits Fluxor.Blazor.Web.Components.FluxorComponent <div class="top-row ps-3 navbar navbar-dark"> ............ <NavLink class="nav-link" href="counter"> @($"Counter( {CounterState.Value.Count} )") </NavLink> ............ </div> @code { ...... }
以下六步完成後,我們實現的效果如下所示:
2、如果狀態數據來源於非同步操作的結果,我們希望在非同步操作完成前,狀態數據更新為結果1;非同步操作完成後,狀態數據更新為結果2
這種情況,我們需要藉助Fluxor提供的另外一個特性Effect來實現。Effect就像是,信使到達Reducer之前的一個中間件,在中間件中,我們執行非同步操作,非同步操作完成前,原信使先抵達相應的Reducer,非同步操作完成後,中間件會釋放一個新的信使到相應的Reducer。我們延續前面的案例,來學習Effect的使用:
//①首先,我們新增一個Reducer,這個Reducer是非同步任務完成後,要執行的狀態操作 //打開文件Store/Counter/CounterReducer.cs,新增以下方法 //這個操作相對於遞增1操作來設計 //假設非同步任務完成前,遞增1;非同步任務完成後,遞增10 [ReducerMethod] public static CounterState ReduceIncr10CountAction(CounterState state, Incr10CountActionAsync action) { return new CounterState(count: state.Count + 10); } //②然後,新增一個信使類Incr10CountActionAsync,不用傳遞參數,所以一個空類就可以 namespace StateManageFluxor.Store.Counter { public class Incr10CountActionAsync { } } //③最後,新增一個Effect類CounterEffect.cs,進行非同步操作 //注入信使IncrCountAction,非同步任務完成後,釋放新的信使。Action/Effect/Reducer,如果配對?關鍵一是[EffectMethod]特殊,關鍵2是Action。 //在這個Effect類中,可以根據需要,注入其它服務 using Fluxor; namespace StateManageFluxor.Store.Counter { public class CounterEffect { [EffectMethod(typeof(IncrCountAction))] public async Task IncrCountAsync(IDispatcher Dispatcher) { await Task.Delay(1000); var action = new Incr10CountActionAsync(); Dispatcher.Dispatch(action); } } } //第③步的另外一種寫法 //如果需要使用信使IncrCountAction攜帶的參數,則使用這種寫法 using Fluxor; namespace StateManageFluxor.Store.Counter { public class CounterEffect { [EffectMethod(typeof(IncrCountAction))] public async Task IncrCountAsync(IDispatcher Dispatcher, IncrCountAction action) { await Task.Delay(1000); var action = new Incr10CountActionAsync(); Dispatcher.Dispatch(action); } } }
以上操作完成後,頁面效果如下:
點擊後,count先遞增1,變成2
延遲1秒後,非同步任務完成,count再遞增10,變成12
3、最後,我們將整個Fluxor的框架邏輯,使用圖例進行總結:
- 因為和Vue的Pinia放在一起學習,所以我們先把概念理清一下。(1)Pinia中的state,相當於Fluxor中的state;(2)Pinia中的Action,相當於Fluxor中的Reducer和Effect;(3)兩者裡面都有一個Action,但兩者天差地別,不要混淆了。Pinia中的Action就是方法,可同步、可非同步,Fluxor中的Action,取意action委託,和事件、消息,是同一個方向上的概念,和一些框架的消息機制很相似
- Fluxor的邏輯雖然比較複雜,但套路還是熟悉的事件訂閱機制。我們雅稱Action為信使,其實它就好比事件訂閱機制中的事件,狀態方法Reducer訂閱事件,並在事件響應程式中修改狀態,調度者Dispatcher觸發事件。事件,即可以是一個空對象(只起到通知作用),也可以攜帶參數。
- Effect像是事件發送到訂閱者過程中的一個中間件,這個中間件可以執行一個非同步請求,根據非同步請求結果,決定傳遞原事件,還是一個新的事件。
- 如果組件要實現響應式更新,「必須」繼承【@inherits Fluxor.Blazor.Web.Components.FluxorComponent】,必須打了引號,是因為調度器所在的組件,可以不用繼承,因為不需要通知,組件就已經觸發的StateHasChange。其實,繼承FluxorComponent類,底層也是觸發組件重新渲染。
4、Fluxor還提供了中間件和調試工作Redux Dev Tools,可詳見github上的倉庫文檔