使用 try...catch
const func = () => {
console.log('fun start')
err
console.log('fun end')
}
try {
func()
} catch (err) {
console.log('err', err)
}
预览代码:trycatch捕获异常
- 缺点:无法捕获 异步 错误
const func = () => {
console.log('fun start')
err
console.log('fun end')
}
try {
setTimeout(() => {
func()
})
} catch (err) {
console.log('err', err)
}
示例
那应该怎么捕获异步的错误呢?
window.onerror 捕获异步错误
const func = () => {
console.log('fun start')
err
console.log('fun end')
}
setTimeout(() => {
func()
})
window.onerror = (...args) => {
console.log('args:', args)
}
示例
这里我们可以发先,使用 window.onerror
捕获到了我们的异步错误。
但是,它可以捕获到所有类型的错误吗?
比如:资源加载地址错误?
<img src="//xxsdfsdx.jpg" >
window.onerror = (...args) => {
console.log('args:', args)
}
示例:资源加载错误
此时,我们看到该资源地址错误没有被 打印出来,那么我们该怎么捕获这种类型错误呢?
window.addEventListener('error)
资源地址错误怎么捕获?
<img src="/xxx.png" />
window.addEventListener('error', (event) => {
console.log('event err:', event)
}, true) // 第三个参数为 true ,选择捕获的方式监听
示例:资源加载错误捕获
promise 怎么捕获?
- 使用
try...catch
无法捕获
const asyncFunc = () => {
return new Promise((res) => {
err
})
}
try {
asyncFunc()
} catch(e) {
console.log('err:', e)
}
示例:try-catch无法捕获promise
- 使用
addEventListener('unhandledrejection')
const asyncFunc = () => {
return new Promise((res) => {
err
})
}
asyncFunc()
window.addEventListener('unhandledrejection', (event) => {
console.log('event err:', event)
})
- promise 捕获
问题:能否使用一个捕获方式捕获所有的错误?
const asyncFunc = () => {
return new Promise((res) => {
err
})
}
asyncFunc()
// 主动抛出捕获到的 promise 类型的错误
window.addEventListener('unhandledrejection', (event) => {
throw event.reason
})
window.addEventListener('error', (err) => {
console.log('err:', err)
}, true)
示例
小结
异常类型 | 同步方法 | 异步方法 | 资源加载 | Promise | async / await | try/catch | y | y | onerror | y | y | addEventListener('error') | y | y | y | addEventListener('unhandledrejection') | y | y |
---|
异常上报服务器
动态创建 img 标签
- 错误监听和上报代码
// 上报错误
function uploadError({lineno, colno, error: { stack }, message, filename }) {
console.log('uploadError---', event)
// 整理我们要的错误信息
const errorInfo = {
lineno,
colno,
stack,
message,
filename
}
// 错误信息序列化后使用 base64 编码,避免出现特殊字符导致的错误
const str = window.btoa(JSON.stringify(errorInfo))
// 创建图片,使用图片给错误收集的后端服务器发送一个 get 请求,
// 上传的信息:错误资源,错误时间
new Image().src = `http://localhost:7001/monitor/error?info=${str}`
}
window.addEventListener('unhandledrejection', (event) => {
// 再次主动抛出
throw event.reason
})
window.addEventListener('error', (err) => {
console.log('error:', err)
// 上报错误
uploadError(err)
})
-
后端收集错误
-
搭建
eggjs
工程,具体参考 Egg.js官网npm i egg-init -g egg-init backend --type=simple cd backend npm i npm run dev
- 编写
error
上传接口——添加路由
// public/router.js module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index); router.get('/monitor/error', controller.monitor.index) };
- 编写
error
上传接口——编写接口,这里使用到了 Buffer-Nodejs
// app/controller/monitor.js 'use strict'; const Controller = require('egg').Controller; class MonitorController extends Controller { async index() { const { ctx } = this; const { info } = ctx.query // Buffer 接受一个 base64 编码的数据 const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8')) console.log('error-info', json) ctx.body = 'hi, json'; } } module.exports = MonitorController;
- 编写
error
上传接口——测试
const info = window.btoa(JSON.stringify({test: 'err'})) // "eyJ0ZXN0IjoiZXJyIn0=" // rest-client 测试接口测试 GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0= // 得到 log 结果:error-info { test: 'err' }
- 编写
eggjs 记入错误日志
方式:
- 可以使用
fs
写入文件进行记录 - 也可以使用 log4j 这种成熟的日志库
当然,在 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
// Buffer 接受一个 base64 编码的数据
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('error-info', json)
// 写入日志
this.ctx.getLogger('frontendLogger').error(json)
ctx.body = 'hi, json';
}
- 测试
// rest-client 测试
GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=
- 结果:查看
/logs/frontend.log
文件中,有具体的日志信息
2021-04-03 11:58:48,543 ERROR 2180 [-/127.0.0.1/-/4ms GET /monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=] { test: 'err' }
Vue项目中异常如何采集
- 初始化 vue 项目
npm i @vue/cli -g
vue create vue-app
cd vue-app
yarn install
yarn serve
- 编写代码,制造
error
// src/components/HelloWorld.vue
// ... 省略部分代码
export default {
name: 'HelloWorld',
props: {
msg: String
},
mounted() {
// methods 中没有定义方法 abc,报错 error
abc()
}
}
- 关闭
eslint
,减少影响,让前端服务能跑起来,新建/编辑vue.config.js
// /vue.config.js
module.exports = {
// close eslint setting
devServer: {
overlay: {
warning: true,
errors: true
}
},
lintOnSave: false
}
- 捕获错误
// src/main.js
// 在 vue 里面统一使用这个 方式捕获错误
Vue.config.errorHandler = (err, vm, info) => {
console.log('errHandler:', err)
uploadError(err)
}
function uploadError({ message, stack }) {
console.log('uploadError---')
// 整理我们要的错误信息
const errorInfo = {
stack,
message,
}
// 错误信息序列化后使用 base64 编码,避免出现特殊字符导致的错误
const str = window.btoa(JSON.stringify(errorInfo))
// 创建图片,使用图片给错误收集的后端服务器发送一个 get 请求,
// 上传的信息:错误资源,错误时间
new Image().src = `http://localhost:7001/monitor/error?info=${str}`
}
new Vue({
render: h => h(App)
}).$mounted('#app')
- 打包 vue 项目,运行测试判断是否捕获错误
yarn build
cd dist
hs
- 删除
dist
目录中的sourcemap
映射文件,此时报错定位代码就不是源代码了,而是压缩后的代码,不美观
因为打包后的代码 js 文件主要有 2 种
app.xxx.js
app.xxx.js.map
我们可以看看 .map
文件的内容结构:
{
"version": 3,
"sources": [
"webpack:///webpack/bootstrap",
"webpack:///./src/App.vue",
"webpack:///./src/components/HelloWorld.vue",
"webpack:///./src/components/HelloWorld.vue?354f",
"webpack:///./src/App.vue?eabf",
"webpack:///./src/main.js",
"webpack:///./src/assets/logo.png",
"webpack:///./src/App.vue?7d22"
],
"names": [
"webpackJsonpCallback",
"data",
//...
],
"mappings": "aACE,SAASA,EAAqBC...",
"file": "js/app.9a4488cf.js",
"sourcesContent": [" \t// install a JSONP callback..."],
"sourceRoot": ""
}
主要包含了这些东西:
version
Source map的版本,目前为3soruces
转换后的文件名names
转换前的所有变量名和属性名mappings
记录位置信息的字符串file
转换后的文件名sourcesContent
源内容列表(可选,和源文件列表顺序一致)sourceRoot
源文件根目录(可选)
关于 source map
可以参考这 2 篇文章 source-map-阮一峰 和 Source Map 原理及源码探索 - Jooger的文章 - 知乎
后面,我们将从 app.xxx.js.map
中进行解析,还原错误代码
sourcemap 上传插件
编写一个 UploadSourceMapWebpackPlugin
插件,用于每次打包代码的时候自动上传到服务器指定目录
- 编写
webpack plugin
// frontend/plugin/uploadSourceMapWebpackPlugin.js
class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
console.log('UploadSourceMapWebpackPlugin apply')
}
}
module.exports = UploadSourceMapWebpackPlugin
- 配置 插件
// /vue.config.js
// refer:https://cli.vuejs.org/zh/config/#configurewebpack
const UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebpackPlugin')
module.exports = {
configureWebpack: {
plugins:[
new UploadSourceMapWebpackPlugin({
uploadUrl: 'http://localhost:7001/monitor/sourcemap'
})
]
},
// close eslint setting
devServer: {
overlay: {
warning: true,
errors: true
}
},
lintOnSave: false
}
- 打包测试
yarn build
# 此时,我们可以看到命令行中的 log
Building for production...UploadSourceMapWebpackPlugin apply
接下来,完成 UploadSourceMapWebpackPlugin
插件的详细功能
const path = require('path')
const glob = require('glob')
const fs = require('fs')
const http = require('http')
class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
console.log('UploadSourceMapWebpackPlugin apply')
// 定义在打包后执行
compiler.hooks.done.tap('UploadSourceMapWebpackPlugin', async status => {
// 读取 sourceMap 文件
const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
console.log('list', list)
// list [
// '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/app.d15f69c0.js.map',
// '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/chunk-vendors.f3b66fea.js.map'
// ]
for (let filename of list) {
await this.upload(this.options.uploadUrl, filename)
}
})
}
upload(url, file) {
return new Promise(resolve => {
console.log('upload Map: ', 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()
})
})
}
}
module.exports = UploadSourceMapWebpackPlugin
作用:
在每一次 build done
的时候:
- 读取
sourceMap
文件 - 将读取的
sourceMap
文件上传到指定服务器中
Eggjs 服务器 sourceMap 上传接口
- 新增后端路由
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
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)
};
- 新增接口,文件信息写入
'use strict';
const Controller = require('egg').Controller;
const path = require('path')
const fs = require('fs')
class MonitorController extends Controller {
// ...
async upload() {
const { ctx } = this
// 拿到的是一个 流
const stream = ctx.req
const filename = ctx.query.name
const dir = path.join(this.config.baseDir, 'upload')
// 判断 upload 是否存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const target = path.join(dir, filename)
// 创建写入流写入信息
console.log('writeFile====', target);
const writeStream = fs.createWriteStream(target)
stream.pipe(writeStream)
}
}
module.exports = MonitorController;
- 关闭
csrf
config.security = {
// 可能存在 scrf 风险,这里设置关闭
csrf: {
enable: false
}
}
- 测试
yarn build
# egg-server log info
writeFile==== D:\Desktop\err-catch-demo\backend\upload\app.d15f69c0.js.map
writeFile==== D:\Desktop\err-catch-demo\backend\upload\chunk-vendors.f3b66fea.js.map
Stack 解析函数
编写测试用例:
- 解析
error.stack
信息
// /app/utils/stackparser.js
const ErrorStackParser = require('error-stack-parser')
const { SourceMapConsumer } = require('source-map')
const path = require('path')
const fs = require('fs')
module.exports = class StackParser {
constructor() {
this.sourceMapDir = sourceMapDir
this.consumers = {}
}
parseStackTrack(stack, message) {
const error = new Error(message)
error.stack = stack
const stackFrame = ErrorStackParser.parse(error)
return stackFrame
}
async getOriginalErrorStack(stackFrame) {
const origin = []
for (let v of stackFrame) {
origin.push( await this.getOriginPosition(v))
}
return origin
}
// 从 sourceMap 文件读取错误信息
async getOriginPosition(stackFrame) {
let { columnNumber, lineNumber, fileName } = stackFrame
fileName = path.basename(fileName)
// 判断 consumers 是否存在
let consumer = this.consumers[fileName]
if (!consumer) {
// 读取 sourceMap
const sourceMapPath = path.resolve(this.sourceMapDir, filename + '.map')
// 判断文件是否存在
if (!fs.existSync(sourceMapPath)) {
// 不存在则返回源文件
return stackFrame
}
const content = fs.readFileSync(sourceMapPath, 'utf-8')
consumer = await new SourceMapConsumer(content, null)
this.consumers[filename] = consumer
}
const parseData = consumer.originalPositionFor({line: lineNumber, columnNumber})
return parseData
}
}
- 测试用例:
// 如何通过sourcemap手工还原错误具体信息? https://www.zhihu.com/question/285449738
// /app/utils/stackparser.spec.js
const StackParser = require('../stackparser')
const { resolve } = require('path')
const { hasUncaughtExceptionCaptureCallback } = require('process')
const error = {
stack: '',
message: '',
filename: '',
}
it('stackparser-on-the-fly', async () => {
const stackParser = new StackParser(__dirname)
console.log('Stack:', error.stack)
const stackFrame = stackParser.parseStackTrack(error.stack, error)
stackFrame.map(v => {
console.log('stackFrame: ', v)
})
const originStack = await stackParser.getOriginalErrorStack(stackFrame)
console.log('originStack', originStack)
// 断言,需要手动修改下面的断言信息
expect(originStack[0]).toMathchObject({
source: 'webpack:///src/index.js',
line: 24,
column: 4,
name: 'xxx'
})
})
参考资料:
- source-map-阮一峰
- Source Map 原理及源码探索 - Jooger的文章 - 知乎
- 如何通过sourcemap手工还原错误具体信息?
- webpack plugin
- source-map/github
- 前端错误监控以及上报方法总结
- 如何做前端异常监控?
- 前端异常监控
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!