前端異常和性能監控(轉)

前端性能與異常上報概述
 
window.onerror = function(errorMessage, scriptURI, lineNo, columNo, error){
    console.log('errorMessage:'+errorMessage);
    console.log('scriptURI:'+scriptURI);
    console.log('lineNo:'+lineNo);//異常行號
    console.log('columNo:'+columNo);//異常列號
    console.log('error:'+error);//異常堆棧信息
}

 

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

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

舉個例子:html

/** * 獲取列表數據 * @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的修復。前端

而對於前端來講呢?能夠看如下的場景。vue

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

異常捕獲

對於前端來講,咱們須要的異常捕獲無非爲如下兩種:node

  • 接口調用狀況;
  • 頁面邏輯是否錯誤,例如,用戶進入頁面後頁面顯示白屏;

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

異常捕獲方法

全局捕獲

能夠經過全局監聽異常來捕獲,經過window.onerror或者addEventListener,看如下例子:git

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('這是一個錯誤'); 

 

 

經過window.onerror事件,能夠獲得具體的異常信息、異常文件的URL、異常的行號與列號及異常的堆棧信息,再捕獲異常後,統一上報至咱們的日誌服務器。github

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

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

 

 

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('這是一個錯誤'); 

 

 

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

解決方案:對script標籤增長一個crossorigin=」anonymous」,而且服務器添加Access-Control-Allow-Origin

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

sourceMap

一般在生產環境下的代碼是通過webpack打包後壓縮混淆的代碼,因此咱們可能會遇到這樣的問題,如圖所示:

 

 

咱們發現全部的報錯的代碼行數都在第一行了,爲何呢?這是由於在生產環境下,咱們的代碼被壓縮成了一行:

!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,這樣瀏覽器就可以定位到具體錯誤的位置:

 

 

開啓source-map的缺陷是兼容性,目前只有Chrome瀏覽器和Firefox瀏覽器纔對source-map支持。不過咱們對這一類狀況也有解決辦法。可使用引入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; 

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

 

 

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文檔

 

 

幾乎全部瀏覽器都支持window.performance接口,下面來看看在控制檯打印window.performance能夠獲得些什麼:

 

 

能夠看到,window,performance主要包括有memorynavigationtiming以及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的各項屬性含義,能夠藉助摘自此文的下圖理解,如下代碼摘自此文做爲計算網站性能的工具函數參考:

 

 

// 獲取 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時的影響。

相關文章
相關標籤/搜索