怎样定位前端线上问题,一直以来,都是很头疼的问题,由于它发生于用户的一系列操做以后。错误的缘由可能源于机型,网络环境,接口请求,复杂的操做行为等等,在咱们想要去解决的时候很难复现出来,天然也就没法解决。 固然,这些问题并不是不能克服,让咱们来一块儿看看如何去监控并定位线上的问题吧。 javascript
背景:市面上的前端监控系统有不少,功能齐全,种类繁多,无论你用或是不用,它都在那里,密密麻麻。每每我须要的功能都在别人家的监控系统里,手动无奈,罢了,怎么才能拥有一个私人定制的前端监控系统呢?作一个自带前端监控系统的前端工程狮是一种怎样的体验呢?html
这是搭建前端监控系统的第四章,主要是介绍如何统计静态资源加载报错,跟着我一步步作,你也能搭建出一个属于本身的前端监控系统。前端
若是感受有帮助,或者有兴趣,请关注 or Star Me 。 java
请移步线上: 前端监控系统jquery
上一章介绍了如何统计静态资源加载报错,今天要说的是前端接口请求监控的问题。git
可能有人会认为接口的报错应该由后台来关注,统计,并修复。 确实如此,并且后台服务有了不少成熟完善的统计工具,彻底可以应对大部分的异常状况, 那么为何还须要前端对接口请求进行监控呢。缘由很简单,由于前端是bug的第一发现位置,在你帮后台背锅以前怎么快速把过甩出去呢,这时候,咱们就须要有一个接口的监控系统,哈哈 :)那么,咱们须要哪些监控数据才可以把锅甩出去呢?github
1. 咱们要监控全部的接口请求web
2. 咱们要监控并记录全部接口请求的返回状态和返回结果ajax
3. 咱们要监控接口的报错状况,及时定位线上问题产生的缘由数组
4. 咱们要分析接口的性能,以辅助咱们对前端应用的优化。
好了, 进入正题吧:
如何监控前端接口请求呢
通常前端请求都是用jquery的ajax请求,也有用fetch请求的,以及前端框架本身封装的请求等等。总之他们封装的方法各不相同,可是万变不离其宗,他们都是对浏览器的这个对象 window.XMLHttpRequest 进行了封装,因此咱们只要可以监听到这个对象的一些事件,就可以把请求的信息分离出来。
若是你用的jquery、zepto、或者本身封装的ajax方法,就能够用以下的方法进行监听。咱们监听 XMLHttpRequest 对象的两个事件 loadstart, loadend。可是监听的结果并非像咱们想象的那么容易理解,咱们先看下ajaxLoadStart,ajaxLoadEnd的回调方法。
/** * 页面接口请求监控 */ function recordHttpLog() { // 监听ajax的状态 function ajaxEventTrigger(event) { var ajaxEvent = new CustomEvent(event, { detail: this }); window.dispatchEvent(ajaxEvent); } var oldXHR = window.XMLHttpRequest; function newXHR() { var realXHR = new oldXHR(); realXHR.addEventListener('loadstart', function () { ajaxEventTrigger.call(this, 'ajaxLoadStart'); }, false); realXHR.addEventListener('loadend', function () { ajaxEventTrigger.call(this, 'ajaxLoadEnd'); }, false); // 此处的捕获的异常会连日志接口也一块儿捕获,若是日志上报接口异常了,就会致使死循环了。 // realXHR.onerror = function () { // siftAndMakeUpMessage("Uncaught FetchError: Failed to ajax", WEB_LOCATION, 0, 0, {}); // } return realXHR; } function handleHttpResult(i, tempResponseText) { if (!timeRecordArray[i] || timeRecordArray[i].uploadFlag === true) { return; } var responseText = ""; try { responseText = tempResponseText ? JSON.stringify(utils.encryptObj(JSON.parse(tempResponseText))) : ""; } catch (e) { responseText = ""; } var simpleUrl = timeRecordArray[i].simpleUrl; var currentTime = new Date().getTime(); var url = timeRecordArray[i].event.detail.responseURL; var status = timeRecordArray[i].event.detail.status; var statusText = timeRecordArray[i].event.detail.statusText; var loadTime = currentTime - timeRecordArray[i].timeStamp; if (!url || url.indexOf(HTTP_UPLOAD_LOG_API) != -1) return; var httpLogInfoStart = new HttpLogInfo(HTTP_LOG, simpleUrl, url, status, statusText, "发起请求", "", timeRecordArray[i].timeStamp, 0); httpLogInfoStart.handleLogInfo(HTTP_LOG, httpLogInfoStart); var httpLogInfoEnd = new HttpLogInfo(HTTP_LOG, simpleUrl, url, status, statusText, "请求返回", responseText, currentTime, loadTime); httpLogInfoEnd.handleLogInfo(HTTP_LOG, httpLogInfoEnd); // 当前请求成功后就,就将该对象的uploadFlag设置为true, 表明已经上传了 timeRecordArray[i].uploadFlag = true; } var timeRecordArray = []; window.XMLHttpRequest = newXHR; window.addEventListener('ajaxLoadStart', function(e) { var tempObj = { timeStamp: new Date().getTime(), event: e, simpleUrl: window.location.href.split('?')[0].replace('#', ''), uploadFlag: false, } timeRecordArray.push(tempObj) }); window.addEventListener('ajaxLoadEnd', function() { for (var i = 0; i < timeRecordArray.length; i ++) { // uploadFlag == true 表明这个请求已经被上传过了 if (timeRecordArray[i].uploadFlag === true) continue; if (timeRecordArray[i].event.detail.status > 0) { var rType = (timeRecordArray[i].event.detail.responseType + "").toLowerCase() if (rType === "blob") { (function(index) { var reader = new FileReader(); reader.onload = function() { var responseText = reader.result;//内容就在这里 handleHttpResult(index, responseText); } try { reader.readAsText(timeRecordArray[i].event.detail.response, 'utf-8'); } catch (e) { handleHttpResult(index, timeRecordArray[i].event.detail.response + ""); } })(i); } else { var responseText = timeRecordArray[i].event.detail.responseText; handleHttpResult(i, responseText); } } } }); }
一个页面上会有不少个请求,当一个页面发出多个请求的时候,ajaxLoadStart事件被监听到,可是却没法区分出来到底发送的是哪一个请求,只返回了一个内容超多的事件对象,并且事件对象的内容几乎彻底同样。当ajaxLoadEnd事件被监听到的时候,也会返回一个内容超多的时间对象,这个时候事件对象里包含了接口请求的全部信息。幸运的是,两个对象是同一个引用,也就意味着,ajaxLoadStart和ajaxLoadEnd事件被捕获的时候,他们做用的是用一个对象。那咱们就有办法分析出来了。
当ajaxLoadStart事件发生的时候,咱们将回调方法中的事件对象全都放进数组timeRecordArray里,当ajaxLoadEnd发生的时候,咱们就去遍历这个数据,遇到又返回结果的事件对象,说明接口请求已经完成,记录下来,并从数组中将该事件对象的uploadFlag属性设置为true, 表明请求已经被记录。这样咱们就可以逐一分析出接口请求的内容了。
2.如何监听fetch请求
经过第一种方法,已经可以监听到大部分的ajax请求了。然而,使用fetch请求的人愈来愈多,由于fetch的链式调用可让咱们摆脱ajax的嵌套地狱,被更多的人所青睐。奇怪的是,我用第一种方式,却没法监听到fetch的请求事件,这是为何呢?
return new Promise(function(resolve, reject) { var request = new Request(input, init) var xhr = new XMLHttpRequest() xhr.onload = function() { var options = { status: xhr.status, statusText: xhr.statusText, headers: parseHeaders(xhr.getAllResponseHeaders() || '') } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') var body = 'response' in xhr ? xhr.response : xhr.responseText resolve(new Response(body, options)) } // ....... xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) })
这个是fetch的一段源码, 能够看到,它建立了一个Promise, 并新建了一个XMLHttpRequest对象 var xhr =newXMLHttpRequest()。因为fetch的代码是内置在浏览器中的,它必然先用监控代码执行,因此,咱们在添加监听事件的时候,是没法监听fetch里边的XMLHttpRequest对象的。怎么办呢,咱们须要重写一下fetch的代码。只要在监控代码执行以后,咱们重写一下fetch,就能够正常监听使用fetch方式发送的请求了。就这么简单 :)
看一下须要监听的字段:
// 设置日志对象类的通用属性 function setCommonProperty() { this.happenTime = new Date().getTime(); // 日志发生时间 this.webMonitorId = WEB_MONITOR_ID; // 用于区分应用的惟一标识(一个项目对应一个) this.simpleUrl = window.location.href.split('?')[0].replace('#', ''); // 页面的url this.completeUrl = utils.b64EncodeUnicode(encodeURIComponent(window.location.href)); // 页面的完整url this.customerKey = utils.getCustomerKey(); // 用于区分用户,所对应惟一的标识,清理本地数据后失效, // 用户自定义信息, 由开发者主动传入, 便于对线上问题进行准肯定位 var wmUserInfo = localStorage.wmUserInfo ? JSON.parse(localStorage.wmUserInfo) : ""; this.userId = utils.b64EncodeUnicode(wmUserInfo.userId || ""); this.firstUserParam = utils.b64EncodeUnicode(wmUserInfo.firstUserParam || ""); this.secondUserParam = utils.b64EncodeUnicode(wmUserInfo.secondUserParam || ""); } // 接口请求日志,继承于日志基类MonitorBaseInfo function HttpLogInfo(uploadType, url, status, statusText, statusResult, currentTime, loadTime) { setCommonProperty.apply(this); this.uploadType = uploadType; // 上传类型 this.httpUrl = utils.b64EncodeUnicode(encodeURIComponent(url)); // 请求地址 this.status = status; // 接口状态 this.statusText = statusText; // 状态描述 this.statusResult = statusResult; // 区分发起和返回状态 this.happenTime = currentTime; // 客户端发送时间 this.loadTime = loadTime; // 接口请求耗时 }
全部工做准备完毕,若是把收集到的日志从不一样的维度展示出来,我就不细说了,直接上图了。如此,便可以对前端接口报错的状况有一个清晰的了解,也可以快速的发现线上的问题。