为什么 Blazor 生命周期方法被执行两次?
Why are Blazor lifecycle methods getting executed twice?
因此,随着 asp.net 核心 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()
{
}
}
我注意到生命周期方法按以下顺序执行
OnInitializedAsync()
OnParametersSetAsync()
OnInitializedAsync()
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>
TL;DR
这是因为预呈现机制将组件初始化为主机页面的一部分 _Host.cshtml
,因此第一个 http 请求将导致主机页面不仅作为脚本加载器出现blazor 应用程序,还具有静态呈现的视图。因此用户可以看到初始视图而无需等待大致如下步骤:
WebSocket 连接由 SignalR 建立。
从服务器接收到第一批渲染指令。
渲染指令应用于视图。
这不仅可以缩短用户看到初始视图之前的响应延迟,而且还有利于 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
从结果可以看出,组件加载了两次。
- 第一次在
/_Host
请求和处理页面时直接加载为一个简单的Mvc组件,必须在_Host.cshtml
中指定以下代码,调用生命周期方法第一次:
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
然后加载资源包括blazor.server.js
。
然后 blazor 应用开始渲染。
然后组件被加载为blazor组件,这里第二次调用了生命周期方法
尝试将 RenderMode.ServerPrerendered
替换为 RenderMode.Server
,然后它会按预期运行,即生命周期方法仅调用一次(当 blazor 应用程序启动时)。
结论:
默认的 RenderMode
是 ServerPrerendered
这一定意味着 Mvc 可以将组件呈现为静态内容,以便在下载和启动 blazor 应用程序之前显示页面内容,然后当 blazor 应用程序启动时,它需要在页面内容上。这一定是一种解决用户体验的方法,浏览器用户可以等待更少的时间来查看内容。
我在一个网站的保留页面上遇到了完全相同的问题,我有一个小的 css 动画,在开发中看起来很棒,但是当我把它放到现场时它 运行 两次。将 RenderMode 更改为 Server 肯定可以解决此问题,但速度会明显变慢。
有趣,因为在我这样做之前我永远不会选择它,对于最终站点我将切换回 ServerPrerendered
您可以使用 IJSRuntime 调用一些方法或 运行 一些 JS 代码一次,您可以检查 属性 IsFirtRender 只执行一次某些方法,但它不会对所有方法都有效如果在 render
之前使用该方法,则该方法有时不会
有同样的问题。
对我来说,<script src="_framework/blazor.server.js"></script>
在 _Host.cshtml
上写了两次
在这里被跟踪:https://github.com/dotnet/aspnetcore/issues/21348
同时:
(Blazor WASM) - 从
更改
protected override async Task OnParametersSetAsync()
await ReloadServerData();
}
到参数setter:
[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() 失败时,属性 也无法设置值。
因此,随着 asp.net 核心 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()
{
}
}
我注意到生命周期方法按以下顺序执行 OnInitializedAsync() OnParametersSetAsync() OnInitializedAsync() 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>
TL;DR
这是因为预呈现机制将组件初始化为主机页面的一部分 _Host.cshtml
,因此第一个 http 请求将导致主机页面不仅作为脚本加载器出现blazor 应用程序,还具有静态呈现的视图。因此用户可以看到初始视图而无需等待大致如下步骤:
WebSocket 连接由 SignalR 建立。
从服务器接收到第一批渲染指令。
渲染指令应用于视图。
这不仅可以缩短用户看到初始视图之前的响应延迟,而且还有利于 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
从结果可以看出,组件加载了两次。
- 第一次在
/_Host
请求和处理页面时直接加载为一个简单的Mvc组件,必须在_Host.cshtml
中指定以下代码,调用生命周期方法第一次:
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
然后加载资源包括
blazor.server.js
。然后 blazor 应用开始渲染。
然后组件被加载为blazor组件,这里第二次调用了生命周期方法
尝试将 RenderMode.ServerPrerendered
替换为 RenderMode.Server
,然后它会按预期运行,即生命周期方法仅调用一次(当 blazor 应用程序启动时)。
结论:
默认的 RenderMode
是 ServerPrerendered
这一定意味着 Mvc 可以将组件呈现为静态内容,以便在下载和启动 blazor 应用程序之前显示页面内容,然后当 blazor 应用程序启动时,它需要在页面内容上。这一定是一种解决用户体验的方法,浏览器用户可以等待更少的时间来查看内容。
我在一个网站的保留页面上遇到了完全相同的问题,我有一个小的 css 动画,在开发中看起来很棒,但是当我把它放到现场时它 运行 两次。将 RenderMode 更改为 Server 肯定可以解决此问题,但速度会明显变慢。
有趣,因为在我这样做之前我永远不会选择它,对于最终站点我将切换回 ServerPrerendered
您可以使用 IJSRuntime 调用一些方法或 运行 一些 JS 代码一次,您可以检查 属性 IsFirtRender 只执行一次某些方法,但它不会对所有方法都有效如果在 render
之前使用该方法,则该方法有时不会有同样的问题。
对我来说,<script src="_framework/blazor.server.js"></script>
在 _Host.cshtml
在这里被跟踪:https://github.com/dotnet/aspnetcore/issues/21348
同时:
(Blazor WASM) - 从
更改protected override async Task OnParametersSetAsync()
await ReloadServerData();
}
到参数setter:
[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() 失败时,属性 也无法设置值。