• 第二章 基础知识(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 类的

    元素。 默认 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);
                }
            }
        }
    }
    
  • 相关阅读:
    单片机内部IO口保护电路及IO口电气特性以及为什么不同电压IO之间为什么串联一个电阻?
    vue模板语法: 插值语法和指令语法以及v-bind指令使用
    I/O
    C++【类的自动类型转换和强制类型转换】,总要了解一下
    【Linux】Linux项目自动化构建工具——make/Makefile
    屏幕开发学习 -- 迪文串口屏
    100天精通Python(爬虫篇)——第45天:lxml库与Xpath提取网页数据
    SOAP WebService 发布服务成功,但是访问404
    Docker架构简介
    02-3解析BeautifulSoup
  • 原文地址:https://blog.csdn.net/jjailsa/article/details/140361455