const func = () => {
console.log('fun start')
err
console.log('fun end')
}
try {
func()
} catch (err) {
console.log('err', err)
}
複製代碼
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" alt="">
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 ,選擇捕獲的方式監聽
複製代碼
window.addEventListener('unhandledrejection', (err) =>{}) 捕獲
const asyncFunc = () => {
return new Promise((res) => {
err
})
}
try {
asyncFunc()
} catch(e) {
console.log('err:', e)
}
複製代碼
const asyncFunc = () => {
return new Promise((res) => {
err
})
}
asyncFunc()
window.addEventListener('unhandledrejection', (event) => {
console.log('event err:', event)
})
複製代碼
問題:能否使用一個捕獲方式捕獲所有的錯誤?
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)
複製代碼
異常上報伺服器主要有2 種方式,一是 動態創建 img 標籤,二是直接使用 ajax 發送請求上報。這裡主要講述第一種方式
動態創建 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
複製代碼
// public/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/monitor/error', controller.monitor.index)
};
複製代碼
// 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;
複製代碼
const info = window.btoa(JSON.stringify({test: 'err'})) // "eyJ0ZXN0IjoiZXJyIn0="
// rest-client 測試接口測試
GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=
// 得到 log 結果:error-info { test: 'err' }
複製代碼
方式:
可以使用 fs 寫入文件進行記錄
也可以使用 log4j 這種成熟的日誌庫
當然,在 eggjs 中是支持我們 自定義日誌 的,那麼我們使用這個功能定製一個前端錯誤日誌就可以了。
config.customLogger = {
frontendLogger: {
file: path.join(appInfo.root, 'logs/frontend.log')
}
}
複製代碼
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=
複製代碼
2021-04-03 11:58:48,543 ERROR 2180 [-/127.0.0.1/-/4ms GET /monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=] { test: 'err' }
複製代碼
Vue3.x 官網
npm i @vue/cli -g
vue create vue-app
cd vue-app
yarn install
yarn serve
複製代碼
// src/components/HelloWorld.vue
// ... 省略部分代碼
export default {
name: 'HelloWorld',
props: {
msg: String
},
mounted() {
// methods 中沒有定義方法 abc,報錯 error
abc()
}
}
複製代碼
// /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')
複製代碼
yarn build
cd dist
hs
複製代碼
因為打包後的代碼 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": ""
}
複製代碼
主要包含了這些東西:
後面,我們將從 app.xxx.js.map 中進行解析,還原錯誤代碼
sourcemap 上傳插件編寫一個 UploadSourceMapWebpackPlugin 插件,用於每次打包代碼的時候自動上傳到伺服器指定目錄
// 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 的時候:
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;
複製代碼
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
複製代碼
編寫測試用例:
// /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'
})
})
複製代碼
轉自:https://juejin.cn/post/6947147474943344647