• 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();
    
    
    
    //在组件中注入服务,并使用
    @page "/counter"
    @inject CountState countState
    
    Counter
    

    Counter

    ">Current count: @countState.Count

    @code { private void IncrementCount() { countState.AddCount(); } } //子组件CounterChild,用于测试存储库对象数据更新时,其它引用组件是否可以响应式更新 //结论:不能响应式更新 @inject CountState countState

    @countState.Count


    复制代码

     

    通过以上方式,我们实现了一个独立于组件树的存储库,任何一个组件,都可以通过注入这个存储库对象的方式,来绑定或修改存储库中的数据,或调用存储库中的方法。我们再具体看一下,绑定了存储库的两个父子组件,都有哪些表现:

    • 无论在父组件中,还是在子组件中,点击增加按钮,都只可以更新本组件中绑定的存储库对象。实际上存储库的状态已经更新了,但没有通知其它组件更新
    • 组件切换/页面跳转后,再回到页面时,父组件和子组件绑定的存储库数据,都更新为最新数据
    • 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中,初始化年有存储库


    ......

     

    第四步:创建存储库的状态类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
    
    //注入Fluxor提供的调度者对象Dispatcher
    //用于释放信使Action
    @inject IDispatcher Dispatcher
    
    //继承Fluxor提供的一个组件内
    //“只有”继续了这个类,组件才能实现响应式更新
    @inherits Fluxor.Blazor.Web.Components.FluxorComponent
    
    @page "/counter"
    
    

    Current count: @CounterState.Value.Count

    @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
    
    //继承Fluxor提供的一个组件类,这样才可以实现响应式更新
    @inherits Fluxor.Blazor.Web.Components.FluxorComponent
    
    
    class="top-row ps-3 navbar navbar-dark"> ............ class="nav-link" href="counter"> @($"Counter( {CounterState.Value.Count} )") ............
    @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上的仓库文档

     

  • 相关阅读:
    TS学习4-模块化
    39 本书助力精益成长
    在NLP中一下常见的任务,可以用作baseline;MRPC,CoLA,STS-B,RTE
    java-net-php-python-ssm二手手机回购网站计算机毕业设计程序
    力扣26--删除有序数组中的重复项
    【计算机毕设之基于python的股票价格智能预测可视化系统-哔哩哔哩】 https://b23.tv/Rlgmbas
    在线权鉴、签约、授权-逐浪CMS面向web3.0的韬略与实践
    【DOM系列】你真的理解事件委托(事件代理)吗?
    基于C#的学生选课管理系统
    思科模拟器--08.三层交换机配置实验(两个交换机)--24.5.22
  • 原文地址:https://www.cnblogs.com/functionMC/p/16542205.html