您将Get的技能网络前端缺点(原生、React、Vue)编写缺点上报逻辑利用Egg.js编写一个缺点日志采集做事编写webpack插件自动上传sourcemap利用sourcemap还原压缩代码源码位置利用Jest进行单元测试事情流程网络缺点上报缺点代码上线打包将sourcemap文件上传至缺点监控做事器发生缺点时监控做事器吸收缺点并记录到日志中根据sourcemap和缺点日志内容进行缺点剖析(一)非常网络事理
首先先看看如何捕获非常。
JS非常js非常的特点是,涌现不会导致JS引擎崩溃 最多只会终止当前实行的任务。比如一个页面有两个按钮,如果点击按钮发生非常页面,这个时候页面不会崩溃,只是这个按钮的功能失落效,其他按钮还会有效。
setTimeout(() => { console.log('1->begin') error console.log('1->end')})setTimeout(() => { console.log('2->begin') console.log('2->end')})复制代码

上面的例子我们用setTimeout分别启动了两个任务,虽然第一个任务实行了一个缺点的方法。程序实行停滞了。但是其余一个任务并没有受到影响。
实在如果你不打开掌握台都看不到发生了缺点。彷佛是缺点是在静默中发生的。
下面我们来看看这样的缺点该如何网络。
try-catchJS作为一门高等措辞我们首先想到的利用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就明白啦,实在这些一步函数都是就好比一群没娘的孩子出了缺点找不到家大人。当然我也想过一些黑邪术来处理这个问题比如代理实行或者用过的异步方法。算了还是还是再看看吧。
window.onerrorwindow.onerror 最大的好处便是可以同步任务还是异步任务都可捕获。
function fun1() { console.log('1->begin') error console.log('1->end')}window.onerror = (...args) => { console.log('onerror:',args)}setTimeout(() => { fun1()})复制代码
onerror返回值 onerror还有一个问题大家要把稳 如果返回返回true 就不会被上抛了。不然掌握台中还会看到缺点日志。监听error事宜
window.addEventListener('error',() => {})
实在onerror固然好但是还是有一类非常无法捕获。这便是网络非常的缺点。比如下面的例子。
<img src="./xxxxx.png">复制代码
试想一下我们如果页面上要显示的图片溘然不显示了,而我们浑然不知那便是麻烦了。
addEventListener便是
window.addEventListener('error', args => { console.log( 'error event:', args ); return true; }, true // 利用捕获办法);复制代码
运行结果如下:
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});复制代码
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就好了。
小结
实际上我们可以将unhandledrejection事宜抛出的非常再次抛出就可以统一通过error事宜进行处理了。
终极用代码表示如下:
window.addEventListener("unhandledrejection", e => { throw e.reason});window.addEventListener('error', args => { console.log( 'error event:', args ); return true;}, true);复制代码
利用vue+node搭建前端非常监控系统(二)-框架如何网络Webpack工程化
现在是前端工程化的时期,工程化导出的代码一样平常都是被压缩稠浊后的。
比如:
setTimeout(() => { xxx(1223)}, 1000)复制代码
出错的代码指向被压缩后的JS文件,而JS文件长下图这个样子。
如果想将缺点和原有的代码关联起来就须要sourcemap文件的帮忙了。
sourceMap是什么
大略说,sourceMap便是一个文件,里面储存着位置信息。
仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。
那么如何利用sourceMap对还原非常代码发生的位置这个问题我们到非常剖析这个章节再讲。
Vue创建工程利用vue-cli工具直接创建一个项目。
# 安装vue-clinpm install -g @vue/cli# 创建一个项目vue create vue-samplecd vue-samplenpm 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中添加缺点事宜监听```jswindow.addEventListener('error', args => { console.log('error', error)})复制代码
这个时候 缺点会在掌握台中被打印出来,但是缺点事宜并没有监听到。
handleError
为了对Vue发生的非常进行统一的上报,须要利用vue供应的handleError句柄。一旦Vue发生非常都会调用这个方法。
我们在src/main.js
Vue.config.errorHandler = function (err, vm, info) { console.log('errorHandle:', err)}复制代码
运行结果结果:
React
npx create-react-app react-samplecd react-sampleyarn 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)})复制代码
但是从运行结果看虽然输出了缺点日志但是还是做事捕获。
ErrorBoundary标签
缺点边界仅可以捕获其子组件的缺点。缺点边界无法捕获其自身的缺点。如果一个缺点边界无法渲染缺点信息,则缺点会向上冒泡至最靠近的缺点边界。这也类似于 JavaScript 中 catch {} 的事情机制。
创建ErrorBoundary组件
import React from 'react'; export default class ErrorBoundary extends React.Component { constructor(props) { super(props); } componentDidCatch(error, info) { // 发生非常时打印缺点 console.log('componentDidCatch',error) } render() { return this.props.children; } }复制代码
在src/index.js中包裹App标签
import ErrorBoundary from './ErrorBoundary'ReactDOM.render( <ErrorBoundary> <App /> </ErrorBoundary> , document.getElementById('root'));复制代码
终极运行的结果
利用vue+node搭建前端非常监控系统(三)-信息上报选择通讯办法动态创建img标签
实在上报便是要将捕获的非常信息发送到后端。最常用的办法首推动态创建标签办法。由于这种办法无需加载任何通讯库,而且页面是无需刷新的。基本上目前包括百度统计 Google统计都是基于这个事理做的埋点。
new Image().src = 'http://localhost:7001/monitor/error'+ '?info=xxxxxx'复制代码
通过动态创建一个img,浏览器就会向做事器发送get要求。可以把你须要上报的缺点数据放在querystring字符串中,利用这种办法就可以将缺点上报到做事器了。
Ajax上报实际上我们也可以用ajax的办法上报缺点,这和我们再业务程序中并没有什么差异。在这里就不赘述。
上报哪些数据我们先看一下error事宜参数:
个中核心的该当是缺点栈,实在我们定位缺点最紧张的便是缺点栈。
缺点堆栈中包含了绝大多数调试有关的信息。个中包括了非常位置(行号,列号),非常信息
有兴趣的同学可以看看这篇文章
https://github.com/dwqs/blog/issues/49
上报数据序列化由于通讯的时候只能以字符串办法传输,我们须要将工具进行序列化处理。
大概分成以下三步:
将非常数据从属性中解构出来存入一个JSON工具将JSON工具转换为字符串将字符串转换为Base64当然在后端也要做对应的反向操作 这个我们后面再说。
window.addEventListener('error', args => { console.log( 'error event:', args ); uploadError(args) return true;}, true);function uploadError({ lineno, colno, error: { stack }, timeStamp, message, filename }) { // 过滤 const info = { lineno, colno, stack, timeStamp, message, filename } // const str = new Buffer(JSON.stringify(info)).toString("base64"); const str = window.btoa(JSON.stringify(info)) const host = 'http://localhost:7001/monitor/error' new Image().src = `${host}?info=${str}`}复制代码
利用vue+node搭建前端非常监控系统(四)-信息整理
非常上报的数据一定是要有一个后端做事吸收才可以。
我们就以比较盛行的开源框架eggjs为例来演示
搭建eggjs工程# 全局安装egg-clinpm i egg-init -g # 创建后端项目egg-init backend --type=simplecd backendnpm i# 启动项目npm run dev复制代码
编写error上传接口
首先在app/router.js添加一个新的路由
module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index); // 创建一个新的路由 router.get('/monitor/error', controller.monitor.index);};复制代码
创建一个新的controller (app/controller/monitor)
'use strict';const Controller = require('egg').Controller;const { getOriginSource } = require('../utils/sourcemap')const fs = require('fs')const path = require('path')class MonitorController extends Controller { async index() { const { ctx } = this; const { info } = ctx.query const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8')) console.log('fronterror:', json) ctx.body = ''; }}module.exports = MonitorController;复制代码
看一下吸收后的结果
记入日志文件下一步便是讲错误记入日志。实现的方法可以自己用fs写,也可以借助log4js这样成熟的日志库。
当然在eggjs中是支持我们定制日志那么我么你就用这个功能定制一个前端缺点日志好了。
在/config/config.default.js中增加一个定制日志配置
// 定义前端缺点日志config.customLogger = { frontendLogger : { file: path.join(appInfo.root, 'logs/frontend.log') }}复制代码
在/app/controller/monitor.js中添加日志记录
async index() { const { ctx } = this; const { info } = ctx.query const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8')) console.log('fronterror:', json) // 记入缺点日志 this.ctx.getLogger('frontendLogger').error(json) ctx.body = ''; }复制代码
末了实现的效果
利用vue+node搭建前端非常监控系统(五)-非常剖析
谈到非常剖析最主要的事情实在是将webpack稠浊压缩的代码还原。
Webpack插件实现SourceMap上传在webpack的打包时会产生sourcemap文件,这个文件须要上传到非常监控做事器。这个功能我们试用webpack插件完成。
创建webpack插件/source-map/plugin
const fs = require('fs')var http = require('http');class UploadSourceMapWebpackPlugin { constructor(options) { this.options = options } apply(compiler) { // 打包结束后实行 compiler.hooks.done.tap("upload-sourcemap-plugin", status => { console.log('webpack runing') }); }}module.exports = UploadSourceMapWebpackPlugin;复制代码
加载webpack插件
webpack.config.js
// 自动上传MapUploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebPackPlugin')plugins: [ // 添加自动上传插件 new UploadSourceMapWebpackPlugin({ uploadUrl:'http://localhost:7001/monitor/sourcemap', apiKey: 'kaikeba' }) ],复制代码
添加读取sourcemap读取逻辑
在apply函数中增加读取sourcemap文件的逻辑
/plugin/uploadSourceMapWebPlugin.js
const glob = require('glob')const path = require('path')apply(compiler) { console.log('UploadSourceMapWebPackPlugin apply') // 定义在打包后实行 compiler.hooks.done.tap('upload-sourecemap-plugin', async status => { // 读取sourcemap文件 const list = glob.sync(path.join(status.compilation.outputOptions.path, `.//.{js.map,}`)) for (let filename of list) { await this.upload(this.options.uploadUrl, filename) } })}复制代码
实现http上传功能
upload(url, file) { return new Promise(resolve => { console.log('uploadMap:', file) const req = http.request( `${url}?name=${path.basename(file)}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', Connection: "keep-alive", "Transfer-Encoding": "chunked" } } ) fs.createReadStream(file) .on("data", chunk => { req.write(chunk); }) .on("end", () => { req.end(); resolve() }); })}复制代码
做事器端添加上传接口
/backend/app/router.js
module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index); router.get('/monitor/error', controller.monitor.index); // 添加上传路由 router.post('/monitor/sourcemap',controller.monitor.upload)}; 复制代码
添加sourcemap上传接口
/backend/app/controller/monitor.js
async upload() { const { ctx } = this const stream = ctx.req const filename = ctx.query.name const dir = path.join(this.config.baseDir, 'uploads') // 判断upload目录是否存在 if (!fs.existsSync(dir)) { fs.mkdirSync(dir) } const target = path.join(dir, filename) const writeStream = fs.createWriteStream(target) stream.pipe(writeStream)}复制代码
终极效果:
实行webpack打包时调用插件sourcemap被上传至做事器。
解析ErrorStack
考虑到这个功能须要较多逻辑,我们准备把他开拓成一个独立的函数并且用Jest来做单元测试
先看一下我们的需求
搭建Jest框架
首先创建一个/utils/stackparser.js文件
module.exports = class StackPaser { constructor(sourceMapDir) { this.consumers = {} this.sourceMapDir = sourceMapDir }}复制代码
在同级目录下创建测试文件stackparser.spec.js
以上需求我们用Jest表示便是
const StackParser = require('../stackparser')const { resolve } = require('path')const error = { stack: 'ReferenceError: xxx is not defined\n' + ' at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392', message: 'Uncaught ReferenceError: xxx is not defined', filename: 'http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js'}it('stackparser on-the-fly', async () => { const stackParser = new StackParser(__dirname) // 断言 expect(originStack[0]).toMatchObject( { source: 'webpack:///src/index.js', line: 24, column: 4, name: 'xxx' } )})复制代码
整理如下:
下面我们运行Jest
npx jest stackparser --watch复制代码
显示运行失落败,缘故原由很大略由于我们还没有实现对吧。下面我们就实现一下这个方法。
反序列Error工具首先创建一个新的Error工具 将缺点栈设置到Error中,然后利用error-stack-parser这个npm库来转化为stackFrame
const ErrorStackParser = require('error-stack-parser')/ 缺点堆栈反序列化 @param {} stack 缺点堆栈 /parseStackTrack(stack, message) { const error = new Error(message) error.stack = stack const stackFrame = ErrorStackParser.parse(error) return stackFrame}复制代码
运行效果
解析ErrorStack
下一步我们将缺点栈中的代码位置转换为源码位置
const { SourceMapConsumer } = require("source-map");async getOriginalErrorStack(stackFrame) { const origin = [] for (let v of stackFrame) { origin.push(await this.getOriginPosition(v)) } // 销毁所有consumers Object.keys(this.consumers).forEach(key => { console.log('key:',key) this.consumers[key].destroy() }) return origin } async getOriginPosition(stackFrame) { let { columnNumber, lineNumber, fileName } = stackFrame fileName = path.basename(fileName) console.log('filebasename',fileName) // 判断是否存在 let consumer = this.consumers[fileName] if (consumer === undefined) { // 读取sourcemap const sourceMapPath = path.resolve(this.sourceMapDir, fileName + '.map') // 判断目录是否存在 if(!fs.existsSync(sourceMapPath)){ return stackFrame } const content = fs.readFileSync(sourceMapPath, 'utf8') consumer = await new SourceMapConsumer(content, null); this.consumers[fileName] = consumer } const parseData = consumer.originalPositionFor({ line:lineNumber, column:columnNumber }) return parseData }复制代码
我们用Jest测试一下
it('stackparser on-the-fly', async () => { const stackParser = new StackParser(__dirname) console.log('Stack:',error.stack) const stackFrame = stackParser.parseStackTrack(error.stack, error.message) stackFrame.map(v => { console.log('stackFrame', v) }) const originStack = await stackParser.getOriginalErrorStack(stackFrame) // 断言 expect(originStack[0]).toMatchObject( { source: 'webpack:///src/index.js', line: 24, column: 4, name: 'xxx' } )})复制代码
看一下结果测试通过。
将源码位置记入日志async index() { console.log const { ctx } = this; const { info } = ctx.query const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8')) console.log('fronterror:', json) // 转换为源码位置 const stackParser = new StackParser(path.join(this.config.baseDir, 'uploads')) const stackFrame = stackParser.parseStackTrack(json.stack, json.message) const originStack = await stackParser.getOriginalErrorStack(stackFrame) this.ctx.getLogger('frontendLogger').error(json,originStack) ctx.body = ''; }复制代码
运行效果:
利用vue+node搭建前端非常监控系统(六)- 开源框架选择Fundebug
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上运用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+缺点事宜,付费客户有阳光保险、荔枝FM、掌门1对1、核桃编程、微脉等浩瀚品牌企业。欢迎免费试用!
Sentry 是一个开源的实时缺点追踪系统,可以帮助开拓者实时监控并修复非常问题。它紧张专注于持续集成、提高效率并且提升用户体验。Sentry 分为做事端和客户端 SDK,前者可以直策应用它家供应的在线做事,也可以本地自行搭建;后者供应了对多种主流措辞和框架的支持,包括 React、Angular、Node、Django、RoR、PHP、Laravel、Android、.NET、JAVA 等。同时它可供应了和其他盛行做事集成的方案,例如 GitHub、GitLab、bitbuck、heroku、slack、Trello 等。目前公司的项目也都在逐步运用上 Sentry 进行缺点日志管理。
总结截止到目前为止,我们把前端非常监控的基本功能算是形成了一个MVP(最小化可行产品)。后面须要升级的还有很多,对缺点日志的剖析和可视化方面可以利用ELK。发布和支配可以采取Docker。对eggjs的上传和上报最好要增加权限掌握功能。
参考代码位置: https://github.com/su37josephxia/frontend-basic/tree/master/monitor
欢迎示正,欢迎Star。