封面图
下午在楼下坐着想了一些事情,觉得还是得稍微赶赶进度。
前端监控系统从前端的角度来说其实核心有三点,监控错误,监控性能,监控用户行为。
那么我们首先要知道有哪些错误,哪些错误指标,哪些用行为。这些问题在之前的文章或者视频里都有提到,这里就不再赘述了,这里主要讲下实现各种功能的方法。
页面导航处理
对页面导航进行处理,是因为在监控系统中我们需要知道当前页面的上一个页面是什么,我们需要对页面以及接口,甚至用户的行为,从哪个页面跳转到哪个页面有一个清晰的记录,所以我们需要对页面导航进行处理。
而在浏览器中,导航Api主要只得是history相关的api。
pushState()方法
pushState() 需要三个参数:一个状态对象,一个标题 (目前被忽略), 和 (可选的) 一个 URL. 让我们来解释下这三个参数详细内容:
- 状态对象 — 状态对象 state 是一个 JavaScript 对象,通过 pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate 事件就会被触发,且该事件的 state 属性包含该历史记录条目状态对象的副本。 状态对象可以是能被序列化的任何东西。原因在于 Firefox 将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有 640k 的大小限制。如果你给 pushState() 方法传了一个序列化后大于 640k 的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.
- 标题 — Firefox 目前忽略这个参数,但未来可能会用到。在此处传一个空字符串应该可以安全的防范未来这个方法的更改。或者,你可以为跳转的 state 传递一个短标题。
- URL — 该参数定义了新的历史 URL 记录。注意,调用 pushState() 后浏览器并不会立即加载这个 URL,但可能会在稍后某些情况下加载这个 URL,比如在用户重新打开浏览器时。新 URL 不必须为绝对路径。如果新 URL 是相对路径,那么它将被作为相对于当前 URL 处理。新 URL 必须与当前 URL 同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前 URL。
replaceState()方法
history.replaceState() 的使用与 history.pushState() 非常相似,区别在于 replaceState() 是修改了当前的历史记录项而不是新建一个。注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。
replaceState() 的使用场景在于为了响应用户操作,你想要更新状态对象 state 或者当前历史记录的 URL。
所以如果我们想要获取当前页面的前一个页面的地址,我们就需要对这两个方法进行妥善的处理。
页面停留时长的处理
页面停留时长的计算简单来讲就是页面隐藏时间时的时间减去页面显示时的时间点。如果用公式来表示的话,如下所示:
页面停留时长(durationTime) = 页面隐藏时的时间戳 - 页面显示时的时间戳。
而在web Api中,能够统计到这个信息的有这么几个API事件:
pageShow
当一条会话历史记录被执行的时候将会触发页面显示 (pageshow) 事件。(这包括了后退/前进按钮操作,同时也会在 onload 事件触发后初始化页面时触发)。
以下示例将会在控制台打印由前进/后退按钮以及 load 事件触发后引起的 pageshow 事件
window.addEventListener('pageshow', function(event) {
console.log('after , pageshow :',event);
});
window.addEventListener('load', function() {
console.log('before');
});
pagehide
当浏览器在显示与会话历史记录不同的页面的过程中隐藏当前页面时,pagehide(页面隐藏) 事件会被发送到一个Window 。例如,当用户单击浏览器的“后退”按钮时,当前页面在显示上一页之前会收到一个pagehide(页面隐藏) 事件。
在此示例中,建立了一个事件处理程序以监视 pagehide (页面隐藏) 事件,并在持久保存页面以进行可能的重用时执行特殊处理。
window.addEventListener("pagehide", event => {
if (event.persisted) {
/* the page isn't being discarded, so it can be reused later */
}
}, false);
这也可以使用 Window 上的 onpagehide 事件处理程序属性来编写
window.onpagehide = event => {
if (event.persisted) {
/* the page isn't being discarded, so it can be reused later */
}
}
visibilitychange
当其选项卡的内容变得可见或被隐藏时,会在文档上触发 visibilitychange (能见度更改) 事件。
该事件不包括文档的更新的可见性状态,但是您可以从文档的 visibilityState 属性中获取该信息。
本示例在文档可见时开始播放音乐曲目,在文档不再可见时暂停音乐。
document.addEventListener("visibilitychange", function() {
console.log( document.visibilityState );
});
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === 'visible') {
backgroundMusic.play();
} else {
backgroundMusic.pause();
}
});
通过对这三个事件进行监听,结合pushState() 以及replaceState() 这两个导航方法,我们可以获取到至少一下几个信息:
- 当前页面的URL
- 前一个页面的URL
- 页面停留时长(durationTime)
- 其他信息
点击事件的处理
在一个完善的前端监控系统中,我们有时候需要知道用户点击了哪些模块、那些元素,用来统计项目中某些模块的使用频率,这时候需要我们对用户的点击行为做一个详细的记录。
那么如何实现对点击事件及元素的监听呢?其实原理很简单,主要还是需要用到事件代理,讲点击事件代理到全局,然后通过事件中的event获取到对应的元素以及点击事件的参数。如果我们需要更多的信息,比如当前页面地址等,则可以通过相应的API获取到,然后通过日志收集方法进行收集。
document.addEventListener('click',(event)=>{
const {target} = event
// 获取元素信息等
let ele = getCurrentEle(target)
//...
})
当然,我这里只是这么写一些,具体到监控系统中,我们需要做更多的处。
元素曝光时长的计算
在监控系统中,我们有时候需要对某一区域、或者某一模块的曝光时长进行统计,尤其是我们使用模块化开发之后,有时候产品会要求我们统计某个模块的曝光时长,用来跟实际的业务数据做一个比较,那么这时候就需要我们能对这一区域的曝光时长做一个统计。
那么如何统计某个区域的曝光时长呢?其实也很简单,如果我们仔细思考一下,曝光时长其实就是指某一区域进入可是区域后,直到页面隐藏,或者这一区域从可视区域内消失的 这一段时间,如果用公式表示,则如下所示:
元素曝光时长 = 元素隐藏时的时间戳 - 元素进入可视区域时的时间戳。
根据这个公式,我们可以从两个方面入手,一方面在元素进入可视区域时记录当前的时间戳,另一方面,当元素超出可视区域或页面隐藏时记录时间戳,然后我们就可以获得对应元素的曝光时长。
那么如何判断元素已经进入了可视区域呢?网上有很多demo,比如:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Document</title>
<style>
span {
display: block;
width: 200px;
height: 200px;
background-color: pink;
margin: 20px;
box-sizing: border-box;
}
#target {
background-color: brown;
}
</style>
</head>
<body>
<div>
<h3>getBoundingClientRect 得到的 top bottom left right 均是以视窗左上角(0,0)坐标为参照点</h3>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span id="target"></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<script>
const target = document.getElementById('target')
const clientHight = document.documentElement.clientHeight
console.log('视窗高度', clientHight);
window.addEventListener('scroll', () => {
const DOMRectInfo = target.getBoundingClientRect()
// console.log(DOMRectInfo);
if (DOMRectInfo.top <= clientHight && DOMRectInfo.top > clientHight - DOMRectInfo.height) {
console.log('目标元素进入可视区');
} else if (DOMRectInfo.top <= 0 && DOMRectInfo.top > -DOMRectInfo.height) {
console.log('目标元素正在离开可视区');
}
if (DOMRectInfo.bottom <= clientHight && DOMRectInfo.bottom > DOMRectInfo.height) {
console.log('目标元素完全进入可视区');
} else if (DOMRectInfo.bottom <= 0) {
console.log('目标元素完全离开可视区');
}
})
</script>
</body>
</html>
当然,这只是其中的一种方法,我们也可以使用其他方法来实现这个功能,比如使用元素进入可视区域的面积占该元素面积的百分比,当进入可视区域面积占比达于一定比例时,开始统计曝光时长。
页面加载性能的计算
关于性能方面,一直是一个值得讨论的话题。但是在我看来,假如在面试中直接问:你对性能优化有什么了解的内容吗?
我觉得问这个问题的意义不大。因为在一般的项目中,如果我们的项目复杂度不高,对性能的要求也不高,那么项目本省就没什么性能的问题。反之,如果我们的项目对性能的要求较高,比如要求页面的加载速度要快,函数的计算速度要快,页面占用的内存要少等等,那么就需要我们针对特定的问题,去分析解决响应的难点。
在前端开发中,假如我们觉得页面的加载速度比较慢。我们就需要以合适的方法来统计到页面加载的时长,这里主要是需要用到 performance 相关的API。
Performance 接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。
从performance的timing属性中,我们可以得到以下信息:
- connectEnd 代表了网络链接建立的时间节点。
- connectStart 请求连接被发送到网络之时的 Unix 毫秒时间戳。
- domainLookupEnd 解析域名结束时的 Unix 毫秒时间戳。
- domainLookupStart 域名开始解析之时的 Unix 毫秒时间戳。
- domComplete 主文档的解析器结束工作,Document.readyState 变为 'complete'且相当于 readystatechange 事件被触发时的 Unix 毫秒时间戳。
- domInteractive 主文档的解析器结束工作,即 Document.readyState 改变为 'interactive' 并且相当于 readystatechange 事件被触发之时的 Unix 毫秒时间戳。
- loadEventStart 为 load 事件被现在的文档触发之时的 Unix 时间戳。如果这个事件没有被触发,则返回 0。
- loadEventEnd 为 load 事件处理程序被终止,加载事件已经完成之时的 Unix 毫秒时间戳。如果这个事件没有被触发,或者没能完成,则该值将会返回 0。
- requestStart 浏览器发送从服务器或者缓存获取实际文档的请求之时的 Unix 毫秒时间戳。
- responseEnd 浏览器从服务器、缓存或者本地资源接收响应的最后一个字节或者连接被关闭之时的 Unix 毫秒时间戳。
等属性。
从以上属性中我们可以计算出相关的页面性能,比如:
- 网络连接时间
- dns解析时间
- 页面加载时间
- 页面白屏时间
- 请求占用时间
- dom解析时间
- ...
我们只要用相应的属性做个减法,就可以得到上面所列的各种情况所消耗的时间。
页面pv的统计方法
页面访问量,即PageView,用户每次对网站的访问均被记录,用户对同一页面的多次访问,访问量累计,用户每次刷新即被计算一次。
那么这样看来,计算页面pv的方法就很简单了。我们可以通过页面的显示或隐藏,或者通过页面的load 和 unload ,甚至可以通过页面的路由跳转来计算PageView的数值。
当然,我们也可以将数据存储下来,通过数据库的查询进行分组查询,然后统计出各个页面的访问量。
信息收集方案
知道了这些统计计算方法之后,我们就可以相应的收集到我们想要监控的信息,根据这些信息做各种各样的统计,得出我们想要的结果。
假设我们已经写好了相关的计算方法,那么我们应该如何收集这些信息呢?把这些信息放在什么地方呢?
其实,如果你熟悉vue的响应式原理,那么我们很快就能找到解决方案。我们可以定义一个类似的依赖收集系统,当监听到相关的数据时,我们就将数据push到我们的依赖中,然后当我们需要上报的时候只要出发依赖的notify方法即可。
其实就是个发布订阅。
所以其原理就是:
第一、 从各种计算方案中获取数据
第二、将数据添加到依赖中
第三、当需要上报数据的时候变量依赖中的数据进行上报。
仅此而已。
当然,这仅仅只是前端需要做的一些前置工作,能够上报数据之后,我们需要考虑到涉及后端的一些内容,比如:数据如何存储,如何查询等等。
甚至对于前端的功能来讲,我们可能还需要考虑一些离线缓存日志,然后当网络畅通的时候在进行上报等等。