异常处理
在Blazor应用中,启用了服务器交互式渲染的 Razor 组件在服务器上是有状态的。 当用户与服务器上的组件交互时,他们会保持与服务器的连接,称为线路(SignalR)。如果用户在多个浏览器标签页中打开应用,则用户就会创建多条独立线路。Blazor 将大部分未经处理的异常视为发生该异常的线路的严重异常。 如果线路由于未经处理的异常而终止,则用户只能重新加载页面来创建新线路,从而继续与应用进行交互。 而其他页签为单独线路,所以不受影响。
因此,在进行Blazor项目的开发时,对异常的处理十分重要。
一、开发过程中的异常处理
默认情况下(使用Blazor web App Auto项目模板),当 Blazor 应用在开发过程中出现错误时,Blazor 应用会在屏幕底部显示一个浅黄色条框:
- 在开发过程中,这个条框会将你定向到浏览器控制台,你可在其中查看异常。
- 在生产过程中,这个条框会通知用户发生了错误,并建议刷新浏览器。
异常模拟
在Counter
组件中抛出异常
......
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
throw new Exception("Test");
}
}
上面这个异常提示的UI是来自于Blazor项目模板的,存放在项目中的MainLayout.razor
组件里,在MainLayout.razor.css
中设置了blazor-error-ui
类的样式为display: none
,因此默认是隐藏的。当发生异常时,框架会将其样式修改为display: block
。
自定义异常样式
我们可以在模板的基础上去自定义异常信息的展示,例如,对MainLayout.razor
组件进行如下修改:
示例-MainLayout.razor
<div id="blazor-error-ui" data-nosnippet>
<span>发生异常拉~~~~</span>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
如果有需要,可以在MainLayout
组件中注入IHostEnvironment
,从而根据不同的环境使用不同的异常信息展示。
示例-MainLayout.razor
HostEnvironment.IsProduction()
:当前环境是否为生产环境。
@inject IHostEnvironment HostEnvironment
......
<div id="blazor-error-ui" data-nosnippet>
@if (HostEnvironment.IsProduction())
{
<span>An error has occurred.</span>
}
else
{
<span>An unhandled exception occurred.</span>
}
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
二、异步线程中的异常处理
如果希望在Razor组件的生命周期外(异步线程中)发生的异常时,使用Blazor的异常处理机制(例如边界异常、默认的开发过程中的异常处理等),则需要将捕获到的异常传递给 DispatchExceptionAsync(Excetion ex)
。
- 其实这点跟Winform或者WPF的异步UI访问是类似的,在异步线程中进行UI的访问要扔给UI线程的调度器去处理。
看下面的例子,在Counter组件的计数方法中,开一条新的线程然后直接抛出异常,异常触发后,Blazor的异常处理机制并没有触发,UI没有变化。
Counter.razor
......
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
Task.Run(() =>
{
throw new Exception("Test");
});
}
}
此时,想要Blazor的异常处理机制能够正常运作,可以采取如下两种方式:
- 通过
await
或Wait()
等方式,等待线程的运行结果,这样异步线程中的异常就可以在同步到当前线程中抛出,被Blazor所捕获处理。 - 在线程中将异常传递给
DispatchExceptionAsync(Excetion ex)
方法。
Counter.razor
......
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
Task.Run(() =>
{
try
{
throw new Exception("Test");
}
catch (Exception ex)
{
DispatchExceptionAsync(ex);
}
});
}
}
全局异常处理
一、异常边界(内置)
1、异常边界组件的使用
Blazor的内置组件ErrorBoundary
提供了一种用于处理异常的便捷方法:
- 在未发生错误时渲染其子内容。
- 在引发未处理的异常时渲染错误 UI。
全局异常边界
要以全局方式实现异常边界,可以在应用主布局的正文内容周围添加边界。
MainLayout.razor
......
<article class="content px-4">
<ErrorBoundary>
@Body
</ErrorBoundary>
</article>
......
需要注意的是,如果异常边界所在组件不是交互式渲染模式的,则只能在静态渲染期间在服务器上起作用。 例如,当组件生命周期方法中引发错误时,异常边界起效果,在渲染完成后抛出的异常,则不起效果(就算其子组件为交互式也不行);如果异常边界所在的组件是交互式的,则在交互过程中也能生效。
在这里,全局的异常边界是在MainLayout
组件上使用的(MainLayout
无法设置渲染模式,只能是静态),默认情况下就仅在静态渲染阶段起效果,如果希望全局异常边界在MainLayout
组件和其余组件上启用交互性,则需要在Components/App.razor
中,给HeadOutlet
和Routes
组件启用交互式渲染模式。
App.razor
<!DOCTYPE html>
<html lang="en">
<head>
......
<HeadOutlet @rendermode="InteractiveServer"/>
</head>
<body>
<Routes @rendermode="InteractiveServer" />
......
</body>
</html>
局部的异常边界
如果不希望从 Routes
组件跨整个应用启用服务器交互性,可以单独对组件使用异常边界。
ErrorTestContent.razor
<h3>ErrorTestContent</h3>
<button @onclick="ErrorHappen">触发异常</button>
@code {
private void ErrorHappen()
{
throw new Exception("OK");
}
}
ErrorTest.razor
@page "/ErrorTest"
@rendermode InteractiveAuto
<ErrorBoundary>
<ErrorTestContent/>
</ErrorBoundary>
全局异常边界的重置处理
有一点需要注意的是,Blazor 将大部分未经处理的异常视为发生该异常的线路的严重异常。 如果线路由于未经处理的异常而终止,则用户只能重新加载页面来创建新线路,从而继续与应用进行交互。也就是说,Blazor的异常机制在展示了未处理异常之后,会中断SingleR连接,需要重新加载页面来创建新线路,从而继续与应用进行交互。
由于全局异常边界是在布局中定义的,因此在错误发生后无论用户导航到哪个页面,都会看到异常提示的UI,因此建议在大多数场景下缩小异常边界的范围。 如果设置了较广泛的异常边界,则可以通过调用异常边界的 Recover
方法,在后续页面导航事件中将其重置为非错误状态,以此来重置异常提示的UI。
- 异常UI的重置需要在
ErrorBoundary
足组件上使用@ref
属性捕获边界的引用,然后在生命周期函数OnParametersSet
中使用Recover
在异常边界上触发恢复。
MainLayout.razor
......
<article class="content px-4">
<ErrorBoundary @ref="errorBoundary">
@Body
</ErrorBoundary>
</article>
......
@code{
private ErrorBoundary? errorBoundary;
protected override void OnParametersSet()
{
errorBoundary?.Recover();
}
}
为了避免无限循环,其中恢复只会重新渲染再次引发错误的组件。
2、异常边界组件的自定义样式
默认情况下,ErrorBoundary
组件会为其错误内容渲染具有 blazor-error-boundary
CSS 类的空 <div>
元素。 默认 UI 的颜色、文本和图标是使用wwwroot
文件夹中应用样式表中的 CSS 定义的,因此我们也可以自定义异常 UI。
自定义异常边界组件的样式需要通过ChildContent
和ErrorContent
属性来组合完成。
示例
<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
</ErrorContent>
</ErrorBoundary>
二、自定义全局异常处理
除了直接使用内置的ErrorBoundary
组件来处理异常外,我们还可以自定义异常处理组件,然后通过CascadingValue
将我们自定义的异常组件向下传递给子孙组件,在子孙组件中去使用我们自定义异常组件中的异常处理方法。
使用自定义异常组件来处理异常可以比ErrorBoundary
组件更为灵活且高度自定义,可以根据我们自己的业务需求去渲染UI,此外对于整个项目而言,有了更加统一、规范的异常处理方法。
创建自定义异常组件
MyErrorHandler.razor
- 其中
RenderFragment
的用法可以查看组件章节,是固定的使用方式。主要用于将子组件内容封装到ChildContent
属性中,方便我们在组件中放置、处理子组件。
@inject ILogger<Error> Logger
<h3>MyErrorHandler</h3>
@if (HasError)
{
<h1>异常发生了</h1>
}
else
{
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
}
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
private bool _hasError;
public bool HasError
{
get { return _hasError; }
set {
_hasError = value;
StateHasChanged();
}
}
public void ProcessError(Exception ex)
{
Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}",
ex.GetType(), ex.Message);
}
}
使用自定义异常组件
如果希望进行全局的异常处理,可以使用自定义的异常组件将Routes
组件包起来。
Routes.razor
@using BlazorServer.Components.Pages
<MyErrorHandler>
<Router AppAssembly="typeof(Program).Assembly">
......
</Router>
</MyErrorHandler>
如果希望在SSR或CSR中也起作用,那么跟异常边界的处理一样,要在App.Razor中,对Routers
和HeadOutlet
组件使用InteractiveServer
渲染模式。
App.razor
<!DOCTYPE html>
<html lang="en">
<head>
......
<HeadOutlet @rendermode="InteractiveServer"/>
</head>
<body>
<Routes @rendermode="InteractiveServer" />
......
</body>
</html>
处理异常:
在任意的子孙组件中,通过CascadingParameter
特性,接手MyErrorHandler组件对象,并进行异常处理。这里拿Counter.Razor组件做示范。
Counter.razor
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
[CascadingParameter]
public MyErrorHandler? MyErrorHandler { get; set; }
private int currentCount = 0;
private void IncrementCount()
{
try
{
currentCount++;
throw new Exception("Counter组件发生异常了");
}
catch (Exception ex)
{
if (MyErrorHandler is object)
{
var e = MyErrorHandler.HasError;
MyErrorHandler.HasError = true;
MyErrorHandler.ProcessError(ex);
}
}
}
}