Blazor Server充分利用Fluxor

目录

介绍

Fluxor模式和术语

用例示例

Counter用例示例

行动

定义状态

定义Counter页面

Effects类

Reducers类

配置

预测用例

预测状态

Forecast Effects类

Forecast Reducers类

WeatherForecast表

FetchData页面

结论

确认


介绍

Blazor Server应用倾向于使用Fluxor的一个常见原因是保留Blazor组件的状态。因此,例如,默认应用程序的计数器页不会在每次返回范围时将其单击计数重置为零。但Fluxor的功能远不止于此,它是一个面向消息的中间件(MOM)的集成包,可以沿着明确定义的路径推进整个应用程序。

Fluxor模式和术语

以下示例中使用的模式和术语是 Fluxor文档中说明的模式和术语。通过应用程序的路径是在用例的基础上定义的。这些是功能部分,它们具有一个事件,该事件具有以某种方式更改应用程序状态的效果。必须提供路径的主要组件、事件、事件处理程序和状态管理处理程序,但它们非常松散地耦合在一起,因此除了提供简单的消息实体外,不需要任何用户定义的集成软件。构建应用程序的用例模式不同于更传统的分层体系结构模式。分层体系结构由通常作为服务实现的功能层组成。这是一种多层蛋糕制作方法。用例模式采用多层蛋糕,并将其实现为一系列垂直切入层的切片。因此,如果服务的功能可以在用例中逐个切片实现,则服务可能不需要成为专用实体。

用例示例

可以在默认的Blazor Server应用程序中标识两个用例。计数器用例,单击按钮,然后计算、存储和显示点击计数,以及预测用例,其中页面的初始化导致上传、显示和存储天气预报。以下Counter用例示例展示了如何将事件、效果、状态变化的通量模式应用于简单路径。

Counter用例示例

在此示例中,事件是按钮单击,效果是重新计算的单击计数,状态更改是应用程序获取新的总数。开始构建用例的一个好方法是定义构成路径上消息跟踪的消息。

行动

Fluxor文档中,消息称为操作。它们是具有描述性名称的简单record类型。需要执行两项操作:

public record EventCounterClicked();
public record SetCounterTotal(int TotalCount);

操作的名称通常以消息类型开头。标准消息类型包括:

  1. 事件
  2. 命令
  3. 文件

文档消息保存数据。我更喜欢用这个单词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>实例具有用于引用StateValue属性,它也具有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方法以及注入组件的IDispatcherIStateProgram实例所需的所有参数。您不需要用这些实例填充容器,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来确定它们是否应呈现。下面的真值表显示了Statebool的可能设置以及为四种可能设置中的每一个呈现的组件。

IsLoading

IsError

显示

False

False

数据表

True

False

旋转

False

True

错误对话框

True

True

未定义

Forecast Effects

WeatherForecast服务完全在ForecastUseCase.Effects类中实现,因此无需注入WeatherForecast服务。异步流用于检索每日预测,以便每个每日预测在流中可用时立即显示。这比使用返回作为TaskTask<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 FoundationBlazor-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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值