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,异步任务完成后,释放新的信使
//在这个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上的仓库文档

Blazor和Vue对比学习(进阶2.2.3):状态管理之状态共享,Blazor的依赖注入和第三方库Fluxor的更多相关文章

  1. Blazor和Vue对比学习(基础1.6):祖孙传值,联级和注入

    前面章节,我们实现了父子组件之间的数据传递.大多数时候,我们以组件形式来构建页面的区块,会涉及到组件嵌套的问题,一层套一层.这种情况,很大概率需要将祖先的数据,传递给子孙后代去使用.我们当然可以使用父 ...

  2. Blazor和Vue对比学习(进阶2.2.4):状态管理之持久化保存(2),Cookie/Session/jwt

    注:本节涉及到前后端,这个系列的对比学习,还是专注在前端Vue和Blazor技术,所以就不撸码了,下面主要学习概念. 我们知道,Http是无状态协议,客户端请求服务端,认证一次后,如果再次请求,又要重 ...

  3. Blazor和Vue对比学习(进阶2.1.1):生命周期,基本理解和使用

    一.基本理解 首次接触"生命周期"这个名词,是比较晦涩的,Vue中又有生命周期钩子,而Blazor则是虚方法重写,容易蒙.所以,我尝试从初学者的角度来阐述一下. 1.我们在基础部分 ...

  4. Blazor和Vue对比学习(基础1.9):表单输入绑定和验证,VeeValidate和EditFrom

    这是基础部分的最后一章,内容比较简单,算是为基础部分来个HappyEnding.我们分三个部分来学习: 表单输入绑定 Vue的表单验证:VeeValidate Blazor的表单验证:EditForm ...

  5. Blazor和Vue对比学习:说在开始前

    1.Vue:现代前端三大框架之一(Vue/React/Angualr),基于HTML.CSS和JavaScript,2014年正式对外发布,目前已发展到3.X版本.值得说道的是,Vue的创始人作者是华 ...

  6. Blazor和Vue对比学习(基础1.4):事件和子传父

    Blazor和Vue的组件事件,都使用事件订阅者模式.相对于上一章的组件属性,需要多绕一个弯,无论Blazor还是Vue,都是入门的第一个难点.要突破这个难点,一是要熟悉事件订阅模式<其实不难& ...

  7. Blazor和Vue对比学习(基础1.1):组件结构

    难度:★ 简单说一说: 1.Vue和Blazor都遵循单文件结果,即HTML(视图模板).CSS(样式).JS/C#(代码逻辑)写在一个文件里,Vue的文件后缀为.vue,Blazor的文件后缀为.r ...

  8. Blazor和Vue对比学习(基础1.2):模板语法和Razor语法

    Vue使用模板语法,Blazor使用祖传的Razor语法,从逻辑和方向上看,两者极为相似,比如: 都基于HTML 都通过声明式地将组件实例的状态(数据/方法)绑定到呈现的DOM上 都通过指令实现更加丰 ...

  9. Blazor和Vue对比学习(知识点杂锦3.04):Blazor中C#和JS互操作(超长文)

    C#和JS互操作的基本语法是比较简单的,但小知识点特别多,同时,受应用加载顺序.组件生命周期以及参数类型的影响,会有比较多坑,需要耐心的学习.在C#中调用JS的场景会比较多,特别是在WASM模式下,由 ...

随机推荐

  1. 常见排序算法的golang 实现

    五种基础排序算法对比 五种基础排序算法对比 1:冒泡排序 算法描述 比较相邻的元素.如果第一个比第二个大,就交换它们两个: 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素 ...

  2. unity---UI管理模块

    UI管理器 任务: 1.所有面板的父类,2.UIMgr 所有UI控件都继承UIBehaviour 面板基类 找到相应空间 简化后 也存在问题:一个物体可以同时挂载两个组件 导致键相同,而值不同, 将值 ...

  3. IntelliJ IDEA中如何优雅的调试Java Stream操作

    Stream操作是Java 8推出的一大亮点!虽然java.util.stream很强大,但依然还是有很多开发者在实际工作中很少使用,其中吐槽最多的一个原因就是不好调试,一开始确实是这样,因为stre ...

  4. 目标检测复习之Loss Functions 总结

    Loss Functions 总结 损失函数分类: 回归损失函数(Regression loss), 分类损失函数(Classification loss) Regression loss funct ...

  5. 【雅礼集训 2017 Day2】棋盘游戏

    loj 6033 description 给一个\(n*m\)的棋盘,'.'为可通行,'#'为障碍.Alice选择一个起始点,Bob先手从该点往四个方向走一步,Alice再走,不能走走过的点,谁不能动 ...

  6. 牛客多校赛2K Keyboard Free

    Description 给定 \(3\) 个同心圆,半径分别为 \(r1,r2,r3\) ,三个点分别随机分布在三个圆上,求这个三角形期望下的面积. Solution 首先可以固定 \(A\) 点,枚 ...

  7. CSP J/S 初赛总结

    CSP J/S 初赛总结 2021/9/19 19:29 用官方答案估计 J 涂卡的时候唯一的一支 2B 铅笔坏了,只能用笔芯一个个涂 选择 \(-6\ pts\) 判断 \(-3\ pts\) 回答 ...

  8. 使用PowerShell压缩和解压ZIP包

    更新记录 本文迁移自Panda666原博客,原发布时间:2021年7月13日. 解压ZIP包 使用PowerShell的Expand-Archive命令.PowerShell官方文档地址. 命令格式: ...

  9. 基于web3D展示技术的煤矿巷道3D可视化系统

    地下开采离不开巷道工程.煤矿的生产.运输.排水.通风等各个环节都少不了巷道的支持.在煤矿智能化建设被提上日程的今天,巷道工程的智能化.可视化建设也成了行业趋势.尤其是复杂的井下作业环境,人员信息安全问 ...

  10. 运行时应用自我保护(RASP):应用安全的自我修养

    应用程序已经成为网络黑客想要渗透到企业内部的绝佳目标. 因为他们知道如果能发现并利用应用程序的漏洞,他们就有超过三分之一的机会成功入侵. 更重要的是,发现应用程序漏洞的可能性也很大. Contrast ...