为什么Blazor的生命周期方法会被执行两次?

84

随着asp.net core 3.0和blazor 1.0的发布,我开始正式使用blazor。当将Blazor组件代码拆分成代码后,我使用以下内容:

public class LogoutModel : BlazorComponent
{
}

不幸的是,BlazorComponent已经不存在了,所以我转到了ComponentBase。不确定这个变化是什么时候发生的...

现在我的代码看起来像这样:

public class LogoutModel : ComponentBase
{
    protected override async Task OnInitializedAsync()
    {
    }

    protected override async Task OnParametersSetAsync()
    {
    }
}

我注意到生命周期方法以以下顺序执行:

  1. OnInitializedAsync()
  2. OnParametersSetAsync()
  3. OnInitializedAsync()
  4. OnParametersSetAsync()

我不太确定为什么每个方法都执行了两次。

这是我的 Blazor 文件的样子。

@page  "/account/logout"
@inherits LogoutModel

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title></title>
</head>
<body>
    Logout page
</body>
</html>

1
你正在使用哪种托管模型? - Alsein
1
我正在使用 Blazor Server。 - mko
6个回答

131

简述

这是因为预渲染机制将组件作为主页_Host.cshtml的一部分进行初始化,因此第一个HTTP请求会返回一个主页,不仅作为Blazor应用程序的脚本加载器,而且还带有静态呈现的视图。因此,用户可以在无需等待以下大致步骤的情况下查看初始视图:

  • SignalR建立WebSocket连接。

  • 从服务器接收到第一批渲染指令。

  • 将渲染指令应用于视图。

这不仅会缩短用户查看初始视图之前的响应延迟,还有益于SEO。预渲染的视图将在Blazor应用程序正常启动后被真正的Blazor组件替换。

新项目模板默认启用预渲染功能,因此您必须选择以下之一:

  • 正确处理该组件被预渲染的情况(可能通过检查是否可以从依赖项注入解析IJSRuntime来实现)。

  • 通过修改_Host.cshtml禁用预渲染功能,将其替换为

<component type="typeof(App)" render-mode="ServerPrerendered" />

使用

<component type="typeof(App)" render-mode="Server" />

对于旧版本,替换为

@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))

使用

@(await Html.RenderComponentAsync<App>(RenderMode.Server))

原始回答

我进行了一项测试,使用全新的blazorserver项目,在调用生命周期方法时进行了记录,并得到了以下输出:

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
      User profile is available. Using 'C:\Users\Alsein\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\repos\HelloWorld
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/_Host'
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[3]
      Route matched with {page = "/_Host"}. Executing page /_Host
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[103]
      Executing an implicit handler method - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[104]
      Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
crit: HelloWorld.MyBase[0]
      OnInitializedAsync
crit: HelloWorld.MyBase[0]
      OnParameterSetAsync
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[4]
      Executed page /_Host in 122.3724ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/_Host'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 216.7341ms 200 text/html; charset=utf-8
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/site.css
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/_framework/blazor.server.js
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/bootstrap/bootstrap.min.css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/site.css'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\site.css'
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/_framework/blazor.server.js'. Physical path: 'N/A'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 44.733000000000004ms 200 text/css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/bootstrap/bootstrap.min.css'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\bootstrap\bootstrap.min.css'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 55.3613ms 200 text/css
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 55.569900000000004ms 200 application/javascript
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/open-iconic/font/css/open-iconic-bootstrap.min.css
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/open-iconic/font/css/open-iconic-bootstrap.min.css'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 4.5189ms 200 text/css
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:5001/_blazor/negotiate text/plain;charset=UTF-8 0
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 GET https://localhost:5001/css/open-iconic/font/fonts/open-iconic.woff
info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2]
      Sending file. Request path: '/css/open-iconic/font/fonts/open-iconic.woff'. Physical path: 'D:\repos\HelloWorld\wwwroot\css\open-iconic\font\fonts\open-iconic.woff'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 4.3562ms 200 application/font-woff
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/_blazor/negotiate'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/_blazor/negotiate'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 24.7409ms 200 application/json
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:5001/_blazor?id=7oyJvbydrUy9tqlsH_DHzQ
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/_blazor'
crit: HelloWorld.MyBase[0]
      OnInitializedAsync
crit: HelloWorld.MyBase[0]
      OnParameterSetAsync

从结果中我们可以看到,该组件被加载了两次。

  • 第一次是作为简单的Mvc组件直接在页面请求时加载,并由/_Host处理,必须通过以下代码在_Host.cshtml中指定,该代码会第一次调用生命周期方法:
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
  • 然后加载资源,包括 blazor.server.js

  • 接着 Blazor 应用程序开始渲染。

  • 然后组件作为 Blazor 组件加载,此时生命周期方法被调用第二次。

试着将 RenderMode.ServerPrerendered 替换为 RenderMode.Server,这样就会按预期行事,即生命周期方法仅在 Blazor 应用程序启动时调用一次。

结论: 默认的 RenderModeServerPrerendered,这意味着 Mvc 可以将组件渲染为静态内容,以便在 Blazor 应用程序下载和启动之前显示页面内容,然后当 Blazor 应用程序启动时,它接管页面内容。这必须是提高用户体验的一种解决方案,因为浏览器用户可以等待更短的时间来查看内容。


2
谢谢您抽出时间查看这个问题,这对我帮助很大。 - Aurelien B
2
谢谢,这是微软文档 - Jordan Ryder
如果这是Nuxt.js,那么这将类似于混合模式,在该模式下,组件或页面的某些部分可以在服务器上进行可控制的预渲染,以加快速度,或者使用仅在服务器端可用(或应该仅在服务器端可用)的某些服务。必须有一种方法来检查组件在哪里被渲染,在生命周期方法中,我想我在某个地方看到过它,但现在我不记得了。然后您只需进行if检查并相应地进行操作即可。 - Paul-Sebastian Manole
如果我可以添加一些内容,那么你需要在 _Host.cshtml 文件的 <app> 元素上将 render-mode="ServerPrerendered" 更改为 render-mode="Server" --- 请参见此处 - Khalid Ab
1
@KhalidAb 这篇答案是在 Blazor 的第一个 GA 版本发布之前写的。现已更新。 - Alsein
显示剩余3条评论

4
你可以使用这种方法,而且只需要一次。
protected override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            //do something
            StateHasChanged();
        }
        return Task.CompletedTask;
    }

但是这个 firstRender 参数只存在于这个特定的事件中。 - undefined

3

我遇到了一个与网站保持页面有关的问题,我在其中添加了一些 CSS 动画,在开发时看起来非常棒,但是当我将其发布后它运行了两次。将 RenderMode 更改为 Server 肯定可以解决此问题,但它似乎明显变慢。

很有趣,因为我在没有做这个之前从来没有想到过这个问题。对于最终的网站,我将会切换回 ServerPrerendered。


1
我切换到“Server”但仍然调用两次。protected override async Task OnInitializedAsync() { await GetData(); } - Jon
@Jon - 请看下面的答案 - 它可以解决你的问题。 - David Bouška

1

正在被追踪: https://github.com/dotnet/aspnetcore/issues/21348

同时:

(Blazor WASM) - 修改自:

protected override async Task OnParametersSetAsync()
    await ReloadServerData();
}

给参数设置器:

[Parameter]
public string Foo
{
    get => _foo;
    set
    {
        // if you put a breakpoint here, you will realize, that this setter
        // gets called multiple times, but only once actually changes value
        if (_foo == value)
            return;

        _foo = value;

        ReloadServerData();
    }
}

必须要清楚,当ReloadServerData()失败时,该属性的值也无法设置。


如果我的参数是复杂类型,例如Class类型,该怎么办? - Kumar
这取决于具体情况,需要您自己决定是否更改对象 - 可以参考 https://dev59.com/Omkv5IYBdhLWcg3wpCXJ - David Bouška
1
或者,如果您需要检测某个复杂对象的特定属性的更改,请使用自定义事件,然后订阅它。 - David Bouška

0
我遇到了同样的问题。 对我来说,问题在于_Host.cshtml上写了两次<script src="_framework/blazor.server.js"></script>

0

你可以使用IJSRuntime调用一些方法或运行一些JS代码,但只能执行一次。为了实现这一点,您可以检查属性IsFirstRender,仅执行一次某些方法,但并不适用于所有方法,有时会出现问题,如果该方法在渲染之前使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接