为什么需要捕获? 前端代码运行在客户端的浏览器里,当客户端(浏览器)出现任何问题,在没有错误日志的情况下,我们都是不知道问题发生在哪,我们只能依靠猜测或者自己不断尝试才知道,或者永远不知道问题。
客户端怎么捕获? 1.通过 window.onerror,可惜只能获得基础的 js 错误,Promise、async/await 里的错误无法捕获,它收到同源决策的影响
2.Promise 通过catch 方法
3.async/await 通过 try - catch
4.Vue 可以通过全局 Vue.config.errorHandler 去获得非 Promise、async/await 里的错误,可以理解为 Vue 里的 window.onerror
1 2 3 4 5 6 7 8 9 window .onerror = function (message, source, lineno, colno, error ) { };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 window .onerror = function ( ) { console .log (arguments ); }; let data;let info = data.info ;
虽然onerror 无法捕获 Promise 里的错误,但是如果 Promise 里面是被 setTimeout 包裹的 js 还是能捕获的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 window .onerror = function ( ) { console .log (arguments ); }; function timer ( ) { setTimeout (function ( ) { let data; let info = data.info ; }, 100 ); } function p ( ) { return new Promise (function (resolve, reject ) { timer (); }).catch (function (error ) { console .log (error); console .log ("inner error" ); }); } p () .then (function ( ) { console .log ("running then" ); }) .catch (function (error ) { console .log (error); console .log ("outer error" ); });
Q:如果没有 catch 方法,是否能捕获 Promise 里的错误? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 window .onerror = function ( ) { console .log (arguments ); console .log ("onerror" ); }; function errorFn ( ) { let data; let info = data.info ; } function p ( ) { return new Promise (function (resolve, reject ) { errorFn (); }); } try { p ().then (function (res ) { console .log ("running then" ); }); } catch (e) { console .log (e); console .log ("try - catch" ); }
我们通过上面的代码发现,Promise 里的错误无论在try - catch 还是onerror 里都无法被捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function errorFn ( ) { let data; let info = data.info ; } function p ( ) { return new Promise (function (resolve, reject ) { errorFn (); }).catch (function (error ) { console .log (error); console .log ("inner error" ); return "return inner error" ; }); } try { p () .then (function (res ) { console .log (res); console .log ("running then" ); }) .catch (function (error ) { console .log (error); console .log ("outer error" ); }); } catch (e) { console .log (e); console .log ("try - catch" ); }
通过上面代码发现,已经被捕获的错误代码,在外层不会再被捕获而是继续执行 then 里的方法,可见在一条 Promise 链上的错误,会被之后最近的catch 捕获。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 window .onerror = function ( ) { console .log (arguments ); }; function errorFn ( ) { let data; let info = data.info ; } function p ( ) { return new Promise (function (resolve, reject ) { errorFn (); }); } (async function ( ) { let res = await p (); console .log (res); })();
我们通过上面的代码发现,Promise 构造函数里的错误并没有被onerror 捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 window .onerror = function ( ) { console .log (arguments ); }; function errorFn ( ) { let data; let info = data.info ; } function p ( ) { return new Promise (function (resolve, reject ) { resolve ("resolve" ); }); } (async function ( ) { let res = await p (); console .log ("get res" ); errorFn (); })();
虽然 Promise 正常执行,但是当后续的代码出错onerror 依旧没有被捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function errorFn ( ) { let data; let info = data.info ; } function p ( ) { return new Promise (function (resolve, reject ) { errorFn (); }); } (async function ( ) { try { let res = await p (); console .log (res); } catch (e) { console .log (e); console .log ("try - catch" ); } })();
try - catch 捕获了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function errorFn ( ) { let data; let info = data.info ; } function p ( ) { return new Promise (function (resolve, reject ) { errorFn (); }).catch (function (error ) { console .log (error); console .log ("inner error" ); return "return inner error" ; }); } (async function ( ) { try { let res = await p (); console .log (res); } catch (e) { console .log (e); console .log ("try - catch" ); } })();
从上面代码我们知道,如果 Promise 构造函数里的错误被它自己 catch 的话,那么 async/await 后续的 try - catch 将不再对它捕获
1 2 3 4 5 Vue .config .errorHandler = function (err, vm, info ) { };
指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
我们该如何去理解官方对 errorHandler 的解释呢?通过 vue-cli 构建工具,创建一个非常基础的 vue 项目,做一些实验。
测试代码库:https://github.com/miser/vue-capture-error
在 main.js
1 2 3 4 Vue .config .errorHandler = function (err, vm, info ) { console .log (arguments ); console .log ("vue errorHandler" ); };
在 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { created () { this .normal () }, methods : { normal () { let data let info = data.info } } }
从上面代码可以看出,errorHandler 确实可以满足我们的需求,在一个统一的地方捕获代码的错误,但是真的如此吗?上文也提到 errorHandler 和 window.onerror 类似,那么当我们使用 Promse 或者 async/await 时会不会得愿以偿。
js 中的异步很大一部分来自网络请求,那么在这我们用 axios (它做了一层 ajax 与 Promise 之间的封装)。
main.js 里添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const request = axios.create ();request.interceptors .response .use ((response ) => { return response; }); Vue .request = (args ) => { return new Promise ((resolve, reject ) => { request (args) .then ((res ) => { resolve (res); }) .catch ((err ) => { reject (err); }); }); };
在 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { created () { this .fetch1 () }, methods : { fetch1 () { Vue .request ('https://api1.github.com/' ) .then (response => { console .log (response) }) } } }
api.github.com 会返回 github 的 api 列表,当我们拼错域名,比如上面代码中的 api1.github.com 时,那肯定是无法获得我们想要的,可是 errorHandler 并没有获得该错误,不过幸好,我们可以在全局统一的 Vue.request 里的 catch 方法去统一捕获网络层面的错误。那如果是非网络层面的呢?比如数据请求回来了,但是绑定数据的时候,后端因为业务的修改等原因并没有返回我们需要的字段,造成 Promise.then 方法的业务处理错误。
在 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { created () { this .fetch2 () }, methods : { fetch2 () { Vue .request ('https://api.github.com/' ) .then (response => { let data = response.data let info = data.api .info }) } } }
上诉代码运行后,errorHandler 同样未能捕获错误,从 vue 的 issue 里面去查询关于捕获 Promise 或者 async/await 时,会得到作者的答复:
https://github.com/vuejs/vue/issues/6551
Vue cannot capture errors that are thrown asynchronously, similar to how try… catch won’t catch async errors. It’s your responsibility to handle async errors properly, e.g. using Promise.catch — @yyx990803
那么该怎么办,不可能每个地方都加 Promise.catch 方法吧!
https://github.com/vuejs/vue/issues/7653
@Doeke 在这个地方给出一个解决方案,通过全局 mixin,给那些 Promise 方法外面包一层 Promise,在这个外层 Promise 链上 catch 里面的错误,不过这样需要做代码的约定,就是原来的方法需要返回一个 Promise 对象。
main.js 里添加@Doeke 的思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Vue .mixin ({ beforeCreate : function ( ) { const methods = this .$options .methods || {}; Object .entries (methods).forEach (([key, method] ) => { if (method._asyncWrapped ) return ; const wrappedMethod = function (...args ) { const result = method.apply (this , args); const resultIsPromise = result && typeof result.then === "function" ; if (!resultIsPromise) return result; return new Promise (async (resolve, reject) => { try { resolve (await result); } catch (error) { if (!error._handled ) { const errorHandler = Vue .config .errorHandler ; errorHandler (error); error._handled = true ; } reject (error); } }); }; wrappedMethod._asyncWrapped = true ; methods[key] = wrappedMethod; }); }, });
在 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { created () { this .fetch2 () this .fetch3 () }, methods : { fetch2 () { Vue .request ('https://api.github.com/' ) .then (response => { let data = response.data let info = data.api .fetch2 }) }, fetch3 () { return Vue .request ('https://api.github.com/' ) .then (response => { let data = response.data let info = data.api .fetch3 }) } } }
通过运行并观察 console 打印可以看出,fetch3 的错误被 errorHandler 捕获,而 fetch2 的错误并没有。
那么 Promise 里的错误统一捕获的问题差不多应该解决了,那么 async/await 的呢?
在 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { created () { this .fetch4 () this .fetch5 () }, methods : { async fetch4 () { let response = await Vue .request ('https://api.github.com/' ) let data = response.data let info = data.api .fetch4 }, async fetch5 () { let response = await Vue .request ('https://api.github.com/' ) let data = response.data let info = data.api .fetch5 return response } } }
fetch4 并没有返回 Promise,fetch5 返回的也不是 Promise 对象,但是当运行的时候我们会发现 fetch4 和 fetch5 的错误信息都被捕获了,这是为什么呢?因为 async/await 本身就是 Promise 的语法糖,在 babeljs 官网的 “Try it out” 尝试用 async/await,你会发现最后编译后的代码就是在外包了一层 Promise。
在哪里捕获更为优雅?(尽量以更少的代码覆盖大部分或者全部代码) 网络层 :可以在 axios.create 创建的实例中
逻辑层 :非 Promise 本身就会被 errorHandler 捕获;Promise 相关的可以通过全局 mixin 给返回 Promise 对象的方法做一个外层包装,统一 catch 并调用 errorHandler 处理(**这个方法的是否有副作用还需要研究! **)
捕获的错误存放在哪? # 自己简易服务 ?
感觉成本很大(人力和工时)
**# 官方推荐的 Sentry **
注册后安装官方的 JS SDK
1 npm install raven-js --save
修改 main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import Raven from "raven-js" ;import RavenVue from "raven-js/plugins/vue" ;Raven .config ("https://1dfc5e63808b41058675b4b3aed4cfb6@sentry.io/1298044" ) .addPlugin (RavenVue , Vue ) .install (); Vue .config .errorHandler = function (err, vm, info ) { Raven .captureException (err); }; Vue .request = (args ) => { return new Promise ((resolve, reject ) => { request (args) .then ((res ) => { resolve (res); }) .catch ((err ) => { Raven .captureException (err); reject (err); }); }); };
修改 App.vue (我们从最普通的 js 测试起)
1 2 3 4 5 6 7 8 9 10 created () { this .normal () }
打开 sentry 页面查看 我们通过上面 2 张图片可以看出,sentry 自带一个简单的 issue 管理功能,此外详情页面的错误栈已经方便我们知道问题出在哪里了。
测试 fetch1 的 ajax 请求错误
除了 fetch2 无法被捕获外(之前提过,它没有返回 Promise 对象),其它的都能被捕获。不过 Promise 和 async/await 的错误栈比较少。尤其是 Promise.then 里的错误,如下 2 张图的对比:
除了默认的数据的收集外,还能收集一些其他数据,比如用户信息
1 2 3 4 Raven .setUser ({ name : "miser name" , id : "miser id" , });
我们测试了代码未被压缩的情况,如果代码压缩了呢?
显然我们不能直观的获得错误定位,不过 sentry 提供SourceMaps 存储服务,它能方便的 debug 被压缩的代码。
我们可以通过webpack-sentry-plugin 工具将整个上传过程写进 webpack 里,我们创建一个 vue.config.js 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const SentryPlugin = require ("webpack-sentry-plugin" );module .exports = { configureWebpack : { plugins : [ new SentryPlugin ({ organization : "fe-org" , project : "popcorn-vue" , apiKey : "17c7d61a800f495c803196e2c02cadeb1b41454247db4f06a5c54193510da150" , release : "1.2.4-beta" , }), ], }, };
修改 main.js
1 2 3 4 5 Raven .config ("https://1dfc5e63808b41058675b4b3aed4cfb6@sentry.io/1298044" , { release : "1.2.4-beta" , }) .addPlugin (RavenVue , Vue ) .install ();
查看 sentry 里 popcorn-vue 项目中的版本
我们打开 build 完的 index.html,虽然错误成功捕获但依旧和上图的一样,无法被 SourceMaps 解析,大概的原因是 js 和 js.map 的目录结构问题。
这个 issue https://github.com/getsentry/sentry-electron/issues/54 是一个很经典的例子,它犯了 2 个错误
– 仅仅传了 js.map 而没有传被压缩的 js 文件,它们应该一一对应的上传到服务器上 – js 和 js.map 目录路径不匹配
这 2 个原因都会导致无法正常解析被压缩的文件。
那么不直接通过浏览器打开 index.html(file:///** /vue-capture-error/dist/index.html),通过 nginx 去模拟正式环境。
1 2 brew install nginx nginx
将 build 出的代码 dist 拷贝到 nginx 默认目录下 /usr/local/var/www/,打开浏览器 http://localhost:8080
回到 sentry 中查看新的错误记录
Comments