那些年,我们解析过的前端异常

前言

本文目的:

能让非前端同学大致了解下,现代『前端异常解析』是怎么做的,以及大部分的坑会是哪些

对于专业的前端同学,本文中也许有些坑,你还没有踩到,也可以看下。

从 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 属性就好了,但还有一个必要条件是服务端要支持这个属性。这个服务端包括:

  1. cdn 的服务器。
  2. 低端的安卓系统版本(离线包拦截请求无法设置这个属性)。

所以在开启这个属性前,一定要确认好以上两个地方是否支持,不然的话,嘿嘿,白屏!

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 代码。

<template>
</template>

<script>
export default {
 mounted () {
   const somePromise = new Promise(_ => {
     let a
     a.doingNothing // 空指针,出错啦!!!
   })

   somePromise.then(console.log)
 }
}
</script>
复制代码

明明里面有一个空指针会导致白屏的问题,可是你会发现 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 来反解析。



猜你喜欢

转载自juejin.im/post/5c765c63e51d453ebd4c7b7a
今日推荐