ASP.NET 核心 Blazor 预呈现状态持久性

如果不保留组件状态,预呈现期间使用的状态将丢失,并且必须在应用完全加载时重新创建。 如果任何状态是异步创建的,那么当组件重新渲染时,用户界面可能会闪烁,因为预渲染的用户界面将被替换。

请考虑以下 PrerenderedCounter1 计数器组件。 该组件在预呈现期间在 OnInitialized 生命周期方法中设置初始随机计数器值。 当组件以交互方式呈现时,第二次执行时 OnInitialized 将替换初始计数值。

PrerenderedCounter1.razor:

@page "/prerendered-counter-1"
@inject ILogger<PrerenderedCounter1> Logger

<PageTitle>Prerendered Counter 1</PageTitle>

<h1>Prerendered Counter 1</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;

    protected override void OnInitialized()
    {
        currentCount = Random.Shared.Next(100);
        Logger.LogInformation("currentCount set to {Count}", currentCount);
    }

    private void IncrementCount() => currentCount++;
}

注释

如果应用采用 交互式路由,并且通过内部 增强导航访问页面,则不会进行预呈现。 因此,必须为 PrerenderedCounter1 组件执行完整页面重载才能查看以下输出。 有关详细信息,请参阅 交互式路由和预呈现 部分。

运行应用并检查组件的日志记录。 下面是示例输出。

info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 41
info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 92

第一个记录的计数发生在预呈现期间。 重新呈现组件时,会在预呈现后再次设置计数。 当计数从 41 更新到 92 时,UI 也会闪烁。

为了在预呈现期间保留计数器的初始值,Blazor 支持使用 PersistentComponentState 服务在预呈现页面中保留状态(对于嵌入到 Razor Pages 或 MVC 应用的页面或视图中的组件,使用持久组件状态标记帮助程序)。

通过使用在预呈现期间使用的相同状态来初始化组件,将只执行一次成本高昂的初始化步骤。 呈现的 UI 也与预呈现 UI 相匹配,因此浏览器不会闪烁。

持久预呈现状态将传输到客户端,用于还原组件状态。 在客户端呈现(CSR)期间,InteractiveWebAssembly数据将暴露给浏览器,并且不得包含敏感的隐私信息。 在交互式服务器端呈现(交互式 SSR)期间,InteractiveServerASP.NET 核心数据保护可确保数据安全传输。 InteractiveAuto 呈现模式结合了 WebAssembly 和服务器交互性,因此有必要考虑向浏览器公开数据,就像在 CSR 案例中一样。

若要保留预呈现状态,请使用 [PersistentState] 属性在属性中保留状态。 在预呈现期间,具有此属性的属性会通过 PersistentComponentState 服务自动持久化。 当组件以交互方式呈现或实例化服务时,将检索状态。

默认情况下,使用带有默认设置的序列化程序序列化 System.Text.Json 属性,并保存在预呈现 HTML 中。 序列化不是更安全的,需要保留所使用的类型。 有关详细信息,请参阅配置适用于 ASP.NET Core Blazor 的裁边器

以下计数器组件在预呈现期间保留计数器状态,并检索用于初始化组件的状态:

  • [PersistentState] 属性应用于可空 int 类型(CurrentCount)。
  • 计数器的状态在nullOnInitialized时分配,并在组件以交互方式呈现时自动还原。

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@inject ILogger<PrerenderedCounter2> Logger

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [PersistentState]
    public int? CurrentCount { get; set; }

    protected override void OnInitialized()
    {
        if (CurrentCount is null)
        {
            CurrentCount = Random.Shared.Next(100);
            Logger.LogInformation("CurrentCount set to {Count}", CurrentCount);
        }
        else
        {
            Logger.LogInformation("CurrentCount restored to {Count}", CurrentCount);
        }
    }

    private void IncrementCount() => CurrentCount++;
}

执行组件时,CurrentCount 仅在预呈现期间设置一次。 重新呈现组件时,会还原该值。 下面是示例输出。

注释

如果应用采用 交互式路由,并且通过内部 增强导航访问页面,则不会进行预呈现。 因此,必须为组件执行完整页面重载才能查看以下输出。 有关详细信息,请参阅 交互式路由和预呈现 部分。

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
CurrentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
CurrentCount restored to 96

在以下示例中,序列化同一类型的多个组件的状态:

  • 使用 [PersistentState] 属性批注的属性在预呈现期间进行序列化。
  • 指令@key属性用于确保状态与组件实例正确关联。
  • Element属性在OnInitialized生命周期方法中初始化,以避免出现 Null 引用异常,这与避免查询参数和表单数据中的 Null 引用类似。

PersistentChild.razor:

<div>
    <p>Current count: @Element.CurrentCount</p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</div>

@code {
    [PersistentState]
    public State Element { get; set; }

    protected override void OnInitialized()
    {
        Element ??= new State();
    }

    private void IncrementCount()
    {
        Element.CurrentCount++;
    }

    private class State
    {
        public int CurrentCount { get; set; }
    }
}

Parent.razor:

@page "/parent"

@foreach (var element in elements)
{
    <PersistentChild @key="element.Name" />
}

序列化服务的状态

在以下示例中,序列化依赖项注入服务的状态:

  • 使用 [PersistentState] 属性批注的属性在预呈现期间进行序列化,并在应用变为交互时进行反序列化。
  • 扩展 RegisterPersistentService 方法用于注册服务以保持持久性。 呈现模式是必需的,因为无法从服务类型推断呈现模式。 使用以下任何值:
    • RenderMode.Server:该服务可用于交互式服务器呈现模式。
    • RenderMode.Webassembly:该服务可用于交互式 Webassembly渲染模式。
    • RenderMode.InteractiveAuto:如果组件在任一模式下呈现,则服务同时可用于交互式服务器和交互式 Web 汇编呈现模式。
  • 服务在初始化交互式呈现模式时被解析,并反序列化带有 [PersistentState] 特性批注的属性。

注释

仅支持持久化范围限定的服务。

从实际服务实例标识序列化属性:

  • 此方法允许将抽象标记为持久服务。
  • 使实际实现可以是内部或不同类型的。
  • 支持不同程序集中的共享代码。
  • 导致每个实例公开相同的属性。

以下计数器服务使用 CounterTracker 属性将其当前计数属性 CurrentCount[PersistentState] 标记。 在预呈现期间,属性会被序列化,而当应用程序变得互动时,无论服务被注入在哪里,属性都会被反序列化。

CounterTracker.cs:

public class CounterTracker
{
    [PersistentState]
    public int CurrentCount { get; set; }

    public void IncrementCount()
    {
        CurrentCount++;
    }
}

Program 文件中,注册作用域服务,并使用 RegisterPersistentService 注册持久化服务。 在以下示例中,CounterTracker 服务可用于交互式服务器和交互式 WebAssembly 呈现模式,而这取决于组件是否在任一模式下呈现,因为它已注册于 RenderMode.InteractiveAuto

Program如果文件尚未使用Microsoft.AspNetCore.Components.Web命名空间,请将以下using语句添加到文件顶部:

using Microsoft.AspNetCore.Components.Web;

服务在 Program 文件中的注册位置是:

builder.Services.AddScoped<CounterTracker>();

builder.Services.AddRazorComponents()
    .RegisterPersistentService<CounterTracker>(RenderMode.InteractiveAuto);

CounterTracker 服务注入组件,并将其用于递增计数器。 为了示范以下示例,服务的 CurrentCount 属性值只在预渲染时设置为 10。

Pages/Counter.razor:

@page "/counter"
@inject CounterTracker CounterTracker

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Rendering: @RendererInfo.Name</p>

<p role="status">Current count: @CounterTracker.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    protected override void OnInitialized()
    {
        if (!RendererInfo.IsInteractive)
        {
            CounterTracker.CurrentCount = 10;
        }
    }

    private void IncrementCount()
    {
        CounterTracker.IncrementCount();
    }
}

若要使用前面的组件来演示保留 10 的 CounterTracker.CurrentCount计数,请导航到该组件并刷新浏览器,这会触发预呈现。 当预呈现发生时,可以短暂看到 RendererInfo.Name 指示“Static”,在最终渲染之前会显示“Server”。 计数器从 10 开始。

直接使用 PersistentComponentState 服务而不是声明性模型

作为使用 [PersistentState] 属性进行状态持久化的声明性模型的替代方法,可以直接使用 PersistentComponentState 服务,这为复杂的状态持久化方案提供了更大的灵活性。 调用 PersistentComponentState.RegisterOnPersisting 以注册回调以在预呈现期间保留组件状态。 在组件以交互方式渲染时,会检索状态。 在初始化代码结束时进行调用,以避免在应用关闭期间出现潜在的争用条件。

以下计数器组件在预呈现期间保留计数器状态,然后检索状态以初始化组件。

PrerenderedCounter3.razor:

@page "/prerendered-counter-3"
@implements IDisposable
@inject ILogger<PrerenderedCounter3> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 3</PageTitle>

<h1>Prerendered Counter 3</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    private void IncrementCount() => currentCount++;

    void IDisposable.Dispose() => persistingSubscription.Dispose();
}

执行组件时,currentCount 仅在预呈现期间设置一次。 重新呈现组件时,会还原该值。 下面是示例输出。

注释

如果应用采用 交互式路由,并且通过内部 增强导航访问页面,则不会进行预呈现。 因此,必须为组件执行完整页面重载才能查看以下输出。 有关详细信息,请参阅 交互式路由和预呈现 部分。

info: BlazorSample.Components.Pages.PrerenderedCounter3[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter3[0]
currentCount restored to 96

若要保留预呈现状态,请决定使用 PersistentComponentState 服务保留什么状态。 PersistentComponentState.RegisterOnPersisting 注册回调以在预呈现期间保留组件状态。 在组件以交互方式渲染时,会检索状态。 在初始化代码结束时进行调用,以避免在应用关闭期间出现潜在的争用条件。

以下计数器组件在预呈现期间保留计数器状态,然后检索状态以初始化组件。

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@implements IDisposable
@inject ILogger<PrerenderedCounter2> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose() => persistingSubscription.Dispose();

    private void IncrementCount() => currentCount++;
}

执行组件时,currentCount 仅在预呈现期间设置一次。 重新呈现组件时,会还原该值。 下面是示例输出。

注释

如果应用采用 交互式路由,并且通过内部 增强导航访问页面,则不会进行预呈现。 因此,必须为组件执行完整页面重载才能查看以下输出。 有关详细信息,请参阅 交互式路由和预呈现 部分。

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount restored to 96

持久性组件状态的序列化扩展性

使用 IPersistentComponentStateSerializer 接口实现自定义序列化程序。 如果没有注册的自定义序列化程序,序列化将回退到现有的 JSON 序列化。

自定义序列化程序在应用的 Program 文件中注册。 在以下示例中,CustomUserSerializerUser 类型注册:

builder.Services.AddSingleton<IPersistentComponentStateSerializer<User>, 
    CustomUserSerializer>();

该类型使用自定义序列化程序自动持久保存和还原:

[PersistentState] 
public User? CurrentUser { get; set; } = new();

嵌入到页面和视图中的组件 (Razor Pages/MVC)

对于嵌入到 Razor Pages 或 MVC 应用的页面或视图中的组件,必须添加持久组件状态标记帮助程序,并将 <persist-component-state /> HTML 标记置于应用布局的结束 </body> 标记内。 这仅对于 Razor Pages 和 MVC 应用是必需的。 有关详细信息,请参阅 ASP.NET Core 中的持久组件状态标记帮助程序

Pages/Shared/_Layout.cshtml:

<body>
    ...

    <persist-component-state />
</body>

交互式路由和预呈现

Routes 组件未定义呈现模式时,应用将使用每页/组件交互和导航。 使用每页/组件导航时,内部导航由 增强型路由 在应用变为交互式后进行处理。 此上下文中的“内部导航”意味着导航事件的 URL 目标是 Blazor 应用中的终结点。

Blazor 支持在 增强导航期间处理持久性组件状态。 在增强导航期间保留的状态可由页面上的交互式组件读取。

默认情况下,持久组件状态仅在最初加载到页面上时由交互式组件加载。 这可以防止在加载组件后将其他增强导航事件覆盖到同一页的重要状态,例如编辑后的 Web 窗体中的数据。

如果数据为只读且不会频繁更改,则选择加入以允许在增强导航期间通过设置AllowUpdates = true属性进行[PersistentState]更新。 这对于显示缓存数据等方案非常有用,这些缓存数据成本高昂,但不会经常更改。 以下示例演示如何对天气预报数据使用 AllowUpdates

[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }

protected override async Task OnInitializedAsync()
{
    Forecasts ??= await ForecastService.GetForecastAsync();
}

若要在预呈现期间跳过还原状态,请设置为RestoreBehaviorSkipInitialValue

[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public string NoPrerenderedData { get; set; }

若要在重新连接期间跳过还原状态,请设置为 RestoreBehaviorSkipLastSnapshot. 这对于在重新连接后确保新数据非常有用:

[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public int CounterNotRestoredOnReconnect { get; set; }

调用 PersistentComponentState.RegisterOnRestoring 以注册回调以强制控制状态的还原方式,类似于 PersistentComponentState.RegisterOnPersisting 如何完全控制状态的持久化方式。

PersistentComponentState 服务仅适用于初始页面加载,不适用于内部增强的页面导航事件。

如果应用使用持久性组件状态对页面执行完整(非增强)导航,则应用在变为交互式时可以使用持久状态。

如果已建立交互式电路,并且对利用持久性组件状态的页面执行增强导航,则现有电路中不提供 状态,供组件使用。 内部页面请求没有预渲染,并且 PersistentComponentState 服务不了解发生了增强导航。 没有向已在现有线路上运行的组件提供状态更新的机制。 原因是,Blazor 仅支持在运行时初始化时将状态从服务器传递到客户端,而不是在运行时启动之后。

PersistentComponentState中介绍了如何禁用增强导航,这虽然会降低性能,但也能避免使用内部页面请求的 Blazor 加载状态的问题。 或者,将应用更新为 .NET 10 或更高版本,在 Blazor 增强导航期间支持处理持久性组件状态。