偶然在angular-realworld-example-app这个项目的
issue
中看到有人说此项目缺乏相应齐备和完善的错误处理,因此还是别叫realword了,因为根本跑不起来,同时还贴了一篇文章来说明一个Angular
项目的错误处理应该是怎样的。这篇文章名字叫做[Error Handling & Angular][link2],写的还真是非常好。我最近也经常思考如何才能完美的优雅又省力的处理前端框架中请求错误,数据错误,兼容错误等等各式各样的错误,尽量提高页面的适应性,保证错误发生时不至于损失太多的用户体验的同时又不会在项目中混杂太多的错误处理代码导致业务代码的
bad smell
。这篇文章,虽然讲的是
Angular
的错误处理,但其中展现出对前端错误的归纳方法和处理之道,无论使用什么框架,都会收获颇多。因此翻译过来,与大家共享。
ps:本篇博客中使用的在线编辑器[StackBlitz][link3]需要翻墙才能访问,也就是说本篇博客的阅读最好在翻墙情况下进行……
再ps:我正在找不需要翻墙又比较好用的在线编辑器,等找到了就会把相应的
Demo
实例迁移过去,希望我能够早点找到……
如果你的年龄大于两岁的话,你肯定已经意识到,错误无论如何都会发生,当然,你的项目也不会例外。
当错误发生的时候,你可以无视它,任由它滋长,或者,你也可以通过做些什么,来消灭它,从而让这个世界更美好一点。
具体的,我们应该采取什么措施来处理错误呢?这取决于错误来自哪里:
外部错误
外部错误是更容易解决一些的错误,因为这类错误不是你造成的,你总可以怪罪到别的什么人头上。另一方面来说我们可以做的也并不是很多,我们只能被动的处理,并去告知这些外部错误的负责者,请求他去修复它们。
这类错误通常来自服务器,携带着一个表示错误分类的status
错误状态码和表示错误原因的message
,通过错误携带的这些信息我们可以知道发生了什么错误,做出相应的处理。
除了来自服务器的,也有一些其他的情况,例如网络连接失败,我们也是可以处理的。
但显然并不是所有的都行,像浏览器崩溃或者系统崩溃这类外部错误,我们明显的无能为力。
内部错误
内部错误就更复杂一些了,因为它要求我们去仔细的自我审查并在内心深处接受我们又一次搞砸了这个现实,然后去找到原因,修复它,被它好好的上一课。
我们可以根据谁发出的错误来对内部错误做一个分类:
服务器
服务器返回给我们的错误所包含的属性并没有什么固定的标准,至少我没找到过此类标准。
它大概会包含以下几个属性:
- status (code) : 以
4
开头的三位错误状态码,目前共有 28 个错误状态码 - name: 错误的名字,比如
HttpErrorResponse
- message: 解释错误原因的消息,形如
Http failure response for...
客户端 (浏览器)
Javascript
每当出错时,都会抛出一个使用Error
构造函数创建的类型错误。
最常见的错误类型是ReferenceError
(意指调用了不存在的变量)和 TypeError
(表示你把变量当成了函数并尝试去执行它),还有其他的5种类型错误,点链接可以详细查看。
一个客户端错误通常会包含:
- name: 例如 ReferenceError
- message: 例如 X is not defined
在更现代的浏览器中,还会有错误发生所在的 fileName文件名,lineNumber行号和 columnNumber列号,及stack(错误发生时所在的函数堆栈)。
错误,异常和调用栈
在Javascript
中,每个函数在执行时都会被按执行顺序依次添加到栈中,当函数执行完毕,返回值后,再从栈中删除。
当一个错误发生时,一个异常会在错误所在栈被抛出,同时会删除当前栈中的所有函数调用,直到其被try/catch
捕获。随后,控制权会移交给catch
块中的代码。
如果没有任何try/catch
块,则异常会移除所有嵌套的栈中的函数执行,使我们的应用彻底崩溃。
让我们来看一个例子
实际例子
- 打开例子编辑器中的
console
命令行 - 打开
app>app.components.ts
文件 - 点击
触发错误
按钮,因为引用错误ReferenceError
没有被捕获,这个错误清除了所有的函数调用栈,应用崩溃了。 - 刷新页面
- 点击
触发错误并捕获
按钮,查看错误被捕获了的情况。
你可以在console
命令行中看到,try/catch
捕获了错误,并继续执行了catch
块中的内容,避免了应用崩溃。
如何全局处理错误
默认的,angular
有自己的一个ErrorHandle
用来拦截所有内部发生的错误并打印到控制台。在上面的例子中,为了演示错误的发生,我禁用了它。
我们可以基于ErrorHandle
创建一个新的错误处理类来修改它的默认行为。
1 | // errors-handler.ts |
在定义了一个错误处理的新的类(它是一个服务)之后,我们需要告诉Angular
使用我们自己定义的类处理全局错误。
1 | // errors.module.ts |
现在,每次当新的错误发生时,Angular
都会打印一条发生错误
并跟随着具体错误信息的消息到console
控制台中。
如何识别错误
在ErrorHandler
中,我们可以识别错误属于哪种类型:
1 | // errors-handler.ts |
如何分别处理错误
server端错误
server
端和网络连接的错误会影响到我们的应用和服务器的通信。当我们的应用离线时,可能发生访问地址不存在(404),无权限访问(403)等错误。Angular
的HttpClient
会给出对应的错误信息提示,不过我们的应用并不会崩溃。但我们仍然必须去处理它,从而避免我们的应用处于数据可能会受损或者丢失的风险中。
一般来说,对于这种错误,我们需要进行一个通知:一个清晰明了的解释信息来告诉用户发生了什么,应该怎么做。
为此我们需要在应用中引入一个notification
服务。
1 | // errors-handler.ts |
我们还可以对某些类型的错误进行专门的处理,例如当 403 错误发生时,我们可以通知用户并重定向页面到登录页
最好的错误是永远不发生的错误。我们可以通过Http
拦截器 HttpInterceptor
改进我们的错误处理。通过HttpInterceptor
,我们拦截所有的服务器响应,并在请求发生错误时重试若干次。
1 | // server-errors.interceptor.ts |
同样我们需要告诉Angular
使用我们自定义的ServerErrorsInterceptor
类来拦截每个Http
请求。
1 | // errors.module.ts |
客户端错误
客户端错误看起来对我们更危险一些。它可能导致我们的应用完全崩溃,或者导致错误的数据被存储,甚至让用户花费很多时间处理的数据无法保存。
我认为在这种情况下,我们需要进行更严格的错误响应:当我们的应用程序出现问题时,我们应该停止应用,并将页面重定向到包含所有错误信息的错误专用页面,然后尽快修复错误并通知用户应用已恢复正常。
为了实现上面说的,我们需要使用一个可以从路由参数中获取错误信息并展示的错误专用组件,在错误发生时重定向到它。
1 | // errors-handler.ts |
实际例子
- 点击
Client Error
触发一个客户端错误,观察错误的处理方法 - 点击
Server Error
触发一个服务端错误,观察错误的处理方法 - 打开
app>core>errors
文件夹查看用于错误处理的ErrorsComponent
,ErrorsHandler
,ServerErrorsInterceptor
和errors-routing
的细节。
如何追踪记录错误
眼不见的心不烦…..,如果我们不知道在我们的应用中发生了什么错误,也就不会有进一步的优化和改进。
我们可以使用一个ErrorsService
错误服务来将与错误有关的上下文信息发送到我们的服务器从而记录错误发生的位置。
1 | //errors.service.ts |
然后,更改我们的错误处理类ErrorsHandler
来使用我们全新的ErrorsService
。
1 | import { ErrorHandler, Injectable, Injector} from '@angular/core'; |
现在,我们对发生的错误拥有了许多的上下文信息并将它们发送到了服务器,从而便于我们的记录和追踪,同时这也让我们的应用能够给予客户更好的体验和反馈。
实际例子
- 点击
Client Error
按钮,查看相关的文件,你会明白我们是如何向用户提供错误的详细信息。同时我们还向客户提供了一个错误ID。
当我们修复这个错误时,我们可以联系客户通知关于这个特定 ID 错误的具体情况,简直太酷了。
当然,如果你不想自己来记录和追踪管理所有错误的话,有很多现成的第三方错误追踪服务可供你选用。例如sentry,rollbar,或者 jsnlog。
404错误
404
错误是十分常见和典型的错误,它会在你请求一个服务器不存在的页面时发生。但在使用单页面应用的项目(SPA)中,页面已经在客户端,不需要通过网络请求获取,网络请求通常只被用来请求用于填充页面的数据。
所以,我们应该在什么时候展示一个404
错误页呢?那就是在我们请求一个新页面填充所需的数据时,换句话说,当路由改变,新页面开始渲染,但新页面所需的数据并不可用时。这主要分为两种情况:
- 路由
URL
改变,但新URL
是未定义或者不可用的。 - 路由守卫解析失败(对应路由所需的数据请求失败)
对于第一种情况,导航向一个不存在的URL
,我们可以声明一个通配符(\*)*来匹配所有不存在的路由导航。
1 | // errors-routing.module.ts |
通配符将匹配到的路由定向到我们上面创建的错误组件并传递一个404
错误给它。
对于第二种情况,当导航的路由守卫解析失败时,我们可以监听路由的导航错误NavigationError
。
我们在上面创建的ErrorsService
中进行NavigationError
错误的订阅。
1 | // errors.service.ts |
现在,当我们解析路由守卫失败时,将会携带着错误信息作为查询参数重定向到ErrorComponent
组件。你可以点击上面实际例子中的Go To Page
按钮来看看实际效果。
我差点忘了NavigationError
在ErrorHandler
中同样也会被捕获,这对我来说很有意义,但现在Angular
在数据请求失败时会抛出一个Uncaught(in Promise)
错误(而不是错误的具体信息,因为具体错误已经被ErrorsService
捕获了)
OK,这篇博客就是这些了。你看到了错误是如何发生的,那么,当错误出现时会发生什么,将由你来决定了。
感谢阅读!