第二章 基础知识(5) - 异常处理

异常处理

在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的异常处理机制能够正常运作,可以采取如下两种方式:

  • 通过awaitWait()等方式,等待线程的运行结果,这样异步线程中的异常就可以在同步到当前线程中抛出,被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中,给HeadOutletRoutes组件启用交互式渲染模式。

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。
自定义异常边界组件的样式需要通过ChildContentErrorContent属性来组合完成。

示例

<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中,对RoutersHeadOutlet组件使用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);
            }
        }
    }
}

相关推荐

  1. 第二:计算机系统基础知识之计算机网络

    2024-07-12 10:18:06       29 阅读
  2. 第二:计算机系统基础知识之系统工程

    2024-07-12 10:18:06       25 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-12 10:18:06       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-12 10:18:06       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-12 10:18:06       58 阅读
  4. Python语言-面向对象

    2024-07-12 10:18:06       69 阅读

热门阅读

  1. Perl 语言进阶学习

    2024-07-12 10:18:06       28 阅读
  2. 超参数的艺术:Mojo模型与动态超参数调整

    2024-07-12 10:18:06       27 阅读
  3. 浅拷贝和深拷贝浅析

    2024-07-12 10:18:06       24 阅读
  4. easyX的基本绘制使用案例

    2024-07-12 10:18:06       26 阅读
  5. 基于gunicorn+flask+docker模型高并发部署

    2024-07-12 10:18:06       24 阅读
  6. 高级IO_多路转接之ET模式Reactor

    2024-07-12 10:18:06       16 阅读