雷达智富

首页 > 内容 > 程序笔记 > 正文

程序笔记

Blazor ServerPrerendered模式OnInitialized{Async}执行两次

2024-10-13 13

创建Blazor应用,刷新页面调试时发现OnInitialized会执行两次。 这里需要注意,进入这个站点的第一个页面的OnInitialized会被执行两次,例如我在浏览器输入URL进去了A页面,那么A页面的OnInitialized会执行两次。然后我通过A页面上的Link进入B页面,B页面的OnInitialized只会执行一次。

OnInitialized执行两次的原因

查看微软Blazor文档作了如下解释:

预呈现后的有状态重新连接

在 Blazor Server 应用中,当 RenderMode 为 ServerPrerendered 时,组件最初作为页面的一部分静态呈现。 浏览器重新建立与服务器的 SignalR 连接后,将再次呈现组件,并且该组件为交互式。 如果存在用于初始化组件的 OnInitialized{Async} 生命周期方法,则该方法执行两次:

在静态预呈现组件时执行一次。
在建立服务器连接后执行一次。

在最终呈现组件时,这可能导致 UI 中显示的数据发生明显变化。 若要避免在 Blazor Server 应用中出现此双重呈现行为,请传递一个标识符以在预呈现期间缓存状态并在预呈现后检索状态。

一开始看这个文档有点难以理解,我们用通俗的话来解释一下:

预呈现ServerPrerendered是一个网页的所有元素都在服务器上编译并将静态 HTML 提供给客户端的过程。 

ServerPrerendered模式下第一次调用OnInitialized发生再服务器上,服务器必须完成创建静态 html 网站的所有工作,并将内容发送给用户后,第二个 OnInitialized开始执行。

ServerPrerendered用于帮助 SPA(单页应用程序)改进其 SEO(搜索引擎优化)。 另一个好处是网站加载速度似乎更快。这一切都发生在非常短的时间内,大多数最终用户是无法察觉的。

直观的来看,如果使用ServerPrerendered模式,你右键网页查看源文件那么它是输出服务端渲染的内容的。如果使用Server模式,那么右键查看源文件没有实际内容。

如何避免OnInitialized执行两次

将render-mode改为server(SEO不友好)

根据文档的提示,我们可以通过修改_Host.cshtml里的render-mode="Server"来避免OnInitialized执行两次。

<component type="typeof(App)" render-mode="ServerPrerendered" />
// 修改为
<component type="typeof(App)" render-mode="Server" />

这样修改后刷新页面只会执行一次OnInitialized。但是,如果我们对SEO搜索引擎优化有要求,那么这样做显然是不行的。

持久保存预呈现组件的状态

之前我一直无法理解“传递一个标识符以在预呈现期间缓存状态并在预呈现后检索状态”,后来看到了persist-component-state提供的解决方案。它的用法也比较简单,先在_Host.razor中添加标签<persist-component-state />

<body>
    <component type="typeof(App)" render-mode="ServerPrerendered" />
    ...
    <persist-component-state />
</body>

然后page页面的示例代码是这样的:

@implements IDisposable
@inject PersistentComponentState ApplicationState

<h1>Hello, world!</h1>
<p>Status: @Status</p>

@code {
    public string? Status { get; set; } = "loading";
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = 
            ApplicationState.RegisterOnPersisting(PersistData);

        if (!ApplicationState.TryTakeFromJson<string>(
            "Status", out var restored))
        {
            Status = await GetStatus();
        }
        else
        {
            Status = restored!;
        }
    }

    private async Task<string> GetStatus()
    {
        await Task.Delay(3000);
        return "loaded";
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("Status", Status);
        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}

示例代码里GetStatus方法模拟需要花费时间的查询,所以故意延迟3秒返回结果。

这样刷新页面时,实际执行了两次OnInitializedAsync,第一次在服务端执行OnInitializedAsync时调用GetStatus查询并缓存了Status,第二次执行OnInitializedAsync时直接从缓存拿到了Status,这样可以避免第二次重复查询浪费性能。这里不需要担心时效性问题,这个缓存范围仅在这个请求,请注意实现了IDisposable接口,会Dispose掉。再次刷新页面时还会调用GetStatus查询并缓存Status,第二次再直接从缓存里拿Status。如果从别的页面跳转过来,只会执行一次OnInitializedAsync并且执行到GetStatus。

用户体验上来看,第一次打开这个页面,白屏loading了3秒,然后输出页面,页面上显示的是Status: loaded。 也许会有人觉得奇怪,那还要loading状态做什么?

是这样的,如果用户先打开了另外一个页面,然后那个页面上有一个Link到上面示例代码的页面,那么点Link过来的时候,页面不需要白屏加载,而是直接先显示Status: loading, 然后等3秒后变成Status: loaded。 而且OnInitializedAsync只执行了一次。

如果改成render-mode="Server"后,用户体验是这样的:

页面不会白屏等待(不会Delay等待3秒),而是直接输出Status: loading,然后执行OnInitializedAsync等待3秒得到Status后页面变成Status:loaded。右键查看网页源文件的话没有实际输出内容(SEO不友好)。

下面两个图是ServerPrerendered和Server两种模式下查看网页源文件的区别,大家可以对比一下:

以上是作者做了很多调试后得到的一些结果,希望对大家理解Blazor的ServerPrerendered和Server的区别有所帮助。如果有什么错误或者更好的建议,请留言分享。

更新于:5天前
赞一波!

文章评论

评论问答