调试 Blazor 组件





5.00/5 (1投票)
如何调试 Blazor 组件
概述
我将从一段简短的编码旅程开始本文:一位 Blazor 新手构建一个简单的数据页面。这展示了调试的困境,并为本文的其余部分提供了组件代码。
本文的其余部分将介绍如何在组件内记录事件序列,并引入 DocumentatedComponentBase
组件来实现自动日志记录。
最后的总结提供了一些关键进程的背景信息。
存储库和包
本文的代码是 Blazor.BaseComponent 库的一部分。
DocumentatedComponentBase
组件可在 Blazr.BaseComponents
Nuget 包中找到。
我的第一个 Blazor 页面
我想进行数据库调用以获取一些数据。我认为这会花费一些时间,所以我想在发生时显示“正在加载”。我**保持简单**:避开**异步**的晦涩难懂之处。
我编写的代码如下。这一切都是同步的,带有阻塞的 Thread.Sleep
来模拟缓慢的数据存储调用。
我的期望是,当我设置 _state = "Loading"
时,组件将[以某种方式]注册该状态更改并立即重新渲染。
@page "/"
<PageTitle>The OnAfterRender Myth</PageTitle>
<h1>The OnAfterRender Myth</h1>
<div class="bg-dark text-white mt-5 m-2 p-2">
<pre>@_state</pre>
</div>
@code {
private string? _state = "New";
protected override void OnInitialized()
{
_state = "Loading";
TaskSync();
_state = "Loaded";
}
// Emulate a synchronous blocking database operation
private void TaskSync()
=> Thread.Sleep(1000);
}
我看到的是一个空白屏幕,然后是“已加载”:没有中间的“正在加载”。
我开始搜索。
StateHasChanged
我了解到 StateHasChanged
并更新了我的代码。
我现在期望在设置 _state
后组件立即渲染。
protected override void OnInitialized()
{
_state = "Loading";
StateHasChanged();
TaskSync();
_state = "Loaded";
}
但徒劳无功。这是怎么回事?“也许我发现了一个 MS Component 代码中的 bug”。
我进行了更多的搜索。
Task.Delay
我找到了 await Task.Delay(1)
。看起来是异步的,但让我们在我的代码中尝试一下。我开始输入 await
,Visual Studio 编辑器会自动将 async
添加到我的方法中
protected override async void OnInitialized()
我完成了更改。它可以编译,所以可能没问题。
我期望它能工作,但并不清楚为什么。
protected override async void OnInitialized()
{
_state = "Loading";
StateHasChanged();
await Task.Delay(1);
TaskSync();
_state = "Loaded";
}
我得到了相反的结果。“正在加载”,但没有完成到“已加载”。
现在感到困惑和沮丧,我继续搜索。
OnAfterRender
我找到了一些关于 OnAfterRender
的内容。我将其添加到我的代码中。
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
StateHasChanged();
}
我希望它能起作用,并且[松了口气]它确实起作用了。我不知道为什么[也许我自欺欺人地说我知道]。它起作用了,所以**问题解决了**。
我学到了一种编写此类场景的新模式。我在其他地方也使用了它。
我未能学到的东西
问题的真正解决方案对更有经验的程序员来说很明显。你不能混淆**同步**和**异步**世界。在大多数情况下,async void
是一种致命的混合体。请使用 OnInitializedAsync
和**异步**数据库操作。
我。我才刚刚开始涉足 Blazor 和 SPA 的道路。我的 async void
翻车事故仍然在几天或几周之后。在此期间,我学到了一个“脏”的反模式,它“有效”。我甚至可能分享它!
如何调试组件
Debug.WriteLine/Console.WriteLine
要有效调试组件,您需要实时输出信息。Debug.WriteLine
和 Console.WriteLine
是您的生命线。
我称之为**记录**,而不是**调试**。您没有使用断点,只是记录正在发生的事情,以后再进行分析。
以上面的代码为例,添加一些如下所示的日志记录
@page "/AsyncOnInitialized"
@using System.Diagnostics;
<PageTitle>Documented Async OnInitialized</PageTitle>
@{
this.Log($"Render Component.");
}
<h1>The OnAfterRender Myth</h1>
<div class="bg-dark text-white mt-5 m-2 p-2">
<pre>@_state</pre>
</div>
@code {
private string? _state = "New";
private string _id = Guid.NewGuid().ToString().Substring(0, 4);
private string _type => this.GetType().Name;
public async override Task SetParametersAsync(ParameterView parameters)
{
this.Log($"SetParametersAsync started.");
await base.SetParametersAsync(parameters);
this.Log($"SetParametersAsync completed.");
}
protected override async void OnInitialized()
{
this.Log($"OnInitialized Started.");
_state = "Loading";
StateHasChanged();
await Task.Delay(1);
TaskSync();
this.Log($"OnInitialized Continuation.");
_state = "Loaded";
this.Log($"OnInitialized Completed.");
}
protected override Task OnInitializedAsync()
{
this.Log($"OnInitializedAsync.");
return Task.CompletedTask;
}
protected override void OnParametersSet()
=> this.Log($"OnParametersSet.");
protected override Task OnParametersSetAsync()
{
this.Log($"OnParametersSetAsync.");
return Task.CompletedTask;
}
protected override bool ShouldRender()
{
this.Log($"ShouldRender.");
return true;
}
private void TaskSync()
=> Thread.Sleep(1000);
private async Task TaskAsync()
=> await Task.Yield();
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
this.Log($"First OnAfterRender.");
StateHasChanged();
}
else
this.Log($"Subsequent OnAfterRender.");
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
this.Log($"First OnAfterRenderAsync.");
else
this.Log($"Subsequent OnAfterRenderAsync.");
return Task.CompletedTask;
}
private void Log(string message)
{
message = $"{_id} - {_type} => {message}";
Debug.WriteLine(message);
Console.WriteLine(message);
}
}
运行此代码,我们现在可以看到事件序列。
30af - AsyncOnInitialized => SetParametersAsync started.
30af - AsyncOnInitialized => OnInitialized Started.
[3] => 30af - AsyncOnInitialized => OnInitializedAsync.
30af - AsyncOnInitialized => OnParametersSet.
30af - AsyncOnInitialized => OnParametersSetAsync.
30af - AsyncOnInitialized => SetParametersAsync completed.
30af - AsyncOnInitialized => Render Component.
[8] => 30af - AsyncOnInitialized => OnInitialized Continuation.
30af - AsyncOnInitialized => OnInitialized Completed.
[10] => 30af - AsyncOnInitialized => First OnAfterRender.
30af - AsyncOnInitialized => ShouldRender.
30af - AsyncOnInitialized => Render Component.
30af - AsyncOnInitialized => First OnAfterRenderAsync.
30af - AsyncOnInitialized => Subsequent OnAfterRender.
30af - AsyncOnInitialized => Subsequent OnAfterRenderAsync.
在第 3 行,事情开始出错。OnInitializedAsync
和其余的生命周期进程运行完毕[包括最终渲染],然后在第 8 行,OnInitialized
的续延运行,OnInitialized
完成。由于 SetParametersAsync
没有返回 Task 来 await
,OnInitialized
已与生命周期分离。
在第 10 行,运行 OnAfterRender
并调用 StateHasChanged
,后者渲染组件,并触发第二个 OnAfterRender
周期。
已记录的 ComponentBase
在示例中,我添加了大量的手动日志记录代码。经常这样做既耗时又乏味。虽然大多数信息都可以记录下来,但它有点笨拙,因为无法访问内部的 ComponentBase
进程。
这就是 DocumentedComponentBase
的作用。它是 ComponentBase
的一个黑盒版本,提供了对内部进程的完整日志记录。
您可以从本文的存储库中复制代码,或者安装Blazr.BaseComponents
Nuget 包,并使用Blazr.BaseComponents.ComponentBase
命名空间。
记录 AsyncOnInitialized
重构上面的初始同步代码很简单。将继承更改为 DocumentedComponentBase
,并在设置 _state
的地方添加一个 Log
行。Log
是 DocumentedComponentBase
提供的 protected
方法。
@page "/AsyncOnInitializedDocumented"
@inherits DocumentedComponentBase
<PageTitle>Documented Async OnInitialized</PageTitle>
<h1>Documented Async OnInitialized</h1>
<div class="bg-dark text-white mt-5 m-2 p-2">
<pre>@_state</pre>
</div>
@code {
private string? _state = "New";
protected override void OnInitialized()
{
this.Log($"OnInitialized - State set to Loading.");
_state = "Loading";
TaskSync();
this.Log($"OnInitialized - State set to Loaded.");
_state = "Loaded";
}
private void TaskSync()
=> Thread.Sleep(1000);
}
这是输出。
我将输出复制粘贴到一个文本文件中,然后进行注释。
===========================================
2c5b - AsyncOnInitializedDocumented => Component Initialized
2c5b - AsyncOnInitializedDocumented => Component Attached
2c5b - AsyncOnInitializedDocumented => SetParametersAsync Started
2c5b - AsyncOnInitializedDocumented => OnInitialized sequence Started
[5] => 2c5b - AsyncOnInitializedDocumented => OnInitialized - State set to Loading.
[6] => 2c5b - AsyncOnInitializedDocumented => OnInitialized - State set to Loaded.
2c5b - AsyncOnInitializedDocumented => OnInitialized sequence Completed
2c5b - AsyncOnInitializedDocumented => OnParametersSet Sequence Started
[9] => 2c5b - AsyncOnInitializedDocumented => StateHasChanged Called
2c5b - AsyncOnInitializedDocumented => Render Queued
2c5b - AsyncOnInitializedDocumented => OnParametersSet Sequence Completed
2c5b - AsyncOnInitializedDocumented => SetParametersAsync Completed
[13] => 2c5b - AsyncOnInitializedDocumented => Component Rendered
2c5b - AsyncOnInitializedDocumented => OnAfterRenderAsync Started
2c5b - AsyncOnInitializedDocumented => OnAfterRenderAsync Completed
状态在第 5 行和第 6 行设置和重置,然后调用第 9 行的 StateHasChanged
,并在第 13 行发生渲染。你可以清楚地看到,当组件在第 13 行实际渲染时,_state
是 Loaded
。在第 5 行和第 6 行之间没有魔法渲染。
记录异步解决方案
现在转到完全**异步**版本
@page "/AsyncOnInitializedAsyncDocumented"
@inherits DocumentedComponentBase
<PageTitle>Documented Async OnInitializedAsync</PageTitle>
<h1>Documented Async OnInitializedAsync</h1>
<div class="bg-dark text-white mt-5 m-2 p-2">
<pre>@_state</pre>
</div>
@code {
private string? _state = "New";
protected override async Task OnInitializedAsync()
{
this.Log($"OnInitialized - State set to Loading.");
_state = "Loading";
await TaskAsync();
this.Log($"OnInitialized - State set to Loaded.");
_state = "Loaded";
}
private async Task TaskAsync()
=> await Task.Delay(1000);
}
你会得到这个
===========================================
cf89 - AsyncOnInitializedAsyncDocumented => Component Initialized
cf89 - AsyncOnInitializedAsyncDocumented => Component Attached
cf89 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Started
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Started
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loading.
[6] => cf89 - AsyncOnInitializedAsyncDocumented => Awaiting Task completion
cf89 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
cf89 - AsyncOnInitializedAsyncDocumented => Render Queued
cf89 - AsyncOnInitializedAsyncDocumented => Component Rendered
[10] => cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
[11] => cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loaded.
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Completed
cf89 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Started
cf89 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
cf89 - AsyncOnInitializedAsyncDocumented => Render Queued
cf89 - AsyncOnInitializedAsyncDocumented => Component Rendered
cf89 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Completed
cf89 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Completed
cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed
注意
- 在第 5 行,
await
产生了一个暂停,在第 5 行和第 11 行之间有一个完整的组件渲染周期。一旦async
方法完成,就会有第二个完整的组件渲染周期。 OnInitialized{Async}/OnParametersSet{Async}
序列按正确的顺序执行。
对代码进行一项更改[将延迟缩短到 1ms]
private async Task TaskAsync()
=> await Task.Delay(1);
检查输出,并注意第一个 OnAfterRenderAsync
已从第 10 行移动到第 18 行。它已从在第一次渲染后立即执行变为在过程结束时执行。
===========================================
e945 - AsyncOnInitializedAsyncDocumented => Component Initialized
e945 - AsyncOnInitializedAsyncDocumented => Component Attached
e945 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Started
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Started
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loading.
e945 - AsyncOnInitializedAsyncDocumented => Awaiting Task completion
e945 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
e945 - AsyncOnInitializedAsyncDocumented => Render Queued
e945 - AsyncOnInitializedAsyncDocumented => Component Rendered
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loaded.
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Completed
e945 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Started
e945 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
e945 - AsyncOnInitializedAsyncDocumented => Render Queued
e945 - AsyncOnInitializedAsyncDocumented => Component Rendered
e945 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Completed
e945 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Completed
[18] => e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed
e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed
序列的变化是由进程完成所需的时间以及它们在 Synchronization Context
上的排队顺序驱动的。
总结
我们学到了什么
- 不要混合**异步**和**同步**代码。口号是**一切都异步**。
StateHasChanged
很少能解决您的问题。它要么不起作用,要么掩盖了潜在的逻辑问题。- 在
OnAfterRender
中运行非 JSInterop 代码似乎可以解决问题,但您最终需要调用StateHasChanged
。上述第 2 点随即适用。您进行了比需要更多的渲染。 - 组件代码没有 bug。您看到的行为是故意的。
- 使您的代码逻辑正确,一切都会顺理成章。
- 不要相信组件中的断点会告诉您真实的状态故事。
一些重要的注意事项
-
StateHasChanged
并不会渲染组件。它只是将组件的RenderFragment
放入渲染队列。Renderer
需要在Synchronization Context
上获得线程时间才能实际进行渲染。这只有在您的代码让出[通过一个让出async
方法]或完成时才会发生。 -
OnAfterRender
不是OnInitialized{Async}/OnParametersSet{Async}
序列的一部分。它是一个事件处理程序,在组件渲染后[就像点击按钮时会调用按钮点击处理程序一样]被调用。因为它是由不同的进程触发的,所以不能保证它何时运行[如上面两个示例所示]。 -
组件状态突变属于
OnInitialized{Async}/OnParametersSet{Async}
。不要在OnAfterRender{Async}
中突变状态。这是不合逻辑的:然后您必须调用StateHasChanged
[并进行另一次渲染周期]才能在 UI 中反映这些更改。
同步上下文
Synchronization Context
是一个所有 UI 代码都在其上运行的虚拟线程。它是异步的,但它保证了单一的执行线程,也就是说,上下文中只有一个代码片段在执行。没有两个操作会并发执行。您可以在此处阅读更多关于它的信息。
历史
- 2023 年 9 月 5 日:初始版本