前端性能与异常上报

概述

对于后台开发来讲,记录日志是一种很是常见的开发习惯,一般咱们会使用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时的影响。

相关文章
相关标签/搜索