前端监控之处理异常的正确姿势

Posted 恪愚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端监控之处理异常的正确姿势相关的知识,希望对你有一定的参考价值。

前端异常

一般来说,根据笔者的目前研究,前端异常大体上可以分为两类:由于对语法的不了解、机制的不清楚或是没有做好降级处理而“主动”造成的错误(多为js异常)和由于资源加载、第三方库、浏览器本身机制造成的“被动异常”。

第二种异常的解决方式很多,通过各种手段也大多可以避免(比如更换源这种常用手段)。我们主要来说一下第一种!

函数的常见处理

我们知道,js中充斥着大量的函数,它们承担封装某个具体功能的作用。但是在看许多代码时,笔者发现了一个问题:我们总喜欢用 return false/true; 来表示该函数内部功能代码的执行错误/正确。
《代码大全2》中说:“使用throw,而不是return false”。笔者深以为然。在我司的前端规范中也有类似的话!这很重要。

通过报错,我们可以传递许多字段信息出来,这些信息被捕获到以后可以成为分析用户行为、监控、定位前端异常的利器。我司自研的前端监控工具spider就是这么做的。

常见JS执行错误

SyntaxError

解析时发生语法错误

// 控制台运行
const xx,

window.onerror捕获不到SyntxError,一般SyntaxError在构建阶段,甚至本地开发阶段就会被发现。

TypeError

值不是所期待的类型

// 控制台运行
const person = void 0
person.name

ReferenceError

引用未声明的变量

// 控制台运行
nodefined

RangeError

当一个值不在其所允许的范围或者集合中

(function fn ( ) { fn() })()

网络错误

ResourceError

资源加载错误

new Image().src = '/remote/image/notdeinfed.png'

HttpError

Http请求错误

// 控制台运行
fetch('/remote/notdefined', {})

搜集错误

所有起因来源于错误,那我们如何进行错误捕获。

try/catch

能捕获常规运行时错误,但是对于语法错误和异步错误不行

// 常规运行时错误,可以捕获
try {
  console.log(notdefined);
} catch(e) {
  console.log('捕获到异常:', e);
}

// 语法错误,不能捕获
try {
  const notdefined,
} catch(e) {
  console.log('捕获到异常:', e);
}

// 异步错误,不能捕获
try {
  setTimeout(() => {
    console.log(notdefined);
  }, 0)
} catch(e) {
  console.log('捕获到异常:',e);
}

try/catch有它细致处理的优势,但缺点也比较明显。

window.onerror

window.onerror —— 当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件。

/**
* @param {String}  message    错误信息
* @param {String}  source    出错文件
* @param {Number}  lineno    行号
* @param {Number}  colno    列号
* @param {Object}  error  Error对象
*/

window.onerror = function(message, source, lineno, colno, error) {
   console.log('捕获到异常:', {message, source, lineno, colno, error});
}

验证下几个错误是否可以捕获:

// 常规运行时错误,可以捕获
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined);

// 语法错误,不能捕获
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}
const notdefined,
      
// 异步错误,可以捕获
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
  console.log(notdefined);
}, 0)

// 资源错误,不能捕获
<script>
  window.onerror = function(message, source, lineno, colno, error) {
  	console.log('捕获到异常:',{message, source, lineno, colno, error});
  	return true;
  }
</script>
<img src="https://yun.tuia.cn/image/kkk.png">

window.onerror 不能捕获资源错误怎么办?

window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。

// 图片、script、css加载错误,都能被捕获
<script>
  window.addEventListener('error', (error) => {
  	console.log('捕获到异常:', error);
	}, true)
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
<script src="https://yun.tuia.cn/foundnull.js"></script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
  
// new Image错误,不能捕获
<script>
  window.addEventListener('error', (error) => {
    console.log('捕获到异常:', error);
  }, true)
</script>
<script>
  new Image().src = 'https://yun.tuia.cn/image/lll.png'
</script>

// fetch错误,不能捕获
<script>
  window.addEventListener('error', (error) => {
    console.log('捕获到异常:', error);
  }, true)
</script>
<script>
  fetch('https://tuia.cn/test')
</script>

其中new Image比较重要(和特别),可以单独自己处理自己的错误。

但通用的fetch怎么办呢,fetch返回Promise,但Promise的错误不能被捕获,怎么办呢?

window.addEventListenerwindow.onerror都不能捕获promise错误

Promise错误的解决

普通Promise错误

try/catch不能捕获Promise中的错误

// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中
try {
  new Promise((resolve,reject) => { 
    JSON.parse('')
    resolve();
  })
} catch(err) {
  console.error('in try catch', err)
}

// 需要使用catch方法
new Promise((resolve,reject) => { 
  JSON.parse('')
  resolve();
}).catch(err => {
  console.log('in catch fn', err)
})

async错误

上面说的普通的 promise 错误可以通过async/await来解决。但是有一点比较特殊的是:
try/catch不能捕获 async 包裹的错误!

const getJSON = async () => {
  throw new Error('inner error')
}

// 通过try/catch处理
const makeRequest = async () => {
    try {
        // 捕获不到
        JSON.parse(getJSON());
    } catch (err) {
        console.log('outer', err);
    }
};

try {
    // try/catch不到
    makeRequest()
} catch(err) {
    console.error('in try catch', err)
}

try {
    // 需要await,才能捕获到
    await makeRequest()
} catch(err) {
    console.error('in try catch', err)
}

import chunk错误

import 其实返回的也是一个 promise。它是一个特殊的语法 —— 在w3c的不懈努力下,import 支持“不必非要写在最顶部”的写法。对此,我们有如下两种方式捕获错误:

// Promise catch方法
import(/* webpackChunkName: "incentive" */'./index').then(module => {
    module.default()
}).catch((err) => {
    console.error('in catch fn', err)
})

// await 方法,try catch
try {
    const module = await import(/* webpackChunkName: "incentive" */'./index');
    module.default()
} catch(err) {
    console.error('in try catch', err)
}

小结:全局捕获Promise中的错误

以上三种其实归结为Promise类型错误,可以通过 unhandledrejection API 捕获:

// 全局统一处理Promise
window.addEventListener("unhandledrejection", function(e){
  console.log('捕获到异常:', e);
});

为了防止有漏掉的 Promise 异常,可通过 unhandledrejection 用来全局监听Uncaught Promise Error。

Vue中的错误

由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror捕获,而是会抛给Vue.config.errorHandler

/**
 * 全局捕获Vue错误,直接扔出给onerror处理
 */
Vue.config.errorHandler = function (err) {
  setTimeout(() => {
    throw err
  })
}

跨域问题

一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。

如果当前投放页面和云端JS所在不同域名,如果云端JS出现错误,window.onerror会出现Script Error。通过以下两种方法能给予解决:

后端配置Access-Control-Allow-Origin、前端script加crossorigin

<script src="http://yun.tuia.cn/test.js" crossorigin></script>
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = 'http://yun.tuia.cn/test.js';
document.body.appendChild(script);

如果不能修改服务端的请求头,可以考虑通过使用 try/catch 绕过,将错误抛出:

<!doctype html>
<html>
<head>
  <title>Test page in http://test.com</title>
</head>
<body>
  <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
  <script>
  window.onerror = function (message, url, line, column, error) {
    console.log(message, url, line, column, error);
  }

  try {
    foo(); // 调用testerror.js中定义的foo方法
  } catch (e) {
    throw e;
  }
  </script>
</body>
</html>

我们捋一下场景,一般调用远端js,有下列三种常见情况。

  1. 调用远端JS的方法出错
  2. 远端JS内部的事件出问题
  3. 要么在setTimeout等回调内出错

上报接口

捕获到了错误,就要开始往服务端发送(可以生成日志或者异常文档,一般监控工具会自动完成)。这其实就是一次请求的过程。

里面有几个需要注意的地方:

为什么不能直接用ajax - GET/POST/HEAD请求接口进行上报?

一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。

为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?

一般来说创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。而且载入js/css资源还会阻塞页面渲染,影响用户体验。

构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点。
使用new Image进行接口上报。最后一个问题,同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。

为什么推荐采用1x1的gif图片进行操作?

首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG。

同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。GIF才是最佳选择。

使用1*1的gif

以上是关于前端监控之处理异常的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章

前端开发中js代码异常处理及监控

Java线程池异常处理的正确姿势

SpringBoot 开发案例之参数传递的正确姿势

SpringBoot 开发案例之参数传递的正确姿势

kotlin协程async await的异常踩坑以及异常处理的正确姿势

kotlin协程async await的异常踩坑以及异常处理的正确姿势