您将Get的技能
- 收集前端错误(原生、React、Vue)
- 编写错误上报逻辑
- 利用Egg.js编写一个错误日志采集服务
- 编写webpack插件自动上传sourcemap
- 利用sourcemap还原压缩代码源码位置
- 利用Jest进行单元测试
工作流程
- 收集错误
- 上报错误
- 代码上线打包将sourcemap文件上传至错误监控服务器
- 发生错误时监控服务器接收错误并记录到日志中
- 根据sourcemap和错误日志内容进行错误分析
一、 前端异常监控平台解决的问题
前端异常监控系统就是为了解决前端系统上线后的稳定性问题。一旦前端系统上线,发生运行异常造成页面阻塞,操作不流畅甚至无法打开网页的状况。我们需要通过技术手段收集、上报、分析异常才能保证前端项目稳定运行。
二、异常收集
首先先看看如何捕获异常。
1. JS异常
js异常的特点是,出现不会导致JS引擎崩溃 最多只会终止当前执行的任务。比如一个页面有两个按钮,如果点击按钮发生异常页面,这个时候页面不会崩溃,只是这个按钮的功能失效,其他按钮还会有效。
setTimeout(() => { console.log('1->begin') error console.log('1->end') }) setTimeout(() => { console.log('2->begin') console.log('2->end') })
上面的例子我们用setTimeout分别启动了两个任务,虽然第一个任务执行了一个错误的方法。程序执行停止了。但是另外一个任务并没有收到影响。
其实如果你不打开控制台都看不到发生了错误。好像是错误是在静默中发生的。
下面我们来看看这样的错误该如何收集。
1.1 try-catch
JS作为一门高级语言我们首先想到的使用try-catch来收集。
setTimeout(() => { try { console.log('1->begin') error console.log('1->end') } catch (e) { console.log('catch',e) } })
如果在函数中错误没有被捕获,错误会上抛。
function fun1() { console.log('1->begin') error console.log('1->end') } setTimeout(() => { try { fun1() } catch (e) { console.log('catch',e) } })
控制台中打印出的分别是错误信息和错误堆栈。
读到这里大家可能会想那就在最底层做一个错误try-catch不就好了吗。确实作为一个从java转过来的程序员也是这么想的。但是理想很丰满,现实很骨感。我们看看下一个例子。
function fun1() { console.log('1->begin') error console.log('1->end') } try { setTimeout(() => { fun1() }) } catch (e) { console.log('catch', e) }
大家注意运行结果,异常并没有被捕获。
这是因为JS的try-catch功能非常有限一遇到异步就不好用了。那总不能为了收集错误给所有的异步都加一个try-catch吧,太坑爹了。其实你想想异步任务其实也不是由代码形式上的上层调用的就比如本例中的settimeout。大家想想eventloop就明白啦,其实这些一步函数都是就好比一群没娘的孩子出了错误找不到家大人。当然我也想过一些黑魔法来处理这个问题比如代理执行或者用过的异步方法。算了还是还是再看看吧。
1.2 window.onerror
window.onerror 最大的好处就是可以同步任务还是异步任务都可捕获。
function fun1() { console.log('1->begin') error console.log('1->end') } window.onerror = (...args) => { console.log('onerror:',args) } setTimeout(() => { fun1() })
- onerror返回值
onerror还有一个问题大家要注意 如果返回返回true 就不会被上抛了。不然控制台中还会看到错误日志。
1.3 监听error事件
window.addEventListener('error',() => {})
其实onerror固然好但是还是有一类异常无法捕获。这就是网络异常的错误。比如下面的例子。
<img src="./xxxxx.png">
试想一下我们如果页面上要显示的图片突然不显示了,而我们浑然不知那就是麻烦了。
addEventListener就是
window.addEventListener('error', args => { console.log( 'error event:', args ); return true; }, true // 利用捕获方式 );
运行结果如下:
1.4 Promise异常捕获
Promise的出现主要是为了让我们解决回调地域问题。基本是我们程序开发的标配了。虽然我们提倡使用es7 async/await语法来写,但是不排除很多祖传代码还是存在Promise写法。
new Promise((resolve, reject) => { abcxxx() });
这种情况无论是onerror还是监听错误事件都是无法捕获的
new Promise((resolve, reject) => { error() }) // 增加异常捕获 .catch((err) => { console.log('promise catch:',err) });
除非每个Promise都添加一个catch方法。但是显然是不能这样做。
window.addEventListener("unhandledrejection", e => { console.log('unhandledrejection',e) });
我们可以考虑将unhandledrejection事件捕获错误抛出交由错误事件统一处理就可以了
window.addEventListener("unhandledrejection", e => { throw e.reason });
1.5 async/await异常捕获
const asyncFunc = () => new Promise(resolve => { error }) setTimeout(async() => { try { await asyncFun() } catch (e) { console.log('catch:',e) } })
实际上async/await语法本质还是Promise语法。区别就是async方法可以被上层的
try/catch捕获。
如果不去捕获的话就会和Promise一样,需要用unhandledrejection事件捕获。这样的话我们只需要在全局增加unhandlerejection就好了。
小结
异常类型 | 同步方法 | 异步方法 | 资源加载 | Promise | async/await |
try/catch | ✔️ | ✔️ | |||
onerror | ✔️ | ✔️ | |||
error事件监听 | ✔️ | ✔️ | ✔️ | ||
unhandledrejection事件监听 | ✔️ | ✔️ |
实际上我们可以将unhandledrejection事件抛出的异常再次抛出就可以统一通过error事件进行处理了。
最终用代码表示如下:
window.addEventListener("unhandledrejection", e => { throw e.reason }); window.addEventListener('error', args => { console.log( 'error event:', args ); return true; }, true);
2. Webpack工程化项目中捕获异常
现在是前端工程化的时代,工程化导出的代码一般都是被压缩混淆后的。
比如:
setTimeout(() => { xxx(1223) }, 1000)
出错的代码指向被压缩后的JS文件,而JS文件长下图这个样子。
如果想将错误和原有的代码关联起来就需要sourcemap文件的帮忙了。
sourceMap是什么
简单说,sourceMap
就是一个文件,里面储存着位置信息。
仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。
那么如何利用sourceMap对还原异常代码发生的位置这个问题我们到异常分析这个章节再讲。
3. Vue捕获异常
3.1 创建工程
利用vue-cli工具直接创建一个项目。
# 安装vue-cli npm install -g @vue/cli # 创建一个项目 vue create vue-sample cd vue-sample npm i // 启动应用 npm run serve
为了测试的需要我们暂时关闭eslint 这里面还是建议大家全程打开eslint
在vue.config.js进行配置
module.exports = { // 关闭eslint规则 devServer: { overlay: { warnings: true, errors: true } }, lintOnSave:false }
我们故意在src/components/HelloWorld.vue
<script> export default { name: "HelloWorld", props: { msg: String }, mounted() { // 制造一个错误 abc() } }; </script> ```html 然后在src/main.js中添加错误事件监听 ```js window.addEventListener('error', args => { console.log('error', error) })
这个时候 错误会在控制台中被打印出来,但是错误事件并没有监听到。
3.2 handleError
为了对Vue发生的异常进行统一的上报,需要利用vue提供的handleError句柄。一旦Vue发生异常都会调用这个方法。
我们在src/main.js
Vue.config.errorHandler = function (err, vm, info) { console.log('errorHandle:', err) }
运行结果结果:
4. React捕获异常
4.1 创建项目
npx create-react-app react-sample cd react-sample yarn start
我们l用useEffect hooks 制造一个错误
import React ,{useEffect} from 'react'; import logo from './logo.svg'; import './App.css'; function App() { useEffect(() => { // 发生异常 error() }); return ( <div className="App"> // ...略... </div> ); } export default App;
并且在src/index.js中增加错误事件监听逻辑
window.addEventListener('error', args => { console.log('error', error) })
但是从运行结果看虽然输出了错误日志但是还是服务捕获。