预呈现 ASP.NET Core Razor 组件

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文的 .NET 9 版本

本文介绍了 Razor 中服务器呈现组件的 Blazor Web App 组件预呈现场景。

预呈现是最初在服务器上呈现页面内容的过程,而无需为呈现的控件启用事件处理程序。 服务器会根据初始请求尽快输出页面的 HTML UI,这会让应用感觉对用户的响应更强。 预呈现还可以通过呈现搜索引擎用于计算网页排名的初始 HTTP 响应的内容,来改进搜索引擎优化 (SEO)

保留预渲染状态

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

请考虑以下 PrerenderedCounter1 计数器组件。 该组件在预呈现期间在 OnInitialized 生命周期方法中设置初始随机计数器值。 与客户端建立 SignalR 连接后,组件重新呈现,并且当第二次执行 OnInitialized 时,将替换初始计数值。

PrerenderedCounter1.razor:

@page "/prerendered-counter-1"
@rendermode @(new InteractiveServerRenderMode(prerender: true))
@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 应用的页面或视图中的组件,使用持久组件状态标记帮助程序)。

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

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

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

  • [SupplyParameterFromPersistentComponentState] 属性应用于 CounterState 类型(State)。
  • 计数器的状态在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: @State?.CurrentCount</p>

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

@code {
    [SupplyParameterFromPersistentComponentState]
    public CounterState? State { get; set; }

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

    private void IncrementCount()
    {
        if (State is not null)
        {
            State.CurrentCount++;
        }
    }

    public class CounterState
    {
        public int CurrentCount { get; set; }
    }
}

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

注意

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

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

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

  • 使用 [SupplyParameterFromPersistentComponentState] 属性批注的属性在预呈现期间进行序列化和反序列化。
  • 指令@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 {
    [SupplyParameterFromPersistentComponentState]
    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" />
}

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

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

注意

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

CounterService.cs:

public class CounterService
{
    [SupplyParameterFromPersistentComponentState]
    public int CurrentCount { get; set; }

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

Program.cs中:

builder.Services.AddPersistentService<CounterService>(RenderMode.InteractiveAuto);

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

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

作为使用 [SupplyParameterFromPersistentComponentState] 属性进行状态持久化的声明性模型的替代方法,可以直接使用 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

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

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

嵌入到页面和视图中的组件 (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 终结点。

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

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

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

.NET 10(2025 年 11 月)正在考虑有关应对此应用场景的 Blazor 框架的其他工作。 有关不支持的解决方法‡ 详细信息和社区讨论,请参阅支持跨增强页面导航的持久性组件状态 (dotnet/aspnetcore #51584)。 不支持的解决方法未被微软认可在 Blazor 应用中使用。 自行承担风险地使用第三方包、方法和代码。

PersistentComponentState中介绍了如何禁用增强导航,这虽然会降低性能,但也能避免使用内部页面请求的 Blazor 加载状态的问题。

预呈现指南

预呈现指南按主题组织在 Blazor 文档中。 以下链接涵盖整个文档中所有按主题设置的预呈现指南:

对于 .NET 7 或更早版本,请参阅 Blazor WebAssembly 安全附加方案:预呈现身份验证。 查看本部分中的内容后,将文档文章版本选择器下拉列表重置为最新的 .NET 版本,以确保在后续访问时加载最新版本的文档页面。