本文介绍 Blazor 如何管理未经处理的异常以及如何开发检测和处理错误的应用。
当 Blazor 应用在开发过程中运行不正常时,从该应用接收详细的错误信息有助于故障排除和修复问题。 出现错误时,Blazor 应用会在屏幕底部显示一个黄色条框:
此错误处理体验的 UI 是 Blazor 项目模板的一部分。
在 Blazor WebAssembly 应用程序中,自定义wwwroot/index.html文件中的体验:
<div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
在 Blazor Server 应用程序中,自定义Pages/_Host文件中的体验:
<div id="blazor-error-ui"> <environment include="Staging,Production"> An error has occurred. This application may no longer respond until reloaded. </environment> <environment include="Development"> An unhandled exception has occurred. See browser dev tools for details. </environment> <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
blazor-error-ui
元素被 Blazor 模板附带的样式隐藏,然后在发生错误时显示。
Blazor Server 是有状态框架。 当用户与应用交互时,它们会保持与服务器(称为线路)的连接。 线路包含活动组件实例,以及状态的许多其他方面,例如:
如果用户在多个浏览器选项卡中打开应用程序,则它们具有多个独立的线路。
Blazor 将最未经处理的异常视为致命的异常,并将其出现在线路上。 如果线路由于未经处理的异常而终止,则用户只可以通过重新加载页面来创建新线路,从而继续与应用进行交互。 已终止的线路(其他用户或其他浏览器选项卡的线路)不会受到影响。 这种情况类似于桌面应用程序崩溃—崩溃的应用程序必须重新启动,但其他应用不受影响。
当发生未处理的异常时,线路会终止,原因如下:
若要使应用在出现错误后继续操作,应用必须具有错误处理逻辑。 本文后面的部分将介绍未经处理的异常的潜在来源。
在生产环境中,不要在 UI 中呈现框架异常消息或堆栈跟踪。 呈现异常消息或堆栈跟踪可以:
如果发生未处理的异常,则会将异常记录到在服务容器中配置 ILogger 实例。 默认情况下,使用控制台日志记录提供程序 Blazor 应用日志输出到控制台输出。 请考虑使用管理日志大小和日志轮换的提供程序,将日志记录到更永久性的位置。 有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录。
在开发过程中,Blazor 通常会将异常的完整详细信息发送到浏览器的控制台,以帮助进行调试。 在生产环境中,默认情况下禁用浏览器控制台中的详细错误,这意味着不会将错误发送到客户端,但异常的完整详细信息仍记录在服务器端。 有关详细信息,请参阅 处理 ASP.NET Core 中的错误。
您必须确定要记录的事件以及记录事件的严重性级别。 恶意用户可能会特意触发错误。 例如,请勿记录一个错误,其中显示产品详细信息的组件 URL 中提供了未知 ProductId
。 不是所有的错误都应视为日志记录的高严重性事件。
框架和应用代码可能会在以下任何位置触发未经处理的异常:
本文的以下部分介绍了前面未处理的异常。
当 Blazor 创建组件的实例时:
如果任何已执行的构造函数或任何 [Inject]
属性的 setter 引发了未处理的异常,则 Blazor 服务器线路会失败。 异常是致命的,因为框架无法实例化组件。 如果构造函数逻辑可能引发异常,应用应使用带有错误处理和日志记录的try-catch语句来捕获异常。
在组件的生存期内,Blazor 调用以下生命周期方法:
OnInitialized
/ OnInitializedAsync
OnParametersSet
/ OnParametersSetAsync
ShouldRender
/ ShouldRenderAsync
OnAfterRender
/ OnAfterRenderAsync
如果任何生命周期方法以同步或异步方式引发异常,则该异常对于 Blazor 服务器线路是致命的。 若要使组件处理生命周期方法中的错误,请添加错误处理逻辑。
在下面的示例中,OnParametersSetAsync
调用方法来获取产品:
ProductRepository.GetProductByIdAsync
方法中引发的异常由 try-catch
语句处理。catch
块时:
loadFailed
设置为 true
,用于向用户显示一条错误消息。@page "/product-details/{ProductId:int}" @using Microsoft.Extensions.Logging @inject IProductRepository ProductRepository @inject ILogger<ProductDetails> Logger @if (_details != null) { <h1>@_details.ProductName</h1> <p>@_details.Description</p> } else if (_loadFailed) { <h1>Sorry, we could not load this product due to an error.</h1> } else { <h1>Loading...</h1> } @code { private ProductDetails _details; private bool _loadFailed; [Parameter] public int ProductId { get; set; } protected override async Task OnParametersSetAsync() { try { _loadFailed = false; _details = await ProductRepository.GetProductByIdAsync(ProductId); } catch (Exception ex) { _loadFailed = true; Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId); } } }
.razor
组件文件中的声明性标记被编译到称为C# BuildRenderTree
的方法中。 当组件呈现时,BuildRenderTree
执行并生成一个数据结构,该结构描述所呈现组件的元素、文本和子组件。
呈现逻辑可能引发异常。 在计算 @someObject.PropertyName
但 @someObject
null
时,会发生这种情况。 呈现逻辑引发的未经处理的异常对于 Blazor 服务器线路是致命的。
若要防止呈现逻辑中出现空引用异常,请在访问其成员之前检查 null
对象。 在以下示例中,如果 person.Address
null
,则不会访问 person.Address
属性:
@if (person.Address != null) { <div>@person.Address.Line1</div> <div>@person.Address.Line2</div> <div>@person.Address.City</div> <div>@person.Address.Country</div> }
前面的代码假定 person
不 null
。 通常,代码的结构保证在呈现组件时存在对象。 在这些情况下,不需要检查呈现逻辑中的 null
。 在前面的示例中,可以保证存在 person
因为在实例化组件时创建 person
。
使用以下代码创建事件处理程序C#时,客户端代码将触发代码调用:
@onclick
@onchange
@on...
特性@bind
在这些情况下,事件处理程序代码可能会引发未处理的异常。
如果事件处理程序引发未经处理的异常(例如,数据库查询失败),则异常对于 Blazor 服务器线路是致命的。 如果应用调用可能因外部原因而失败的代码,则使用带有错误处理和日志记录的try-catch语句来捕获异常。
如果用户代码不会捕获并处理异常,则框架将记录异常并终止线路。
例如,可以从 UI 中删除组件,因为用户已导航到另一个页面。 当从 UI 中删除实现 System.IDisposable 的组件时,框架将调用该组件的 Dispose 方法。
如果组件的 Dispose
方法引发未处理的异常,则该异常对于 Blazor 服务器线路是致命的。 如果处理逻辑可能引发异常,应用应使用带有错误处理和日志记录的try-catch语句来捕获异常。
有关组件处置的详细信息,请参阅 ASP.NET Core Blazor 生命周期。
IJSRuntime.InvokeAsync<T>
允许 .NET 代码对用户浏览器中的 JavaScript 运行时进行异步调用。
以下条件适用于带有 InvokeAsync<T>
的错误处理:
InvokeAsync<T>
的调用同步失败,则会发生 .NET 异常。 例如,对 InvokeAsync<T>
的调用可能会失败,因为不能序列化提供的自变量。 开发人员代码必须捕获异常。 如果事件处理程序或组件生命周期方法中的应用代码未处理异常,则生成的异常对于 Blazor 服务器线路是致命的。InvokeAsync<T>
的调用异步失败,则 .NET Task 会失败。 例如,对 InvokeAsync<T>
的调用可能会失败,这是因为 JavaScript 端代码引发异常或返回以 rejected
完成的 Promise
。 开发人员代码必须捕获异常。 如果使用await运算符,请考虑在包含错误处理和日志记录的try-catch语句中包装方法调用。 否则,失败的代码会导致未处理的异常,这是 Blazor 服务器线路的严重错误。InvokeAsync<T>
的调用必须在特定时间段内完成,否则调用会超时。默认超时期限为一分钟。 超时可防止代码丢失网络连接或从不发送回完成消息的 JavaScript 代码。 如果调用超时,则生成的 Task
将失败,并出现 OperationCanceledException。 捕获并处理日志记录的异常。同样,JavaScript 代码可能会启动对[JSInvokable]
属性所指示的 .net 方法的调用。 如果这些 .NET 方法引发未经处理的异常:
Promise
被拒绝。您可以选择在 .NET 端或方法调用的 JavaScript 端使用错误处理代码。
有关详细信息,请参阅 <xref:blazor/javascript-interop>。
Blazor Server 允许代码定义线路处理程序,该处理程序允许在用户线路的状态发生更改时运行代码。 线路处理程序是通过从 CircuitHandler
派生并在应用程序的服务容器中注册该类来实现的。 以下线路处理程序示例将跟踪打开的 SignalR 连接:
using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Server.Circuits; public class TrackingCircuitHandler : CircuitHandler { private HashSet<Circuit> _circuits = new HashSet<Circuit>(); public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) { _circuits.Add(circuit); return Task.CompletedTask; } public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken) { _circuits.Remove(circuit); return Task.CompletedTask; } public int ConnectedCircuits => _circuits.Count; }
使用 DI 注册线路处理程序。 为每个线路实例创建范围内的实例。 使用前面示例中的 TrackingCircuitHandler
,将创建一个单一实例服务,因为必须跟踪所有线路的状态:
public void ConfigureServices(IServiceCollection services) { ... services.AddSingleton<CircuitHandler, TrackingCircuitHandler>(); }
如果自定义线路处理程序的方法引发未经处理的异常,则该异常对于 Blazor 服务器线路是致命的。 若要容忍处理程序代码或调用方法中的异常,请使用错误处理和日志记录将代码包装在一个或多个try-catch语句中。
当某个线路由于用户已断开连接并且该框架正在清理线路状态而结束时,框架会释放该线路的 DI 范围。 释放作用域将释放任何实现 System.IDisposable的线路范围的 DI 服务。 如果任何 DI 服务在处理过程中引发未经处理的异常,则框架将记录该异常。
使用 Component
标记帮助器可以预呈现 Blazor 组件,以便在用户初始 HTTP 请求过程中返回其呈现的 HTML 标记。 此功能的工作方式如下:
disconnected
,直到用户的浏览器将 SignalR 连接回同一服务器。 建立连接后,将恢复对线路的交互,并更新组件的 HTML 标记。如果任何组件在预呈现期间引发未经处理的异常,例如,在生命周期方法或呈现逻辑中:
Component
标记帮助程序中的调用堆栈引发。 因此,整个 HTTP 请求将失败,除非开发人员代码显式捕获了异常。在正常情况下,如果预呈现失败,则继续生成并呈现组件没有意义,因为无法呈现工作组件。
若要容忍在预呈现期间可能发生的错误,必须将错误处理逻辑放置在可能引发异常的组件中。 使用带有错误处理和日志记录的try catch语句。 不要将 Component
标记帮助程序包装在 try-catch
语句中,而是将错误处理逻辑放在由 Component
标记帮助器呈现的组件中。
组件可以递归嵌套。 这适用于表示递归数据结构。 例如,TreeNode
组件可以为节点的每个子元素呈现更多 TreeNode
组件。
以递归方式呈现时,请避免将导致无限递归的编码模式:
呈现过程中的无限循环:
在这些情况下,受影响的 Blazor 服务器线路会失败,并且该线程通常会尝试执行以下操作:
若要避免无限递归模式,请确保递归呈现代码包含合适的停止条件。
大多数 Blazor 组件都作为razor文件实现,并且编译为生成可对 RenderTreeBuilder
进行操作以呈现其输出的逻辑。 开发人员可以使用过程C#代码手动实现 RenderTreeBuilder
逻辑。 有关详细信息,请参阅 ASP.NET Core Blazor 高级方案。
警告
使用手动渲染树生成器逻辑被视为一种高级不安全的方案,不建议用于常规组件开发。
如果编写 RenderTreeBuilder
代码,开发人员必须保证代码的正确性。 例如,开发人员必须确保:
OpenElement
和 CloseElement
的调用已正确平衡。手动呈现树生成器逻辑不正确可能会导致任意未定义的行为,包括崩溃、服务器挂起和安全漏洞。
请考虑在相同程度的复杂性上手动呈现树生成器逻辑,并使用与手动编写程序集代码或 MSIL 指令相同的危险级别。