前端性能與異常上報

概述

對于后臺開發來說,記錄日志是一種非常常見的開發習慣,通常我們會使用try...catch代碼塊來主動捕獲錯誤、對于每次接口調用,也會記錄下每次接口調用的時間消耗,以便我們監控服務器接口性能,進行問題排查。

剛進公司時,在進行Node.js的接口開發時,我不太習慣每次排查問題都要通過跳板機登上服務器看日志,后來慢慢習慣了這種方式。

舉個例子:

/**  * 獲取列表數據  * @parma req, res  */ exports.getList = async function (req, res) {     //獲取請求參數     const openId = req.session.userinfo.openId;     logger.info(`handler getList, user openId is ${openId}`);     try {         // 拿到列表數據         const startTime = new Date().getTime();         let res = await ListService.getListFromDB(openId);         logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() - startDate}`);         // 對數據處理,返回給前端         // ...     } catch(error) {         logger.error(`handler getList is error, ${JSON.stringify(error)}`);     } }; 復制代碼

以下代碼經常會出現在用Node.js的接口中,在接口中會統計查詢DB所耗時間、亦或是統計RPC服務調用所耗時間,以便監測性能瓶頸,對性能做優化;又或是對異常使用try ... catch主動捕獲,以便隨時對問題進行回溯、還原問題的場景,進行bug的修復。

而對于前端來說呢?可以看以下的場景。

最近在進行一個需求開發時,偶爾發現webgl渲染影像失敗的情況,或者說影像會出現解析失敗的情況,我們可能根本不知道哪張影像會解析或渲染失??;又或如最近開發的另外一個需求,我們會做一個關于webgl渲染時間的優化和影像預加載的需求,如果缺乏性能監控,該如何統計所做的渲染優化和影像預加載優化的優化比例,如何證明自己所做的事情具有價值呢?可能是通過測試同學的黑盒測試,對優化前后的時間進行錄屏,分析從進入頁面到影像渲染完成到底經過了多少幀圖像。這樣的數據,可能既不準確、又較為片面,設想測試同學并不是真正的用戶,也無法還原真實的用戶他們所處的網絡環境?;剡^頭來發現,我們的項目,雖然在服務端層面做好了日志和性能統計,但在前端對異常的監控和性能的統計。對于前端的性能與異常上報的可行性探索是有必要的。

異常捕獲

對于前端來說,我們需要的異常捕獲無非為以下兩種:

  • 接口調用情況;

  • 頁面邏輯是否錯誤,例如,用戶進入頁面后頁面顯示白屏;

對于接口調用情況,在前端通常需要上報客戶端相關參數,例如:用戶OS與瀏覽器版本、請求參數(如頁面ID);而對于頁面邏輯是否錯誤問題,通常除了用戶OS與瀏覽器版本外,需要的是報錯的堆棧信息及具體報錯位置。

異常捕獲方法

全局捕獲

可以通過全局監聽異常來捕獲,通過window.onerror或者addEventListener,看以下例子:

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); // 異常堆棧信息   // ...   // 異常上報 }; throw new Error('這是一個錯誤'); 復制代碼

前端性能與異常上報的圖1

通過window.onerror事件,可以得到具體的異常信息、異常文件的URL、異常的行號與列號及異常的堆棧信息,再捕獲異常后,統一上報至我們的日志服務器。

亦或是,通過window.addEventListener方法來進行異常上報,道理同理:

window.addEventListener('error', function() {   console.log(error);   // ...   // 異常上報 }); throw new Error('這是一個錯誤'); 復制代碼

前端性能與異常上報的圖2

try... catch

使用try... catch雖然能夠較好地進行異常捕獲,不至于使得頁面由于一處錯誤掛掉,但try ... catch捕獲方式顯得過于臃腫,大多代碼使用try ... catch包裹,影響代碼可讀性。

常見問題

跨域腳本無法準確捕獲異常

通常情況下,我們會把靜態資源,如JavaScript腳本放到專門的靜態資源服務器,亦或者CDN,看以下例子:

<!DOCTYPE html> <html> <head>   <title></title> </head> <body>   <script type="text/javascript">     // 在index.html     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); // 異常堆棧信息       // ...       // 異常上報     };   </script>   <script src="./error.js"></script> </body> </html> 復制代碼
// error.js throw new Error('這是一個錯誤'); 復制代碼

前端性能與異常上報的圖3

結果顯示,跨域之后window.onerror根本捕獲不到正確的異常信息,而是統一返回一個Script error,

解決方案:對script標簽增加一個crossorigin=”anonymous”,并且服務器添加Access-Control-Allow-Origin。

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

sourceMap

通常在生產環境下的代碼是經過webpack打包后壓縮混淆的代碼,所以我們可能會遇到這樣的問題,如圖所示:

@4I0V0U{TTQ8VO5L)F4S@O8.png 前端性能與異常上報的圖5

我們發現所有的報錯的代碼行數都在第一行了,為什么呢?這是因為在生產環境下,我們的代碼被壓縮成了一行:

!function(e){var n={};function r(o){if(n[o])return n[o].exports;var t=n[o]={i:o,l:!1,exports:{}};return e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return o},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){throw window.onerror=function(e,n,r,o,t){console.log("errorMessage: "+e),console.log("scriptURI: "+n),console.log("lineNo: "+r),console.log("columnNo: "+o),console.log("error: "+t);var l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var u=new XMLHttpRequest;u.open("post","/middleware/errorMsg",!0),u.setRequestHeader("Content-Type","application/json"),u.send(JSON.stringify(l))}},new Error("這是一個錯誤")}]); 復制代碼

在我的開發過程中也遇到過這個問題,我在開發一個功能組件庫的時候,使用npm link了我的組件庫,但是由于組件庫被npm link后是打包后的生產環境下的代碼,所有的報錯都定位到了第一行。

解決辦法是開啟webpacksource-map,我們利用webpack打包后的生成的一份.map的腳本文件就可以讓瀏覽器對錯誤位置進行追蹤了。此處可以參考webpack document。

其實就是webpack.config.js中加上一行devtool: 'source-map',如下所示,為示例的webpack.config.js

var path = require('path'); module.exports = {     devtool: 'source-map',     mode: 'development',     entry: './client/index.js',     output: {         filename: 'bundle.js',         path: path.resolve(__dirname, 'client')     } } 復制代碼

webpack打包后生成對應的source-map,這樣瀏覽器就能夠定位到具體錯誤的位置:

前端性能與異常上報的圖6

開啟source-map的缺陷是兼容性,目前只有Chrome瀏覽器和Firefox瀏覽器才對source-map支持。不過我們對這一類情況也有解決辦法??梢允褂靡?code>npm庫來支持source-map,可以參考mozilla/source-map。這個npm庫既可以運行在客戶端也可以運行在服務端,不過更為推薦的是在服務端使用Node.js對接收到的日志信息時使用source-map解析,以避免源代碼的泄露造成風險,如下代碼所示:

const express = require('express'); const fs = require('fs'); const router = express.Router(); const sourceMap = require('source-map'); const path = require('path'); const resolve = file => path.resolve(__dirname, file); // 定義post接口 router.get('/error/', async function(req, res) {     // 獲取前端傳過來的報錯對象     let error = JSON.parse(req.query.error);     let url = error.scriptURI; // 壓縮文件路徑     if (url) {         let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路徑         // 解析sourceMap         let consumer = await new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一個promise對象         // 解析原始報錯數據         let result = consumer.originalPositionFor({             line: error.lineNo, // 壓縮后的行號             column: error.columnNo // 壓縮后的列號         });         console.log(result);     } }); module.exports = router; 復制代碼

如下圖所示,我們已經可以看到,在服務端已經成功解析出了具體錯誤的行號、列號,我們可以通過日志的方式進行記錄,達到了前端異常監控的目的。

J5%U]9XQY8H5[W~M_~9$~3Y.png

Vue捕獲異常

在我的項目中就遇到這樣的問題,使用了js-tracker這樣的插件來統一進行全局的異常捕獲和日志上報,結果發現我們根本捕獲不到Vue組件的異常,查閱資料得知,在Vue中,異常可能被Vue自身給try ... catch了,不會傳到window.onerror事件觸發,那么我們如何把Vue組件中的異常作統一捕獲呢?

使用Vue.config.errorHandler這樣的Vue全局配置,可以在Vue指定組件的渲染和觀察期間未捕獲錯誤的處理函數。這個處理函數被調用時,可獲取錯誤信息和Vue 實例。

Vue.config.errorHandler = function (err, vm, info) {   // handle error   // `info` 是 Vue 特定的錯誤信息,比如錯誤所在的生命周期鉤子   // 只在 2.2.0+ 可用 } 復制代碼

React中,可以使用ErrorBoundary組件包括業務組件的方式進行異常捕獲,配合React 16.0+新出的componentDidCatch API,可以實現統一的異常捕獲和日志上報。

class ErrorBoundary extends React.Component {   constructor(props) {     super(props);     this.state = { hasError: false };   }   componentDidCatch(error, info) {     // Display fallback UI     this.setState({ hasError: true });     // You can also log the error to an error reporting service     logErrorToMyService(error, info);   }   render() {     if (this.state.hasError) {       // You can render any custom fallback UI       return <h1>Something went wrong.</h1>;     }     return this.props.children;   } } 復制代碼

使用方式如下:

<ErrorBoundary>   <MyWidget /> </ErrorBoundary> 復制代碼

性能監控

最簡單的性能監控

最常見的性能監控需求則是需要我們統計用戶從開始請求頁面到所有DOM元素渲染完成的時間,也就是俗稱的首屏加載時間,DOM提供了這一接口,監聽documentDOMContentLoaded事件與windowload事件可統計頁面首屏加載時間即所有DOM渲染時間:

<!DOCTYPE html> <html> <head>   <title></title>   <script type="text/javascript">     // 記錄頁面加載開始時間     var timerStart = Date.now();   </script>   <!-- 加載靜態資源,如樣式資源 --> </head> <body>   <!-- 加載靜態JS資源 -->   <script type="text/javascript">     document.addEventListener('DOMContentLoaded', function() {       console.log("DOM 掛載時間: ", Date.now() - timerStart);       // 性能日志上報     });     window.addEventListener('load', function() {       console.log("所有資源加載完成時間: ", Date.now()-timerStart);       // 性能日志上報     });   </script> </body> </html> 復制代碼

對于使用框架,如Vue或者說React,組件是異步渲染然后掛載到DOM的,在頁面初始化時并沒有太多的DOM節點,可以參考下文關于首屏時間采集自動化的解決方案來對渲染時間進行打點。

performance

但是以上時間的監控過于粗略,例如我們想統計文檔的網絡加載耗時、解析DOM的耗時與渲染DOM的耗時,就不太好辦到了,所幸的是瀏覽器提供了window.performance接口,具體可見MDN文檔

前端性能與異常上報的圖8

幾乎所有瀏覽器都支持window.performance接口,下面來看看在控制臺打印window.performance可以得到些什么:

前端性能與異常上報的圖9

可以看到,window,performance主要包括有memory、navigationtiming以及timeOriginonresourcetimingbufferfull方法。

  • navigation對象提供了在指定的時間段里發生的操作相關信息,包括頁面是加載還是刷新、發生了多少次重定向等等。

  • timing對象包含延遲相關的性能信息。這是我們頁面加載性能優化需求中主要上報的相關信息。

  • memoryChrome添加的一個非標準擴展,這個屬性提供了一個可以獲取到基本內存使用情況的對象。在其它瀏覽器應該考慮到這個API的兼容處理。

  • timeOrigin則返回性能測量開始時的時間的高精度時間戳。如圖所示,精確到了小數點后四位。

  • onresourcetimingbufferfull方法,它是一個在resourcetimingbufferfull事件觸發時會被調用的event handler。這個事件當瀏覽器的資源時間性能緩沖區已滿時會觸發??梢酝ㄟ^監聽這一事件觸發來預估頁面crash,統計頁面crash概率,以便后期的性能優化,如下示例所示:

function buffer_full(event) {   console.log("WARNING: Resource Timing Buffer is FULL!");   performance.setResourceTimingBufferSize(200); } function init() {   // Set a callback if the resource buffer becomes filled   performance.onresourcetimingbufferfull = buffer_full; } <body onload="init()"> 復制代碼

計算網站性能

使用performancetiming屬性,可以拿到頁面性能相關的數據,這里在很多文章都有提到關于利用window.performance.timing記錄頁面性能的文章,例如alloyteam團隊寫的初探 performance – 監控網頁與程序性能,對于timing的各項屬性含義,可以借助摘自此文的下圖理解,以下代碼摘自此文作為計算網站性能的工具函數參考:

前端性能與異常上報的圖10

// 獲取 performance 數據 var performance = {       // memory 是非標準屬性,只在 Chrome 有     // 財富問題:我有多少內存     memory: {         usedJSHeapSize:  16100000, // JS 對象(包括V8引擎內部對象)占用的內存,一定小于 totalJSHeapSize         totalJSHeapSize: 35100000, // 可使用的內存         jsHeapSizeLimit: 793000000 // 內存大小限制     },       //  哲學問題:我從哪里來?     navigation: {         redirectCount: 0, // 如果有重定向的話,頁面通過幾次重定向跳轉而來         type: 0           // 0   即 TYPE_NAVIGATENEXT 正常進入的頁面(非刷新、非重定向等)                           // 1   即 TYPE_RELOAD       通過 window.location.reload() 刷新的頁面                           // 2   即 TYPE_BACK_FORWARD 通過瀏覽器的前進后退按鈕進入的頁面(歷史記錄)                           // 255 即 TYPE_UNDEFINED    非以上方式進入的頁面     },       timing: {         // 在同一個瀏覽器上下文中,前一個網頁(與當前頁面不一定同域)unload 的時間戳,如果無前一個網頁 unload ,則與 fetchStart 值相等         navigationStart: 1441112691935,           // 前一個網頁(與當前頁面同域)unload 的時間戳,如果無前一個網頁 unload 或者前一個網頁與當前頁面不同域,則值為 0         unloadEventStart: 0,           // 和 unloadEventStart 相對應,返回前一個網頁 unload 事件綁定的回調函數執行完畢的時間戳         unloadEventEnd: 0,           // 第一個 HTTP 重定向發生時的時間。有跳轉且是同域名內的重定向才算,否則值為 0          redirectStart: 0,           // 最后一個 HTTP 重定向完成時的時間。有跳轉且是同域名內部的重定向才算,否則值為 0          redirectEnd: 0,           // 瀏覽器準備好使用 HTTP 請求抓取文檔的時間,這發生在檢查本地緩存之前         fetchStart: 1441112692155,           // DNS 域名查詢開始的時間,如果使用了本地緩存(即無 DNS 查詢)或持久連接,則與 fetchStart 值相等         domainLookupStart: 1441112692155,           // DNS 域名查詢完成的時間,如果使用了本地緩存(即無 DNS 查詢)或持久連接,則與 fetchStart 值相等         domainLookupEnd: 1441112692155,           // HTTP(TCP) 開始建立連接的時間,如果是持久連接,則與 fetchStart 值相等         // 注意如果在傳輸層發生了錯誤且重新建立連接,則這里顯示的是新建立的連接開始的時間         connectStart: 1441112692155,           // HTTP(TCP) 完成建立連接的時間(完成握手),如果是持久連接,則與 fetchStart 值相等         // 注意如果在傳輸層發生了錯誤且重新建立連接,則這里顯示的是新建立的連接完成的時間         // 注意這里握手結束,包括安全連接建立完成、SOCKS 授權通過         connectEnd: 1441112692155,           // HTTPS 連接開始的時間,如果不是安全連接,則值為 0         secureConnectionStart: 0,           // HTTP 請求讀取真實文檔開始的時間(完成建立連接),包括從本地讀取緩存         // 連接錯誤重連時,這里顯示的也是新建立連接的時間         requestStart: 1441112692158,           // HTTP 開始接收響應的時間(獲取到第一個字節),包括從本地讀取緩存         responseStart: 1441112692686,           // HTTP 響應全部接收完成的時間(獲取到最后一個字節),包括從本地讀取緩存         responseEnd: 1441112692687,           // 開始解析渲染 DOM 樹的時間,此時 Document.readyState 變為 loading,并將拋出 readystatechange 相關事件         domLoading: 1441112692690,           // 完成解析 DOM 樹的時間,Document.readyState 變為 interactive,并將拋出 readystatechange 相關事件         // 注意只是 DOM 樹解析完成,這時候并沒有開始加載網頁內的資源         domInteractive: 1441112693093,           // DOM 解析完成后,網頁內資源加載開始的時間         // 在 DOMContentLoaded 事件拋出前發生         domContentLoadedEventStart: 1441112693093,           // DOM 解析完成后,網頁內資源加載完成的時間(如 JS 腳本加載執行完畢)         domContentLoadedEventEnd: 1441112693101,           // DOM 樹解析完成,且資源也準備就緒的時間,Document.readyState 變為 complete,并將拋出 readystatechange 相關事件         domComplete: 1441112693214,           // load 事件發送給文檔,也即 load 回調函數開始執行的時間         // 注意如果沒有綁定 load 事件,值為 0         loadEventStart: 1441112693214,           // load 事件的回調函數執行完畢的時間         loadEventEnd: 1441112693215           // 字母順序         // connectEnd: 1441112692155,         // connectStart: 1441112692155,         // domComplete: 1441112693214,         // domContentLoadedEventEnd: 1441112693101,         // domContentLoadedEventStart: 1441112693093,         // domInteractive: 1441112693093,         // domLoading: 1441112692690,         // domainLookupEnd: 1441112692155,         // domainLookupStart: 1441112692155,         // fetchStart: 1441112692155,         // loadEventEnd: 1441112693215,         // loadEventStart: 1441112693214,         // navigationStart: 1441112691935,         // redirectEnd: 0,         // redirectStart: 0,         // requestStart: 1441112692158,         // responseEnd: 1441112692687,         // responseStart: 1441112692686,         // secureConnectionStart: 0,         // unloadEventEnd: 0,         // unloadEventStart: 0     } }; 復制代碼
// 計算加載時間 function getPerformanceTiming() {     var performance = window.performance;     if (!performance) {         // 當前瀏覽器不支持         console.log('你的瀏覽器不支持 performance 接口');         return;     }     var t = performance.timing;     var times = {};     //【重要】頁面加載完成的時間     //【原因】這幾乎代表了用戶等待頁面可用的時間     times.loadPage = t.loadEventEnd - t.navigationStart;     //【重要】解析 DOM 樹結構的時間     //【原因】反省下你的 DOM 樹嵌套是不是太多了!     times.domReady = t.domComplete - t.responseEnd;     //【重要】重定向的時間     //【原因】拒絕重定向!比如,http://example.com/ 就不該寫成 http://example.com     times.redirect = t.redirectEnd - t.redirectStart;     //【重要】DNS 查詢時間     //【原因】DNS 預加載做了么?頁面內是不是使用了太多不同的域名導致域名查詢的時間太長?     // 可使用 HTML5 Prefetch 預查詢 DNS ,見:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)                 times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;     //【重要】讀取頁面第一個字節的時間     //【原因】這可以理解為用戶拿到你的資源占用的時間,加異地機房了么,加CDN 處理了么?加帶寬了么?加 CPU 運算速度了么?     // TTFB 即 Time To First Byte 的意思     // 維基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte     times.ttfb = t.responseStart - t.navigationStart;     //【重要】內容加載完成的時間     //【原因】頁面內容經過 gzip 壓縮了么,靜態資源 css/js 等壓縮了么?     times.request = t.responseEnd - t.requestStart;     //【重要】執行 onload 回調函數的時間     //【原因】是否太多不必要的操作都放到 onload 回調函數里執行了,考慮過延遲加載、按需加載的策略么?     times.loadEvent = t.loadEventEnd - t.loadEventStart;     // DNS 緩存時間     times.appcache = t.domainLookupStart - t.fetchStart;     // 卸載頁面的時間     times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;     // TCP 建立連接完成握手的時間     times.connect = t.connectEnd - t.connectStart;     return times; } 復制代碼

日志上報

單獨的日志域名

對于日志上報使用單獨的日志域名的目的是避免對業務造成影響。其一,對于服務器來說,我們肯定不希望占用業務服務器的計算資源,也不希望過多的日志在業務服務器堆積,造成業務服務器的存儲空間不夠的情況。其二,我們知道在頁面初始化的過程中,會對頁面加載時間、PV、UV等數據進行上報,這些上報請求會和加載業務數據幾乎是同時刻發出,而瀏覽器一般會對同一個域名的請求量有并發數的限制,如Chrome會有對并發數為6個的限制。因此需要對日志系統單獨設定域名,最小化對頁面加載性能造成的影響。

跨域的問題

對于單獨的日志域名,肯定會涉及到跨域的問題,采取的解決方案一般有以下兩種:

  • 一種是構造空的Image對象的方式,其原因是請求圖片并不涉及到跨域的問題;

var url = 'xxx'; new Image().src = url; 復制代碼
  • 利用Ajax上報日志,必須對日志服務器接口開啟跨域請求頭部Access-Control-Allow-Origin:*,這里Ajax就并不強制使用GET請求了,即可克服URL長度限制的問題。

if (XMLHttpRequest) {   var xhr = new XMLHttpRequest();   xhr.open('post', 'https://log.xxx.com', true); // 上報給node中間層處理   xhr.setRequestHeader('Content-Type', 'application/json'); // 設置請求頭   xhr.send(JSON.stringify(errorObj)); // 發送參數 } 復制代碼

在我的項目中使用的是第一種的方式,也就是構造空的Image對象,但是我們知道對于GET請求會有長度的限制,需要確保的是請求的長度不會超過閾值。

省去響應主體

對于我們上報日志,其實對于客戶端來說,并不需要考慮上報的結果,甚至對于上報失敗,我們也不需要在前端做任何交互,所以上報來說,其實使用HEAD請求就夠了,接口返回空的結果,最大地減少上報日志造成的資源浪費。

合并上報

類似于雪碧圖的思想,如果我們的應用需要上報的日志數量很多,那么有必要合并日志進行統一的上報。

解決方案可以是嘗試在用戶離開頁面或者組件銷毀時發送一個異步的POST請求來進行上報,但是嘗試在卸載(unload)文檔之前向web服務器發送數據。保證在文檔卸載期間發送數據一直是一個困難。因為用戶代理通常會忽略在卸載事件處理器中產生的異步XMLHttpRequest,因為此時已經會跳轉到下一個頁面。所以這里是必須設置為同步的XMLHttpRequest請求嗎?

window.addEventListener('unload', logData, false); function logData() {     var client = new XMLHttpRequest();     client.open("POST", "/log", false); // 第三個參數表明是同步的 xhr     client.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");     client.send(analyticsData); } 復制代碼

使用同步的方式勢必會對用戶體驗造成影響,甚至會讓用戶感受到瀏覽器卡死感覺,對于產品而言,體驗非常不好,通過查閱MDN文檔,可以使用sendBeacon()方法,將會使用戶代理在有機會時異步地向服務器發送數據,同時不會延遲頁面的卸載或影響下一導航的載入性能。這就解決了提交分析數據時的所有的問題:使它可靠,異步并且不會影響下一頁面的加載。此外,代碼實際上還要比其他技術簡單!

下面的例子展示了一個理論上的統計代碼模式——通過使用sendBeacon()方法向服務器發送數據。

window.addEventListener('unload', logData, false); function logData() {     navigator.sendBeacon("/log", analyticsData); } 復制代碼

小結

作為前端開發者而言,要對產品保持敬畏之心,時刻保持對性能追求極致,對異常不可容忍的態度。前端的性能監控與異常上報顯得尤為重要。

代碼難免有問題,對于異常可以使用window.onerror或者addEventListener的方式添加全局的異常捕獲偵聽函數,但可能使用這種方式無法正確捕獲到錯誤:對于跨域的腳本,需要對script標簽增加一個crossorigin=”anonymous”;對于生產環境打包的代碼,無法正確定位到異常產生的行數,可以使用source-map來解決;而對于使用框架的情況,需要在框架統一的異常捕獲處埋點。

而對于性能的監控,所幸的是瀏覽器提供了window.performance API,通過這個API,很便捷地獲取到當前頁面性能相關的數據。

而這些異常和性能數據如何上報呢?一般說來,為了避免對業務產生的影響,會單獨建立日志服務器和日志域名,但對于不同的域名,又會產生跨域的問題。我們可以通過構造空的Image對象來解決,亦或是通過設定跨域請求頭部Access-Control-Allow-Origin:*來解決。此外,如果上報的性能和日志數據高頻觸發,則可以在頁面unload時統一上報,而unload時的異步請求又可能會被瀏覽器所忽略,且不能改為同步請求。此時navigator.sendBeacon API可算幫了我們大忙,它可用于通過HTTP將少量數據異步傳輸到Web服務器。而忽略頁面unload時的影響。


作者:counterxing
來源:掘金

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP