成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

前端性能與異常上報(bào)

gggggggbong / 2684人閱讀

摘要:回過頭來發(fā)現(xiàn),我們的項(xiàng)目,雖然在服務(wù)端層面做好了日志和性能統(tǒng)計(jì),但在前端對異常的監(jiān)控和性能的統(tǒng)計(jì)。對于前端的性能與異常上報(bào)的可行性探索是有必要的。這是我們頁面加載性能優(yōu)化需求中主要上報(bào)的相關(guān)信息。

概述

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

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

舉個例子:

/**
 * 獲取列表數(shù)據(jù)
 * @parma req, res
 */
exports.getList = async function (req, res) {
    //獲取請求參數(shù)
    const openId = req.session.userinfo.openId;
    logger.info(`handler getList, user openId is ${openId}`);

    try {
        // 拿到列表數(shù)據(jù)
        const startTime = new Date().getTime();
        let res = await ListService.getListFromDB(openId);
        logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() - startDate}`);
        // 對數(shù)據(jù)處理,返回給前端
        // ...
    } catch(error) {
        logger.error(`handler getList is error, ${JSON.stringify(error)}`);
    }
};

以下代碼經(jīng)常會出現(xiàn)在用Node.js的接口中,在接口中會統(tǒng)計(jì)查詢DB所耗時間、亦或是統(tǒng)計(jì)RPC服務(wù)調(diào)用所耗時間,以便監(jiān)測性能瓶頸,對性能做優(yōu)化;又或是對異常使用try ... catch主動捕獲,以便隨時對問題進(jìn)行回溯、還原問題的場景,進(jìn)行bug的修復(fù)。

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

最近在進(jìn)行一個需求開發(fā)時,偶爾發(fā)現(xiàn)webgl渲染影像失敗的情況,或者說影像會出現(xiàn)解析失敗的情況,我們可能根本不知道哪張影像會解析或渲染失?。挥只蛉缱罱_發(fā)的另外一個需求,我們會做一個關(guān)于webgl渲染時間的優(yōu)化和影像預(yù)加載的需求,如果缺乏性能監(jiān)控,該如何統(tǒng)計(jì)所做的渲染優(yōu)化和影像預(yù)加載優(yōu)化的優(yōu)化比例,如何證明自己所做的事情具有價值呢?可能是通過測試同學(xué)的黑盒測試,對優(yōu)化前后的時間進(jìn)行錄屏,分析從進(jìn)入頁面到影像渲染完成到底經(jīng)過了多少幀圖像。這樣的數(shù)據(jù),可能既不準(zhǔn)確、又較為片面,設(shè)想測試同學(xué)并不是真正的用戶,也無法還原真實(shí)的用戶他們所處的網(wǎng)絡(luò)環(huán)境。回過頭來發(fā)現(xiàn),我們的項(xiàng)目,雖然在服務(wù)端層面做好了日志和性能統(tǒng)計(jì),但在前端對異常的監(jiān)控和性能的統(tǒng)計(jì)。對于前端的性能與異常上報(bào)的可行性探索是有必要的。

異常捕獲

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

接口調(diào)用情況;

頁面邏輯是否錯誤,例如,用戶進(jìn)入頁面后頁面顯示白屏;

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

異常捕獲方法 全局捕獲

可以通過全局監(jiān)聽異常來捕獲,通過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); // 異常堆棧信息
  // ...
  // 異常上報(bào)
};
throw new Error("這是一個錯誤");

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

亦或是,通過window.addEventListener方法來進(jìn)行異常上報(bào),道理同理:

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

try... catch

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

常見問題 跨域腳本無法準(zhǔn)確捕獲異常

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




  


  
  

// error.js
throw new Error("這是一個錯誤");

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

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

sourceMap

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

我們發(fā)現(xiàn)所有的報(bào)錯的代碼行數(shù)都在第一行了,為什么呢?這是因?yàn)樵谏a(chǎn)環(huán)境下,我們的代碼被壓縮成了一行:

!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("這是一個錯誤")}]);

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

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

其實(shí)就是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打包后生成對應(yīng)的source-map,這樣瀏覽器就能夠定位到具體錯誤的位置:

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

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) {
    // 獲取前端傳過來的報(bào)錯對象
    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對象
        // 解析原始報(bào)錯數(shù)據(jù)
        let result = consumer.originalPositionFor({
            line: error.lineNo, // 壓縮后的行號
            column: error.columnNo // 壓縮后的列號
        });
        console.log(result);
    }
});
module.exports = router;

如下圖所示,我們已經(jīng)可以看到,在服務(wù)端已經(jīng)成功解析出了具體錯誤的行號、列號,我們可以通過日志的方式進(jìn)行記錄,達(dá)到了前端異常監(jiān)控的目的。

Vue捕獲異常

在我的項(xiàng)目中就遇到這樣的問題,使用了js-tracker這樣的插件來統(tǒng)一進(jìn)行全局的異常捕獲和日志上報(bào),結(jié)果發(fā)現(xiàn)我們根本捕獲不到Vue組件的異常,查閱資料得知,在Vue中,異??赡鼙?b>Vue自身給try ... catch了,不會傳到window.onerror事件觸發(fā),那么我們?nèi)绾伟?b>Vue組件中的異常作統(tǒng)一捕獲呢?

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

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的錯誤信息,比如錯誤所在的生命周期鉤子
  // 只在 2.2.0+ 可用
}
React中,可以使用ErrorBoundary組件包括業(yè)務(wù)組件的方式進(jìn)行異常捕獲,配合React 16.0+新出的componentDidCatch API,可以實(shí)現(xiàn)統(tǒng)一的異常捕獲和日志上報(bào)。
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 

Something went wrong.

; } return this.props.children; } }

使用方式如下:


  
性能監(jiān)控 最簡單的性能監(jiān)控

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




  
  
  


  
  

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

performance

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

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

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

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

timing對象包含延遲相關(guān)的性能信息。這是我們頁面加載性能優(yōu)化需求中主要上報(bào)的相關(guān)信息。

memoryChrome添加的一個非標(biāo)準(zhǔn)擴(kuò)展,這個屬性提供了一個可以獲取到基本內(nèi)存使用情況的對象。在其它瀏覽器應(yīng)該考慮到這個API的兼容處理。

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

onresourcetimingbufferfull方法,它是一個在resourcetimingbufferfull事件觸發(fā)時會被調(diào)用的event handler。這個事件當(dāng)瀏覽器的資源時間性能緩沖區(qū)已滿時會觸發(fā)??梢酝ㄟ^監(jiān)聽這一事件觸發(fā)來預(yù)估頁面crash,統(tǒng)計(jì)頁面crash概率,以便后期的性能優(yōu)化,如下示例所示:

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;
}
計(jì)算網(wǎng)站性能

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

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

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

跨域的問題

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

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

var url = "xxx";
new Image().src = url;

利用Ajax上報(bào)日志,必須對日志服務(wù)器接口開啟跨域請求頭部Access-Control-Allow-Origin:*,這里Ajax就并不強(qiáng)制使用GET請求了,即可克服URL長度限制的問題。

if (XMLHttpRequest) {
  var xhr = new XMLHttpRequest();
  xhr.open("post", "https://log.xxx.com", true); // 上報(bào)給node中間層處理
  xhr.setRequestHeader("Content-Type", "application/json"); // 設(shè)置請求頭
  xhr.send(JSON.stringify(errorObj)); // 發(fā)送參數(shù)
}

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

省去響應(yīng)主體

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

合并上報(bào)

類似于雪碧圖的思想,如果我們的應(yīng)用需要上報(bào)的日志數(shù)量很多,那么有必要合并日志進(jìn)行統(tǒng)一的上報(bào)。

解決方案可以是嘗試在用戶離開頁面或者組件銷毀時發(fā)送一個異步的POST請求來進(jìn)行上報(bào),但是嘗試在卸載(unload)文檔之前向web服務(wù)器發(fā)送數(shù)據(jù)。保證在文檔卸載期間發(fā)送數(shù)據(jù)一直是一個困難。因?yàn)橛脩舸硗ǔ雎栽谛遁d事件處理器中產(chǎn)生的異步XMLHttpRequest,因?yàn)榇藭r已經(jīng)會跳轉(zhuǎn)到下一個頁面。所以這里是必須設(shè)置為同步的XMLHttpRequest請求嗎?

window.addEventListener("unload", logData, false);

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

使用同步的方式勢必會對用戶體驗(yàn)造成影響,甚至?xí)層脩舾惺艿綖g覽器卡死感覺,對于產(chǎn)品而言,體驗(yàn)非常不好,通過查閱MDN文檔,可以使用sendBeacon()方法,將會使用戶代理在有機(jī)會時異步地向服務(wù)器發(fā)送數(shù)據(jù),同時不會延遲頁面的卸載或影響下一導(dǎo)航的載入性能。這就解決了提交分析數(shù)據(jù)時的所有的問題:使它可靠,異步并且不會影響下一頁面的加載。此外,代碼實(shí)際上還要比其他技術(shù)簡單!

下面的例子展示了一個理論上的統(tǒng)計(jì)代碼模式——通過使用sendBeacon()方法向服務(wù)器發(fā)送數(shù)據(jù)。

window.addEventListener("unload", logData, false);

function logData() {
    navigator.sendBeacon("/log", analyticsData);
}
小結(jié)

作為前端開發(fā)者而言,要對產(chǎn)品保持敬畏之心,時刻保持對性能追求極致,對異常不可容忍的態(tài)度。前端的性能監(jiān)控與異常上報(bào)顯得尤為重要。

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

而對于性能的監(jiān)控,所幸的是瀏覽器提供了window.performance API,通過這個API,很便捷地獲取到當(dāng)前頁面性能相關(guān)的數(shù)據(jù)。

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

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96403.html

相關(guān)文章

  • 前端監(jiān)控和前端埋點(diǎn)方案設(shè)計(jì)

    摘要:異常監(jiān)控包括前端腳本執(zhí)行報(bào)錯等。本文針對整個前端監(jiān)控,設(shè)計(jì)適用的方案。前端埋點(diǎn)系統(tǒng)的前后端通信加密在上報(bào)數(shù)據(jù)的前后端通信中,需要和端協(xié)商加密機(jī)制,利用庫來實(shí)現(xiàn)的加密,已經(jīng)是一個廣泛被采用的加密算法。 在線上項(xiàng)目中,需要統(tǒng)計(jì)產(chǎn)品中用戶行為和使用情況,從而可以從用戶和產(chǎn)品的角度去了解用戶群體,從而升級和迭代產(chǎn)品,使其更加貼近用戶。用戶行為數(shù)據(jù)可以通過前端數(shù)據(jù)監(jiān)控的方式獲得,除此之外,前端還...

    April 評論0 收藏0
  • 前端監(jiān)控和前端埋點(diǎn)方案設(shè)計(jì)

    摘要:異常監(jiān)控包括前端腳本執(zhí)行報(bào)錯等。本文針對整個前端監(jiān)控,設(shè)計(jì)適用的方案。前端埋點(diǎn)系統(tǒng)的前后端通信加密在上報(bào)數(shù)據(jù)的前后端通信中,需要和端協(xié)商加密機(jī)制,利用庫來實(shí)現(xiàn)的加密,已經(jīng)是一個廣泛被采用的加密算法。 在線上項(xiàng)目中,需要統(tǒng)計(jì)產(chǎn)品中用戶行為和使用情況,從而可以從用戶和產(chǎn)品的角度去了解用戶群體,從而升級和迭代產(chǎn)品,使其更加貼近用戶。用戶行為數(shù)據(jù)可以通過前端數(shù)據(jù)監(jiān)控的方式獲得,除此之外,前端還...

    Hwg 評論0 收藏0
  • 前端監(jiān)控?cái)?shù)據(jù)采集更高效

    摘要:如何在新的技術(shù)背景下讓前端數(shù)據(jù)采集工作更加完善高效,是本文討論的重點(diǎn)。具體來說,我們對前端的數(shù)據(jù)采集具體主要分為路由切換性能資源錯誤日志上報(bào)路由切換等前端技術(shù)的快速發(fā)展使單頁面應(yīng)用盛行。 隨著業(yè)務(wù)的快速發(fā)展,我們對生產(chǎn)環(huán)境下的問題感知能力越來越關(guān)注。作為距離用戶最近的一層,前端的表現(xiàn)是否可靠、穩(wěn)定、好用,很大程度上決定著用戶對整個產(chǎn)品的體驗(yàn)和感受。因此,對于前端的監(jiān)控不容忽視。 搭建一...

    Half 評論0 收藏0
  • 在單頁應(yīng)用中,如何優(yōu)雅的上報(bào)前端性能數(shù)據(jù)

    摘要:本文的介紹的是如何設(shè)計(jì)一個通用的可以以較小的侵入性,自動上報(bào)前端的性能數(shù)據(jù)。具體的做法可以看我的上一篇文章在單頁應(yīng)用中,如何優(yōu)雅的監(jiān)聽的變化。三如何上報(bào)性能數(shù)據(jù)那么如何上報(bào)性能數(shù)據(jù)呢,我們第一反應(yīng)就是通過請求的形式來上報(bào)前端性能數(shù)據(jù)。 ??最近在做一個較為通用的前端性能監(jiān)控平臺,區(qū)別于前端異常監(jiān)控,前端的性能監(jiān)控主要需要上報(bào)和展示的是前端的性能數(shù)據(jù),包括首頁渲染時間、每個頁面的白屏?xí)r...

    KitorinZero 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<