Vue 2.0 生產(chǎn)環(huán)境前端錯誤日志記錄實踐

核心內(nèi)容參考自
https://www.cnblogs.com/luozhihao/p/8635507.html
開始實現(xiàn)的時候知道很簡單,中間的時候感覺就難了一點,后面實現(xiàn)出來的時候感覺真的很簡單。
——————————————————————————
?大佬和我進行了一番深刻的交流,總結(jié)起來就一句話,在生產(chǎn)環(huán)境的時候,前端的報錯要能夠記錄下來。
開發(fā)環(huán)境的時候的報錯,我們都能夠直接通過看開發(fā)者工具就能夠看到具體的報錯信息,什么問題,哪一行、哪一列,那些代碼報錯,這是我們不會進行抓取考慮的——這些報錯顯然是我們肯定要解決的。
生產(chǎn)環(huán)境的時候,我們上傳的代碼都是打包以后的代碼,對于打包好了抓取到的代碼報錯,我們可以通過source map查找到具體的代碼報錯,然后進行存儲。
?
摘要:
window全局捕獲錯誤+promise捕獲錯誤+koa2存儲+source map
那么,開始。
——————————————————————————
開始規(guī)劃
俺們村里人,做事都有計劃。
規(guī)劃為兩點,一點是捕獲報錯,一點是存儲報錯。

捕獲報錯——基礎(chǔ)捕獲(windows全局報錯+promise報錯)
我先解釋下為什么叫基礎(chǔ)捕獲,因為我在搜索相關(guān)的解決方案的時候看到了國內(nèi)外兩家很牛提供前端報錯日志收集解決方案的公司
國內(nèi):FunDebug?https://www.fundebug.com/

國外:?sentry??https://sentry.io/welcome/

它們牛不僅僅是因為它們涵蓋的語言、端多,還因為收集錯誤日志的內(nèi)容上非常多,甚至FunDebug還能夠模擬用戶當時是怎么用的。
這也是我們不能選擇這兩家公司的原因。
那你選擇本地版的咯【比我時下的實習生年薪還要高一倍,你覺得BOSS會不會要?

這就不提了,作為第一版的基礎(chǔ)版,我只需要基礎(chǔ)捕獲就夠了。
window全局報錯捕獲
全局捕獲報錯的時候,使用window.onerror就可以捕獲到了。
But,wait
如果你使用的是MVVM框架,那么在是你用winodw.onerror方法捕獲的報錯,或者叫異常,可能就捕獲不到。因為框架一般都有自身的異常機制來進行捕獲。你可以通過覆蓋這個方法的形式來進行。
比如:
Vue的
Vue.config.errorHandler = function (err, vm, info) { ? ?
????let { ? ? ? ?
????????????message, // 異常信息 ? ? ? ?
????????????name, // 異常名稱 ? ? ? ?
????????????script, ?// 異常腳本url ? ? ? ?
????????????line, ?// 異常行號 ? ? ? ?
????????????column, ?// 異常列號 ? ? ? ?
????????????stack ?// 異常堆棧信息 ? ?
????} = err; ? ?// vm為拋出異常的 Vue 實例 ? ?// info為 Vue 特定的錯誤信息,比如錯誤所在的生命周期鉤子}
在實際項目中,是這么調(diào)用的
// 全局捕獲錯誤
const errorHandler = (error, vm) => {
????reportError(error)? ?
????????? ?//?reportError 是我封裝的上傳報錯信息的內(nèi)容?
}
Vue.config.errorHandler = errorHandler
Vue.prototype.$throw = (error, msg) => errorHandler(error, this)
-----------
在實際的項目運用上,除了這種window全局錯誤捕獲,還要考慮promise下發(fā)生的異步報錯——這種是剛剛我們用的方法難以捕捉的。我們經(jīng)常用try catch來捕獲錯誤,這邊也會遇到不能捕獲的問題。
不過我們知道,promise有專門的catch來捕獲錯誤,但是對于已經(jīng)成熟的大項目來說,一個個去查找catch,去添加我們的上傳錯誤信息的repotrError是不顯示的。
在參考了別人的寫法以后,于是考慮到了去 xxoo promise?——對promise進行一次侵犯。
// 如果瀏覽支持Promise,捕獲promise里面then的報錯,因為promise里面的錯誤onerror和try-catch都無法捕獲
if (Promise && Promise.prototype.then) {
var promiseThen = Promise.prototype.then
/* eslint-disable */
Promise.prototype.then = function(resolve, reject) {
return promiseThen.call(this, _wrapPromiseFunction(resolve), _wrapPromiseFunction(reject))
}
/* eslint-enable */
}
// 異步報錯統(tǒng)一捕捉
var _wrapPromiseFunction = (fn) => {
// 如果fn是個函數(shù),則直接放到try-catch中運行,否則要將類的方法包裹起來,promise中的fn要返回null,不能返回空函數(shù)
if (typeof fn !== 'function') {
return null
}
return function () {
try {
return fn.apply(this, arguments)
} catch (error) {
reportError(error)
throw (error)
}
}
}
?針對線上環(huán)境,如果支持promise,那么我們就會對promise的prototype(見我之前的文章內(nèi)容)的taen方法進行一次重新改造處理,具體的改造方法是返回一個封裝好的try catch函數(shù),相當于是自帶了try catch。如果正常,那么返回正常的函數(shù)調(diào)用方法。這邊有一個小問題——我忘記了為何要再寫個throw方法來拋出異常了,在捕獲了異常以后(reportError),我還需要throw(error)嗎?回頭思考以后編輯下。
通過上面的方法,我們可以捕獲最常見的兩種報錯,基本上滿足了我們的需求。
--------------------------------------
存儲錯誤——與source map 愉快地玩耍
前端拿到錯誤的時候,并不像我想的那么順利,在我這里,我不能夠直接拿到具體的行、具體的列,只能夠通過錯誤棧找到
說起來,如果是開發(fā)環(huán)境中,用webpack開啟的服務器直接捕獲的到的錯誤棧信息如下
SyntaxError: Unexpected token s in JSON at position 4
? ? at JSON.parse (<anonymous>)
? ? at eval (webpack-internal:///1440:455:52)
? ? at eval (webpack-internal:///465:109:15)
而如果是線上打包環(huán)境的話,
拿到的錯誤棧信息如下
SyntaxError: Unexpected token s in JSON at position 4
? ? at JSON.parse (<anonymous>)
? ? at 【馬賽克】11.4e17da6131b2c0b1d277.js:21:9826
? ? at 【馬賽克】app.bc205a634089f0bd3803.js:1:46844
根據(jù)這個錯誤棧信息,我們知道這個錯誤涉及到兩個js文件的名字,對應的行以及對應的列——但是這樣對我們解決bug基本上沒有什么幫助。
不過有了對應的js文件的名字,錯誤的行和列以及足夠了。
開始做存儲錯誤處理的操作;
第一步:生成打包后的js文件對應的map文件
如果你是webpack,可以這么配置
?productionSourceMap: true
然后你就會看到在每一個.js文件下都會有對應的同名的.map文件,這是一一對應的,所以我們很方便就可以聯(lián)系起來?

第二步:選擇適合自己的source map解析方式,我沒有直接自己去解析map文件,雖然參考了一些網(wǎng)上的案例,最后還是直接用了sourceMap?這個npm包進行解析處理
const sourceMap = require('source-map'); // *核心內(nèi)容,解析source-map的內(nèi)容
第三步:解析我們打包的報錯代碼
我使用的是node服務器,已知
SyntaxError: Unexpected token s in JSON at position 4
? ? at JSON.parse (<anonymous>)
? ? at 【馬賽克】11.4e17da6131b2c0b1d277.js:21:9826
? ? at 【馬賽克】app.bc205a634089f0bd3803.js:1:46844
如上的錯誤信息,我只要獲得錯誤的文件名【找到對應的map文件】?,行數(shù),列數(shù)這三個數(shù)據(jù)即可。
核心代碼如下:
const fs = require('fs'); // 讀取文件
const path = require('path'); // 讀取文件路徑
const readline = require('readline'); // 按行讀取文件
const sourceMap = require('source-map'); // *核心內(nèi)容,解析source-map的內(nèi)容
/*
* 按行讀取文件內(nèi)容
* 返回:字符串數(shù)組
* 參數(shù):fReadName:文件名路徑
* ? ? ?callback:回調(diào)函數(shù)
* */
/*
* 按行讀取文件內(nèi)容
* 返回:字符串數(shù)組
* 參數(shù):fReadName:文件名路徑
* ? ? ?callback:回調(diào)函數(shù)
* */
async function readFileToArr(fReadName, callback){
? var fRead = fs.createReadStream(fReadName);
? var objReadline = readline.createInterface({
? ? input:fRead
? });
? var arr = new Array();
? objReadline.on('line',function (line) {
? ? arr.push(line);
? });
? await new Promise((resolve, reject) => {
? ? objReadline.on('close',function () {
? ? ? callback(arr)
? ? ? resolve('done!')
? ? });
? })
}
/**
? ? * ctx 內(nèi)容
? ? * @param ret.name // 報錯對應的名稱
? ? * @param ret.source // 報錯文件路徑
? ? * @param ret.line // 報錯文件行號
? ? * @param ret.lcolumn // 報錯文件列號
? ?*/
let {
? ? ?userAgent,
? ? ?pathname,
? ? ?name,
? ? ?error,
? ?} = ctx.request.body
// 下面的解析錯誤棧的方式,是因為我不知道為何不能直接拿到報錯文件路徑、行號、列號所做的hack,如果你能直接拿到,可以不這么做。直接看核心代碼即可
? ?// 得到js地址的正則表達式
? ?var geJsMap = /(?<=js\/).*?(?=:)/
? ?// 存儲了js鏈接,不一定只有一個
? ?const jsList = []
? ?// 存儲了每個js文件報錯對應的行
? ?const lineList = []
? ?// 存儲了每個js文件報錯對應的列
? ?const columnList = []
? ?// 存儲了得到的真正的資源文件的內(nèi)容
? ?const sourceret = []
? ??
// 取得js文件、報錯的行和列
error.split('at').forEach(element => {
? ? if (element.match(geJsMap)) {
? ? // 取得js文件
? ? jsList.push(element.match(geJsMap)[0])
? }
? // 取得報錯的行和列
? if (element.split(':').length > 2) {
? ? lineList.push(element.split(':')[1])
? ? columnList.push(element.split(':')[2].split('\\n')[0])
? }
? ?});
? ?// 遍歷顯示異常的js文件
? ?for (let i in jsList) {
? ? ?// 得到js文件對應的.map文件
? ? ?let fileUrl = `${jsList[i]}.map`;
? ? ?// 得到解析了map文件的smc對象 核心內(nèi)容
? ? ?let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve(`../js/${fileUrl}`), 'utf8')); // 返回一個promise對象
? ? ?// 通過originalPositionFor獲得result文件的內(nèi)容
? ? ?await smc.then(function (result) {
? // 核心內(nèi)容
? let ret = result.originalPositionFor({
? ? line: parseInt(lineList[i]), // 壓縮后的行號
? ? column: parseInt(columnList[i])// 壓縮后的列號
? ?//?到了這一步,你可以獲得下面的內(nèi)容

? ? ? ?});
? ? ? ?/**
? ? ? ? * 解析原始報錯數(shù)據(jù)
? ? ? ? * @param ret.name // 報錯對應的名稱
? ? ? ? * @param ret.source // 報錯文件路徑
? ? ? ? * @param ret.line // 報錯文件行號
? ? ? ? * @param ret.lcolumn // 報錯文件列號
? ? ? ?*/
? ? ? ?sourceret[i] = ret
? ? ?})
? ?}
// 下面是錦上添花的存儲錯誤日志內(nèi)容,大家看看就行
? ?// 打印錯誤
? ?const errorTime = `${new Date().toLocaleString()}\r\n`
? ?const errorUserAgent = `${userAgent}\r\n`
? ?const errorPath = `${pathname}\r\n`
? ?const errorName = `${name}\r\n`
? ?let errorStack = ''
? ?for (let i in sourceret) {
? ? ?errorStack += `${sourceret[i].name}\r\n${sourceret[i].source.slice(sourceret[i].source.indexOf('src'))}:${sourceret[i].line}:${sourceret[i].column}\r\n`
? ? ?await readFileToArr(sourceret[i].source.slice(sourceret[i].source.indexOf('src')), (arr) => {
? ? ? ?let leftSpace = ''
? ? ? ?for (let j = 0; j < arr[sourceret[i].line - 1].trim().indexOf(sourceret[i].name); j++) {
? ? ? ? ?leftSpace += '-'
? ? ? ?}
? ? ? ?leftSpace += '△\r\n'
? ? ? ?errorStack += arr[sourceret[i].line - 1].trim() + '\r\n' + leftSpace
? ? ?})
? ?}
? ?let data = errorTime + errorUserAgent + errorPath + errorName + errorStack + '\r\n'
? ?// 當前時間
? ?const nowLocalDate = `${new Date().getFullYear()}-${new Date().getMonth() + 1}-${new Date().getDate()}`
? ?/** 打印錯誤內(nèi)容到{當前時間}_error.log */
? ?fs.appendFile(`${nowLocalDate}_error.log`, data, function (err) {
? ? ?if (err) {
? ? ? ?console.log(err)
? ? ?}
? ?})
最后
這個錯誤日志的存儲最終暫時還是擱置了,因為還涉及到埋點等的問題,需要我們后期進一步的研究。希望這個文章能夠給想要進行線上前端報錯存儲的你帶來一些幫助。