Blazor和Vue對比學習(進階2.2.3):狀態管理之狀態共享,Blazor的依賴注入和第三方庫Fluxor

Blazor沒有提供狀態共享的方案,雖然依賴注入可以實現一個全局對象,這個對象可以擁有狀態、計算屬性、方法等特徵,但並不具備響應式。比如,組件A和組件B,都注入了這個全局對象,並引用了全局對象上的數據。我們通過組件A,修改全局對象的數據,全局對象上的數據更新,但引用了這個數據的組件B,並不會自動更新。如果要實現真正的狀態共享,需要藉助第三方庫Fluxor。

 

一、通過依賴注入,實現全局狀態

打開官方預製的Counter模板,無論是WASM模式,還是Server模式,組件切換/URL地址變更/頁面刷新等情況下,組件的狀態CurrenCount數據,都會恢復為初始值,狀態無法保持。依賴注入有三種生命周期,我們可以利用單例AddSingletonWASMServe的注入生命周期有差異,此處不展開)。在應用啟動時,創建一個對象(實現類和服務類一致),在組件中注入這個對象後,就可以使用。這個對象,與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上的倉庫文檔