目录
介绍
Blazor Server应用倾向于使用Fluxor的一个常见原因是保留Blazor组件的状态。因此,例如,默认应用程序的“计数器”页不会在每次返回范围时将其单击计数重置为零。但Fluxor的功能远不止于此,它是一个面向消息的中间件(MOM)的集成包,可以沿着明确定义的路径推进整个应用程序。
Fluxor模式和术语
以下示例中使用的模式和术语是 Fluxor文档中说明的模式和术语。通过应用程序的路径是在“用例”的基础上定义的。这些是功能部分,它们具有一个事件,该事件具有以某种方式更改应用程序状态的效果。必须提供路径的主要组件、事件、事件处理程序和状态管理处理程序,但它们非常松散地耦合在一起,因此除了提供简单的消息实体外,不需要任何用户定义的集成软件。构建应用程序的用例模式不同于更传统的分层体系结构模式。分层体系结构由通常作为服务实现的功能层组成。这是一种多层蛋糕制作方法。用例模式采用多层蛋糕,并将其实现为一系列垂直切入层的切片。因此,如果服务的功能可以在用例中逐个切片实现,则服务可能不需要成为专用实体。
用例示例
可以在默认的Blazor Server应用程序中标识两个用例。计数器用例,单击按钮,然后计算、存储和显示点击计数,以及预测用例,其中页面的初始化导致上传、显示和存储天气预报。以下Counter用例示例展示了如何将“事件、效果、状态变化”的通量模式应用于简单路径。
Counter用例示例
在此示例中,事件是按钮单击,效果是重新计算的单击计数,状态更改是应用程序获取新的总数。开始构建用例的一个好方法是定义构成路径上消息跟踪的消息。
行动
在Fluxor文档中,消息称为操作。它们是具有描述性名称的简单record类型。需要执行两项操作:
public record EventCounterClicked();
public record SetCounterTotal(int TotalCount);
操作的名称通常以消息类型开头。标准消息类型包括:
- 事件
- 命令
- 文件
文档消息保存数据。我更喜欢用这个单词Set来描述用于更改状态的文档操作。在调试乱序发送或接收的消息时,此命名法非常有用。
定义状态
状态是只读的不可变数据存储,用于保存点击次数总数。状态是一种record类型,状态的定义比操作的定义稍微复杂一些,因为它需要有一个默认的构造函数。
[FeatureState]
//defines a record with a public readonly property TotalCount
public record CounterState(int TotalCount)
{
//The required default constructor
public CounterState() : this(0)
{
}
}
定义Counter页面
这里的目的是实现一个能很好地完成一件事的组件。对于页面,一件事就是管理UI。组件应松散耦合且可重用。它不需要了解任何其他组件,也不需要任何预定义的功能,例如计算点击次数和存储点击次数。通过使用组件的OnClicked处理程序将EventCounterClicked消息分派到Fluxor的消息路由器,可以轻松实现这一点。
@page "/counter"
@using BlazorFluxor.Store
@using BlazorFluxor.Store.CounterUseCase
@using Fluxor.Blazor.Web.Components
@using Fluxor;
@inherits FluxorComponent
<PageTitle >Counter </PageTitle >
<h1 >Counter </h1 >
<p role="status" >Current count: @CounterState!.Value.TotalCount </p >
<button class="btn btn-primary" @onclick="OnClicked" >Click me </button >
@code {
[Inject]
protected IState <CounterState >? CounterState { get; set; }
[Inject]
public IDispatcher? Dispatcher { get; set; }
private void OnClicked()
{
Dispatcher!.Dispatch(new EventCounterClicked());
}
}
注入的IState<CounterState>实例具有用于引用State的Value属性,它也具有StateChanged事件。页面继承的FluxorComponent,订阅StateChanged事件,这会导致每次中间件更新状态TotalCount时页面都会重新呈现。共享相同CounterState TotalCount且在作用域内的任何其他FluxorComponent也将重新呈现。因此,例如,当单击“计数页面”按钮时,购物车组件的商品计数将更新。
Effects类
具有更新状态以外的效果的操作通常在Effects类中处理。Effects类名是复数形式,但从某种意义上说,它只是一个集合,因为它可以包含多个消息处理程序。操作处理程序方法的名称可以是任何名称,但所有处理程序必须具有相同的签名,并使用该EffectMethod属性进行修饰。以下处理程序处理EventCounterClicked操作,并确定接收操作对应用程序状态的影响。
[EffectMethod]
public Task HandleEventCounterClicked
(EventCounterClicked action,IDispatcher dispatcher)
{
int totalCount = _counterState.Value.TotalCount + 5;
dispatcher.Dispatch(new SetCount (totalCount));
return Task.CompletedTask;
}
该SetCount操作的TotalCount属性设置为更新的计数值,然后调度到消息路由器。该SetCount操作在Reducers类中处理。
Reducers类
该Reducers类是状态管理类。它具有将两条或多条记录减少到单个新记录实例中的处理程序。这是State可以更改的唯一途径。此类的格式与Effects类的格式类似。所有处理程序必须具有相同的签名,并用ReducerMethod属性进行修饰。
[ReducerMethod]
public static CounterState ReduceSetCounterTotal
(CounterState state,SetCounterTotal action)
{
return state with { TotalCount = action.TotalCount };
}
}
Record类型具有通过使用关键字with来更新自身的优化方式。该reduce方法正在返回一个新的状态实例,该实例与旧状态相同,但TotalCount属性更新为操作TotalCount属性的属性
配置
配置并不像看起来那么困难,因为Fluxor将消息与其处理程序相关联,并提供Reducer方法和Effects方法以及注入组件的IDispatcher和IStateProgram实例所需的所有参数。您不需要用这些实例填充容器,Fluxor Service会处理这些实例。需要将服务添加到类的builder部分。
builder.Services.AddFluxor(options = >
{
options.ScanAssemblies(typeof(Program).Assembly);
#if DEBUG
options.UseReduxDevTools();
#endif
});
Redux Dev Tools 选项包含在上面的代码中。它是一个有用的浏览器扩展,可以绘制消息跟踪,并可以显示跟踪中每个阶段的状态值。Fluxor需要的最后一点配置是将标记<Fluxor.Blazor.Web.StoreInitializer/>添加为App.razor中的第一行。推荐的文件夹结构如下图所示,Store 是Fluxor的根目录。
预测用例
在默认应用程序中,预测用例几乎完全在FetchData页面内处理。该页面同时管理UI和注入WeatherForecast的服务。没有状态管理,因此每次页面进入范围时都会加载新的每日预测。数据库错误由默认错误处理程序处理,并且不是特定于数据库的。下面的代码实现了一个基于单页通量的预测用例,该用例维护页面的状态,因此它不会在每次进入范围时都更新。UI的控制方式是将页面中的类型绑定到不可变State记录中的属性,并使用智能组件填充页面,这些组件仅在State需要时呈现。该WeatherForecast服务完全在Effects类中实现,因此页面仅负责呈现UI。
预测状态
不可变State记录的定义如下:
using BlazorFluxor.Data;
using Fluxor;
using System.Collections.Immutable;
namespace BlazorFluxor.Store.ForecastUseCase
{
[FeatureState]
public record ForecastState(
ImmutableList<WeatherForecast> Forecasts,
string? Message,
bool IsError,
bool IsLoading)
{
public ForecastState() : this(ImmutableList.Create<WeatherForecast>(),
null, false, true)
{
}
}
}
该Forecasts列表包含每日预测,Message string用于存储错误消息。智能组件使用这两个bool来确定它们是否应呈现。下面的真值表显示了State的bool的可能设置以及为四种可能设置中的每一个呈现的组件。
IsLoading | IsError | 显示 |
False | False | 数据表 |
True | False | 旋转 |
False | True | “错误”对话框 |
True | True | 未定义 |
Forecast Effects类
该WeatherForecast服务完全在ForecastUseCase.Effects类中实现,因此无需注入WeatherForecast服务。异步流用于检索每日预测,以便每个每日预测在流中可用时立即显示。这比使用返回作为Task的Task<IEnumerable<Weatherforecast>>方法更好,因为在显示任何数据之前必须完成,并且页面呈现会延迟。
[EffectMethod]
public async Task HandleEventFetchDataInitialized
(EventMsgPageInitialized action, IDispatcher dispatcher)
{
try
{
await foreach (var forecast in ForecastStreamAsync
(DateOnly.FromDateTime(DateTime.Now), 7))
{
dispatcher.Dispatch(new SetForecast(forecast, false, false));
}
}
catch (TimeoutException ex)
{
dispatcher.Dispatch(new SetDbError(ex.Message, true, false));
}
}
为了演示错误处理,该GetForecastAsyncStream方法将在第一次调用时超时,并显示错误对话框。后续调用不会超时。
public async IAsyncEnumerable<WeatherForecast>
ForecastStreamAsync(DateOnly startDate, int count)
{
int timeout = _cts == null ? 1500 : 2000;//timeout on first call only
using var cts = _cts = new(timeout);
try
{
await Task.Delay(1750, _cts.Token);
}
//make sure the correct OperationCanceledException is caught here
catch (OperationCanceledException) when (_cts.Token.IsCancellationRequested)
{
throw new TimeoutException("The operation timed out.Please try again");
}
for (int i = 0; i < count; i++)
{
int temperatureIndex = Random.Shared.Next(0, Temperatures.Length);
int summaryIndex = temperatureIndex / 4;//relate summary to a temp range
await Task.Delay(125); //simulate slow data stream
yield return new WeatherForecast
{
Date = startDate.AddDays(i),
TemperatureC = Temperatures[temperatureIndex],
Summary = Summaries[summaryIndex]
};
}
}
该方法使用整数除法将温度范围与适当的汇总值相关联:
private static readonly string[] Summaries = new[]
{
"Freezing", "Cold", "Mild", "Hot"
};
private static readonly int[] Temperatures = new[]
{
0,-2,-4,-6,//index range 0-3 relates to summaries[index/4]==summaries[0]
2,6,8,10, //index range 4-7 relates to summaries[index/4]==summaries[1]
12,14,16,18,
23,24,26,28
};
Forecast Reducers类
Reducer通过调用列表的Add方法更新Forecasts列表。该方法的设计使其能够创建列表的新更新实例,而不会产生与添加值和创建新列表相关的常见开销。
public static ForecastState ReduceSetForecast(ForecastState state, SetForecast action)
{
return state with
{
Forecasts = state.Forecasts.Add(action.Forecast),
IsError = action.IsError,
IsLoading = action.IsLoading
};
}
WeatherForecast表
WeatherForecastTable是智能组件的一个示例。它只是使用一个添加了IsShow bool的模板表,如果将其设置为true,则组件将呈现。
@if (IsShow)
{
<TableTemplate Items="Forecasts" Context="forecast">
<TableHeader>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</TableHeader>
<RowTemplate>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</RowTemplate>
</TableTemplate>
}
@code {
#nullable disable
[Parameter]
public IEnumerable<WeatherForecast> Forecasts { get; set; }
[Parameter]
public bool IsShow { get; set; }
}
FetchData页面
@inherits FluxorComponent
@inject NavigationManager NavManager
<PageTitle>Weather Forecasts</PageTitle>
<h1>Weather Forecasts</h1>
<p>This component simulates fetching data from an async stream.
Each forecast is listed as soon as it is available.</p>
<Spinner IsVisible=@IsShowSpinner />
<WeatherForecastTable IsShow="@IsShowTable" Forecasts="@Forecasts" />
<TemplatedDialog IsShow="@IsShowError">
<OKDialog Heading="Error"
BodyText="Whoops, an error has occurred."
OnOK="NavigateToIndex">@ForecastState!.Value.Message</OKDialog>
</TemplatedDialog>
Spinner作为 NuGet包,并且模板化组件基于.NET Foundation的Blazor-Workshop中的示例。下面的代码部分主要涉及简化显示每个组件的逻辑。
@code {
[Inject]
protected IState<ForecastState>? ForecastState { get; set; }
[Inject]
public IDispatcher? Dispatcher { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (ForecastState!.Value.IsLoading is true)
{
Dispatcher!.Dispatch(new EventMsgPageInitialized());
}
}
//An expression body definition of a read-only property
protected IEnumerable<WeatherForecast> Forecasts => ForecastState!.Value.Forecasts!;
protected bool IsShowError => (ForecastState!.Value.IsLoading is false &&
ForecastState!.Value.IsError is true);
protected bool IsShowTable => (ForecastState!.Value.IsLoading is false &&
ForecastState!.Value.IsError is false);
protected bool IsShowSpinner => (ForecastState!.Value.IsLoading is true &&
ForecastState!.Value.IsError is false);
private void NavigateToIndex()
{
Dispatcher!.Dispatch(new SetStateToNew());
NavManager.NavigateTo("/");
}
}
结论
这些示例说明了使用Fluxor的好处。它产生清晰的路径,可很好地扩展,并且易于遵循和调试。在示例中,只考虑一个用例,但在企业应用程序中,将考虑多个用例。他们每个人都需要动作、效果处理程序、化简器和状态,因此代码库会很大。但这没关系,因为我们都相信详细代码可以是智能代码的格言。不是吗?
确认
我要感谢Peter Morris,《Fluxor》的作者。他是一位优秀的开发人员,他的 GitHub页面非常值得关注。
https://www.codeproject.com/Articles/5363042/Blazor-Server-Making-the-Most-of-Fluxor