前言
本文目的:
能让非前端同学大致了解下,现代『前端异常解析』是怎么做的,以及大部分的坑会是哪些
对于专业的前端同学,本文中也许有些坑,你还没有踩到,也可以看下。
从 window.onerror 说起
相信大部分接触过『前端错误监控』话题的同学都知道,通过浏览器提供的 window.onerror 能够捕获大部分的前端异常,比如 js 报错(本文讨论的话题),assets 加载等。
对于 js 报错,这个 api 会提供出报错消息,源文件,lineno, colno
(行号列号),详细的错误堆栈等消息,如下所示:
window.onerror = function(message, source, lineno, colno, error) { ... }复制代码
我们拿到这些消息后就能知道,『哦,原来的我的某个 js 里的第 x 行,第 y 列,我写错了啊』。
但是,事情真的这么简单吗?
反解压缩过的 js 代码
首先第一个现实问题,我线上的 js 代码一定是经过压缩(压缩文件传输)的。所以,在 window.onerror
这个方法里拿到的 lineno 通常是 1,colno 通常是 123456 (一个很大的数字),然后当你定位到对应源码位置的时候,通常看到这类代码:
什么鬼?js 的压缩代码不仅会压缩代码行数,还会对某些可替换的临时变量名做重命名,换成简短的形式。所以正常靠肉眼要定位到真正错误的源码是很难很费劲的。
source-map 登场
这个时候我们第一个王牌登场,既然知道是怎么压缩的,那这个压缩的对应关系也一定是有的,这个对应关系就是 source-map。
然后我们通过一些反解析 source-map 的工具,把源码的 lineno, colno
(行号,列号) 和 source-map 文件输入,就能得到知道哪行源码写错了,类似效果如下:
一些安全性的考虑
通常出于安全性考虑,source-map 文件是不会随着 js 资源文件一起发布的。js 资源的发布又是很频繁的,所以会造成大量 souce-map 文件管理的问题,后面会讲到一种方案。
现在看似我们能够 cover 住所有的前端 js 异常了。但是事实上是,如果你的前端资源是通过 cdn 的方式来部署的话,你会收到很多 script error(可能 80% 都是这个错误).
script error
script error 大部分是浏览器 安全限制导致的。通常的解决方式是,对跨域的脚本加上 crossorigin 属性就好了,但还有一个必要条件是服务端要支持这个属性。这个服务端包括:
- cdn 的服务器。
- 低端的安卓系统版本(离线包拦截请求无法设置这个属性)。
所以在开启这个属性前,一定要确认好以上两个地方是否支持,不然的话,嘿嘿,白屏!
ok,到现在我们基本解决 window.onerror 报上来的问题了。
那还有其他的前端异常吗?
成也框架,败也框架
近些年,随着前端工程化,组件化的流行,越来越多的 MVVM 的框架登上历史舞台(vue,react,angular 等)。
对前端监控来说,拿 vue 来举例。
你写的 vue 业务代码,会发现 window.onerror 是监控不到的。原因是这些框架他们内部会消化掉这些错误,必须得通过他们的 API 才能 catch 到。比如:
Vue.config.errorHandler = function (err, vm, info) { // handle error // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子}复制代码
重点注意一下,这个函数不像 window.onerror
会把错误的 lineno, colno
一起上报。只能看到一个干巴巴的错误消息,比如:
TypeError: Cannot read property 'id' of null at a.next (https://xxx.com/0-1.0.0-5ba93bf.js:1:25746) at n (https://xxx.com/x2-1.0.0-5ba93bf.js:13:1417)复制代码
这是线上代码,同样也是经过压缩的,而且由于被框架代码包了一层,在压缩过的代码里定位变得更加费劲。
那有什么方法可以拿到 lineno, colno
然后通过 source-map 反解析源码么?
答案其实就在上面,在错误堆栈里。仔细看 https://xxx.com/0-1.0.0-5ba93bf.js:1:25746
,按照 js stack 的生成规则,通常*.js:x:y
第一个冒号后面的 x 代表了 lineno
行数,第二个冒号后面的 y 代表了 colno
列数。
所以我们通过肉眼或者是可以正则解析出 lineno, colno
的,然后通过 source-map 工具能够定位出具体的 vue 代码出错位置了。
同样要问一个,那还有其他的前端异常了吗?
明明白屏了,咋不报错呢
假设不小心,写了如下的 vue 代码。
复制代码
明明里面有一个空指针会导致白屏的问题,可是你会发现 window.onerror
捕捉不到,Vue.config.errorHandler
同样也捕捉不到(不是说好框架全吃的吗!)。
仔细看,哦,原来这是写在 promise 里的代码啊,这个代码被这个 promise 吃掉了,因为这里没有 catch,所以也就不知道了。
随着 js 语言的发展,越来越多的特性被引入到浏览器中,按照新的规范,像 promise 里面的错误应该 promise 由开发者自己负责来 catch 而不会被 window.onerror 捕获。
但是理想很丰满,现实却还是 ---- 不能保证每个人都会写 catch。
所以对于 promise 的错误,我们需要做全局监听,代码如下:
window.addEventListener('unhandledrejection', function (event) { const error = event && event.reason console && console.warn && console.warn('WARNING: Unhandled promise rejection. Shame on you! Reason: ' + error) // ... report error}复制代码
同样对于这里没有上报 lineno, colno
,但是有 error stack,我们可以使用同样的套路对他们用 source-map 来反解析。