談談前端異常捕獲與上報

来源:https://www.cnblogs.com/luozhihao/archive/2018/03/26/8635507.html
-Advertisement-
Play Games

關於 微信公眾號:前端呼啦圈(Love-FED) 我的博客:勞卜的博客 知乎專欄:前端呼啦圈 前言 Hello,大家好,又與大家見面了,這次給大家分享下前端異常監控中需要瞭解的異常捕獲與上報機制的一些要點,同時包含了實戰性質的參考代碼和流程。 首先,我們為什麼要進行異常捕獲和上報呢? 正所謂百密一疏 ...


關於

前言

Hello,大家好,又與大家見面了,這次給大家分享下前端異常監控中需要瞭解的異常捕獲與上報機制的一些要點,同時包含了實戰性質的參考代碼和流程。

首先,我們為什麼要進行異常捕獲和上報呢?

正所謂百密一疏,一個經過了大量測試及聯調的項目在有些時候還是會有十分隱蔽的bug存在,這種複雜而又不可預見性的問題唯有通過完善的監控機制才能有效的減少其帶來的損失,因此對於直面用戶的前端而言,異常捕獲與上報是至關重要的。

雖然目前市面上已經有一些非常完善的前端監控系統存在,如sentrybugsnag等,但是知己知彼,才能百戰不殆,唯有瞭解原理,摸清邏輯,使用起來才能得心應手。

異常捕獲方法

1. try catch

通常,為了判斷一段代碼中是否存在異常,我們會這一寫:

try {
    var a = 1;
    var b = a + c;
} catch (e) {
    // 捕獲處理
    console.log(e); // ReferenceError: c is not defined
}

使用try catch能夠很好的捕獲異常並對應進行相應處理,不至於讓頁面掛掉,但是其存在一些弊端,比如需要在捕獲異常的代碼上進行包裹,會導致頁面臃腫不堪,不適用於整個項目的異常捕獲。

2. window.onerror

相比try catch來說window.onerror提供了全局監聽異常的功能:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
    console.log('errorMessage: ' + errorMessage); // 異常信息
    console.log('scriptURI: ' + scriptURI); // 異常文件路徑
    console.log('lineNo: ' + lineNo); // 異常行號
    console.log('columnNo: ' + columnNo); // 異常列號
    console.log('error: ' + error); // 異常堆棧信息
};

console.log(a);

如圖:

window.onerror即提供了我們錯誤的信息,還提供了錯誤行列號,可以精準的進行定位,如此似乎正是我們想要的,但是接下來便是填坑過程。

異常捕獲問題

1. Script error.

我們合乎情理地在本地頁面進行嘗試捕獲異常,如:

<!-- http://localhost:3031/ -->
<script>
window.onerror = function() {
    console.log(arguments);
};
</script>
<script src="http://cdn.xxx.com/index.js"></script>

這裡我們把靜態資源放到異域上進行優化載入,但是捕獲的異常信息卻是:

經過分析發現,跨域之後window.onerror是無法捕獲異常信息的,所以統一返回Script error.,解決方案便是script屬性配置 crossorigin=”anonymous” 並且伺服器添加Access-Control-Allow-Origin。

<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

一般的CDN網站都會將Access-Control-Allow-Origin配置為*,意思是所有域都可以訪問。

2. sourceMap

解決跨域或者將腳本存放在同域之後,你可能會將代碼壓縮一下再發佈,這時候便出現了壓縮後的代碼無法找到原始報錯位置的問題。如圖,我們用webpack將代碼打包壓縮成bundle.js:

// webpack.config.js
var path = require('path');

// webpack 4.1.1
module.exports = {
    mode: 'development',
    entry: './client/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'client')
    }
}

最後我們頁面引入的腳本文件是這樣的:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;

所以我們看到的異常信息是這樣的:

lineNo可能是一個非常小的數字,一般是1,而columnNo會是一個很大的數字,這裡是730,因為所有代碼都壓縮到了一行。

那麼該如何解決呢?聰明的童鞋可能已經猜到啟用source-map了,沒錯,我們利用webpack打包壓縮後生成一份對應腳本的map文件就能進行追蹤了,在webpack中開啟source-map功能:

module.exports = {
    ...
    devtool: '#source-map',
    ...
}

打包壓縮的文件末尾會帶上這樣的註釋:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;
//# sourceMappingURL=bundle.js.map

意思是該文件對應的map文件為bundle.js.map。下麵便是一個source-map文件的內容,是一個JSON對象:

version: 3, // Source map的版本
sources: ["webpack:///webpack/bootstrap", ...], // 轉換前的文件
names: ["installedModules", "__webpack_require__", ...], // 轉換前的所有變數名和屬性名
mappings: "aACA,IAAAA,KAGA,SAAAC...", // 記錄位置信息的字元串
file: "bundle.js", // 轉換後的文件名
sourcesContent: ["// The module cache var installedModules = {};..."], // 源代碼
sourceRoot: "" // 轉換前的文件所在的目錄

如果你想詳細瞭解關於sourceMap的知識,可以前往:JavaScript Source Map 詳解

如此,既然我們拿到了對應腳本的map文件,那麼我們該如何進行解析獲取壓縮前文件的異常信息呢?這個我會在下麵異常上報的時候進行介紹。

3. MVVM框架

現在越來越多的項目開始使用前端框架,在MVVM框架中如果你一如既往的想使用window.onerror來捕獲異常,那麼很可能會竹籃打水一場空,或許根本捕獲不到,因為你的異常信息被框架自身的異常機制捕獲了。比如Vue 2.x中我們應該這樣捕獲全局異常

Vue.config.errorHandler = function (err, vm, info) {
    let { 
        message, // 異常信息
        name, // 異常名稱
        script,  // 異常腳本url
        line,  // 異常行號
        column,  // 異常列號
        stack  // 異常堆棧信息
    } = err;

    // vm為拋出異常的 Vue 實例
    // info為 Vue 特定的錯誤信息,比如錯誤所在的生命周期鉤子
}

目前script、line、column這3個信息列印出來是undefined,不過這些信息在stack中都可以找到,可以通過正則匹配去進行獲取,然後進行上報。

同樣的在react也提供了異常處理的方式,在 React 16.x 版本中引入了 Error Boundary:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });

        // 將異常信息上報給伺服器
        logErrorToMyService(error, info); 
    }

    render() {
        if (this.state.hasError) {
            return '出錯了';
        }

        return this.props.children;
    }
}

然後我們就可以這樣使用該組件:

<ErrorBoundary>
    <MyWidget />
</ErrorBoundary>

詳見官方文檔:Error Handling in React 16

異常上報

以上介紹了前端異常捕獲的相關知識點,那麼接下來我們既然成功捕獲了異常,那麼該如何上報呢?

在腳本代碼沒有被壓縮的情況下可以直接捕獲後上傳對應的異常信息,這裡就不做介紹了,下麵主要講解常見的處理壓縮文件上報的方法。

1. 提交異常

當捕獲到異常時,我們可以將異常信息傳遞給介面,以window.onerror為例:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {

    // 構建錯誤對象
    var errorObj = {
        errorMessage: errorMessage || null,
        scriptURI: scriptURI || null,
        lineNo: lineNo || null,
        columnNo: columnNo || null,
        stack: error && error.stack ? error.stack : null
    };

    if (XMLHttpRequest) {
        var xhr = new XMLHttpRequest();

        xhr.open('post', '/middleware/errorMsg', true); // 上報給node中間層處理
        xhr.setRequestHeader('Content-Type', 'application/json'); // 設置請求頭
        xhr.send(JSON.stringify(errorObj)); // 發送參數
    }
}

2. sourceMap解析

其實source-map格式的文件是一種數據類型,既然是數據類型那麼肯定有解析它的辦法,目前市面上也有專門解析它的相應工具包,在瀏覽器環境或者node環境下比較流行的是一款叫做’source-map’的插件。

通過require該插件,前端瀏覽器可以對map文件進行解析,但因為前端解析速度較慢,所以這裡不做推薦,我們還是使用伺服器解析。如果你的應用有node中間層,那麼你完全可以將異常信息提交到中間層,然後解析map文件後將數據傳遞給後臺伺服器,中間層代碼如下:

const express = require('express');
const fs = require('fs');
const router = express.Router();
const fetch = require('node-fetch');
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);

// 定義post介面
router.post('/errorMsg/', function(req, res) {
    let error = req.body; // 獲取前端傳過來的報錯對象
    let url = error.scriptURI; // 壓縮文件路徑

    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路徑

        // 解析sourceMap
        let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一個promise對象

        smc.then(function(result) {

            // 解析原始報錯數據
            let ret = result.originalPositionFor({
                line: error.lineNo, // 壓縮後的行號
                column: error.columnNo // 壓縮後的列號
            });

            let url = ''; // 上報地址

            // 將異常上報至後臺
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    errorMessage: error.errorMessage, // 報錯信息
                    source: ret.source, // 報錯文件路徑
                    line: ret.line, // 報錯文件行號
                    column: ret.column, // 報錯文件列號
                    stack: error.stack // 報錯堆棧
                })
            }).then(function(response) {
                return response.json();
            }).then(function(json) {
                res.json(json);         
            });
        })
    }
});

module.exports = router;

這裡我們通過前端傳過來的異常文件路徑獲取伺服器端map文件地址,然後將壓縮後的行列號傳遞給sourceMap返回的promise對象進行解析,通過originalPositionFor方法我們能獲取到原始的報錯行列號和文件地址,最後通過ajax將需要的異常信息統一傳遞給後臺存儲,完成異常上報。下圖可以看到控制台列印出了經過解析後的真是報錯位置和文件:

附:source-map API

3. 註意點

以上是異常捕獲和上報的主要知識點和流程,還有一些需要註意的地方,比如你的應用訪問量很大,那麼一個小異常都可能會把你的伺服器搞掛,所以上報的時候可以進行信息過濾和採樣等,設置一個調控開關,伺服器也可以對相似的異常進行過濾,在一個時間段內不進行多次存儲。另外window.onerror這樣的異常捕獲不能捕獲promise的異常錯誤信息,這點需要註意。

最終大致的流程圖如下:

結語

前端異常捕獲與上報是前端異常監控的前提,瞭解並做好了異常數據的收集和分析才能實現一個完善的錯誤響應和處理機制,最終達成數據可視化。本文詳細實例代碼地址:https://github.com/luozhihao/error-catch-report


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、特點 學習一個東西,至少首先得知道它能做什麼?適合做什麼?有什麼優缺點吧? 傳統關係型資料庫,遵循三大範式。即原子性、唯一性、每列與主鍵直接關聯性。但是後來人們慢慢發現,不要把這些數據分散到多個表、節點或實體中,將這些信息收集到一個非規範化(也就是文檔)的結構中會更有意義。儘管兩個或兩個以上的文 ...
  • --CHARINDEX 返回指定字元的位置--2個參數,第一個是要查找的字元串 第二個參數:要搜索的字元串 參數3:開始查找的位置--查找位置從1開始,返回結果為0時表示為結果為空 SELECT CHARINDEX('bai','www.baidu.com') SELECT CHARINDEX('b ...
  • 1.安裝MySQL 2.啟動MySQL 3.為MySQL設置密碼 a.進入資料庫 b.修改MySQL密碼為123 4.設置遠程登陸 5.重啟MySQL資料庫 吐槽一句 Centos比Ubuntu好用 ...
  • 本篇文章主要介紹資料庫事務的四大特性ACID,以及資料庫的隔離級別。 ...
  • 一.安裝版本簡介 MySQL是一個小巧玲瓏但功能強大的資料庫,目前十分流行。但是官網給出的安裝包有兩種格式,一個是msi格式,一個是zip格式的。很多人下了zip格式的解壓發現沒有setup.exe,面對一堆文件一頭霧水,不知如何安裝。 MySQL安裝文件分為兩種,一種是msi格式的,一種是zip格 ...
  • viewDidLoad:是視圖第一次載入到記憶體中後調用的,viewWillApear:則是在每次視圖顯示到屏幕上之前調用。 參考資料:《iOS編程指南》 ...
  • 引子用戶行為統計(User Behavior Statistics, UBS)一直是移動互聯網產品中必不可少的環節,也俗稱埋點。在保證移動端流量不會受較大影響的前提下,PM們總是希望埋點覆蓋面越廣越好。目前常規的做法是將埋點代碼封裝成工具類,但凡工程中需要埋點(如點擊事件、頁面跳轉)的地方都插入埋點 ...
  • android QMI機制 概論 android QMI機制 QMUX android QMI機制 Qcril初始化流程 android QMI機制 QCRIL消息發送 android QMI機制 底層消息發送 android QMI機制 Modem消息接收 android QMI機制 modem消 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...