1. 程式人生 > 實用技巧 >前端錯誤監控,web前端開發

前端錯誤監控,web前端開發

前端錯誤監控在前端領域越來越重要,很多常見的bug,jquery is not define,Script error,為了給使用者更好的體驗,需要把前端可能出錯的概率事件給消除掉,提高系統的穩定性

內建錯誤物件

在程式碼執行出錯的時候瀏覽器會丟擲異常,比如使用未定義的變數丟擲ReferenceError,在物件裡使用未定義的函式丟擲TypeError。如果沒有做錯誤處理,通常會導致指令碼終止執行。javascript定義了7中內建的錯誤物件,有Error,RangeError,ReferenceError,SyntaxError,TypeError,URIError,EvalError

Error

通用的異常物件。我們通常使用Error來自定義異常,Error物件有name和message屬性,可以通過message來得到具體的錯誤資訊,比如

let error = new Error('介面報錯');
let name = error.name; // 'Error'
let msg = error.message;    // '介面報錯'

RangeError

超出指定範圍錯誤,比如宣告一個負數的陣列,使用toFixec超過了規定小數的位數(0-20)

new Array(-1)
(1.2).toFixed(21)

ReferenceError

訪問未定義的變數,比如

function foo() {
    bar++;    // bar未定義
}

TypeError

型別錯誤,比如一個變數不是函式,卻把它當做函式來呼叫

let a = 1;
a();    // 型別錯誤

SyntaxError

語法錯誤,一般出程式碼語句不完整,比如

let a = 1 > 0 ?    // 正則不完整
if (a) {         // 少了一個分號

URIError

在使用全域性的URI函式,引數錯誤的時候丟擲,比如

encodeURI('\uD800')
encodeURIComponent('\uD800')
decodeURI('%')
decodeURIComponent('%')

EvalError

呼叫eval()函式丟擲錯誤,例如

eval('3a')

錯誤處理

我們可以通過try/catch語法來捕捉錯誤。最常用的是在函式裡面捕捉錯誤,有錯誤就在catch處理

function foo () {
    try {
        bar();
    } catch (e) {
        // 錯誤處理,錯誤上報等等
        console.log(e);
        console.log(e.message)
    }
}

try/catch只能捕捉同步程式碼丟擲的錯誤,不能捕捉非同步程式碼丟擲的錯誤

// 下面在定時器外和async函式是捕捉不到非同步程式碼塊丟擲的錯誤
try {
    setTimeout(() => {
        throw new Error('async error')
    }, 0)
} catch (e) {
    console.log(e.message)
}

// async/await
async function foo () {
    let a = 1;
    let b = await a + 2;
    console.log(b);
    throw new Error('async error')
}

try {
    foo();
} catch (e) {
    console.log(e.message);
}

// 在非同步程式碼塊裡面的同步程式碼就可以捕捉到

setTimeout(() => {
    try {
        throw new Error('async error')
    } catch (e) {
        console.log(e.message);
    }
}, 0)

async function foo () {
    try {
        let a = 1;
        let b = await a + 2;
        console.log(b);
        throw new Error('async error')
    } catch (e) {
        console.log(e.message);
    }
}
foo();
複製程式碼

這種錯誤處理有一個弊端就是對每一個函式都需要進行try/catch捕捉再進行處理,需要寫很多重複的程式碼,其實可以使用一個全域性的error事件來捕獲所有的error

window.onerror = function(message, source, lineno, colno, error) {
  // 錯誤資訊,原始檔,行號
  console.log(message + '\n' + source + '\n' + lineno);
  // 禁止瀏覽器列印標準的錯誤資訊
  return true;
}

window.onerror可以捕捉上面的執行時錯誤和自定義丟擲的錯誤和非同步丟擲的錯誤,但是不能捕捉Script error和網路異常,還有promise錯誤

網路異常捕捉

網路異常可以在事件捕獲的階段捕捉到,通過window.addEventListener來實現,程式碼必須放在文件載入之前

// ie11和主流瀏覽器
window.addEventListener('error', function(e) {
  e.stopImmediatePropagation();
  const srcElement = e.srcElement;
  if (srcElement === window) {
    // 全域性錯誤
    console.log(e.message)
  } else {
    // 元素錯誤,比如引用資源報錯
    console.log(srcElement.tagName)
    console.log(srcElement.src);
  }
}, true)

Promise錯誤捕捉

promise的異常可以通過下面兩種捕捉方式

  • 通過then函式的第二個引數捕捉

  • 通過catch函式捕捉

    let pro = new Promise((resolve, reject) => { console.log(c); // 丟擲 c is not defined reject('some error happen'); })

誰先提前宣告錯誤捕捉回撥,誰就先捕捉,但是隻要有一個錯誤捕捉到了,後面的錯誤捕捉函式就不會呼叫到

pro.catch(err => {
  console.log(`通過catch捕捉錯誤: ${err}`);
}).then(res => {}, err => {
  console.log(`在then第二個引數捕捉錯誤: ${err}`); 
})

//通過catch捕捉錯誤: ReferenceError: c is not defined

pro.then(res => {}, err => {
  console.log(`在then第二個引數捕捉錯誤: ${err}`); 
}).catch(err => {
  console.log(`通過catch捕捉錯誤: ${err}`);
})

// 在then第二個引數捕捉錯誤: ReferenceError: c is not defined

如果promise例項自身沒有做錯誤捕捉,會丟擲一個全域性的錯誤unhandledrejection

window.addEventListener('unhandledrejection', function(e) {
  e.preventDefault();
  console.log(e.type)    // unhandledrejection
})

Async/await錯誤捕捉

async/await基於Promise實現的,它不能用於普通的回撥函式,可以在async通過try/catch處理同步或者非同步的錯誤

// 獲取資料
function getData () {
  return new Promise((resolve, reject) => {
    throw new Error('error')
  })
}

try {
  getData();
} catch (err) {
  // 這裡是無法捕捉到錯誤的
  console.log(err);
}

(async function f() {
  try {
    await getData();
  } catch (err) {
    // 這裡可以捕捉到錯誤
    console.log(err);
  }
})();

Script error

如果引用外鏈不同源的js檔案,外鏈不同源js檔案報錯,onerror只會提示Script error,無法精確到指定檔案和行數,可以通過script標籤的crossorigin="anonymous",設定了該屬性的話,那麼需要在伺服器對響應的靜態檔案設定Access-Control-Allow-Origin:*響應頭

<script type="text/javascript" src="http://localhost:3000/test/script.js" crossorigin="anonymous"></script>

這樣就可以捕捉到script.js檔案的的錯誤資訊,如下

壓縮js的錯誤定位

通過控制script標籤的crossorigin="anonymous"可以捕捉到不同域的js錯誤資訊,在線上的程式碼都是經過壓縮的,可以捕捉到的錯誤為壓縮後的行數和變數,可以通過node提供的source-map模組來定位上報錯誤資訊對應原始檔錯誤的行號

const path = require('path')
const sourceMap = require('source-map')
const fs = require('fs')

const readFile = function (url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, (err, res) => {
            if (err) {
                reject(err)
            } else {
                resolve(res);
            }
        })
    })
}

// 客戶端傳過來生產環境下的js檔案
const error = {"message":"Uncaught ReferenceError: a is not defined","source":"http://localhost:3000/dist/index.min.js","line":1,"column":588,"error":"ReferenceError: a is not defined"}

console.log(error);

// 根據source獲取source map檔案
async function getSourceMap(source) {
    let basename = path.basename(source);
    let sm = await readFile(path.join(__dirname, './dist/' + basename + '.map'));
    let smObj = {};
    try {
        smObj = JSON.parse(sm);
    } catch (err) {
        console.log('找不到對應的source map檔案')
    }
    return smObj;
}

async function analyze(errObj) {
    let rawSourceMap = await getSourceMap(errObj.source);
    try {
        await sourceMap.SourceMapConsumer.with(rawSourceMap, null, consumer => {
            let sourcePos = consumer.originalPositionFor({
                line: errObj.line,
                column: errObj.column
            });
            Object.assign(errObj, sourcePos);
            return errObj;
        });
    } catch (err) {
        console.log(err.message);
    }
    return errObj;
}

analyze(error).then(res => {
	// 定位錯誤後的具體資訊
    console.log(res);
});

上報的錯誤資訊是index.min.js檔案的第1行,第588列

解析後的錯誤資訊定位是在src/foo.js檔案的第2行,第11列

(foo.js是index.js引用的模組)