通过实例化img对象进行打点统计

  每次到了周末都有写点什么的冲动。这次我们来聊聊前端的统计问题。

  最早的需求来自公司市场部门,要统计广告位的点击,作为广告素材优化的决策参考。

问题背景

  1. 统计要能跨域,因为统计逻辑可能单独部署;
  2. 没有严重的安全性问题,且对业务逻辑无侵入;
  3. 实现方式有较好的浏览器兼容性,并且尽量减少丢失;

思考过程

  要跨域,其实可以后端直接设置响应头来CORS,但是此法要劳烦后端同学改服务器配置,不方便后期服务器的调整,而且IE8以下不支持CORS方式跨域。会损失一部分统计。

  所以又想到JSONP,由于其具体实现是历史遗留问题,兼容性没毛病,支持到史前浏览器,看似很美好。

  但是想到第二条要求,我就有点虚了。万一我们的统计服务器DNS被劫持,攻击者将返回的JSONP变成恶意代码,就会对页面内容进行篡改;其次,我们的统计服务器目前没走HTTPS,HTTP包一旦被截取,在其中加点料,可能就很崩(我就不点名批评ISP们了)。

  于是乎就开始想起了CNZZ,这么多站点使用的统计,应该没毛病吧。查了下原理,原来是利用img标签来get一个很小的GIF图片。跟JSONP异曲同工,也是利用了src没有跨域限制的漏洞。这个方法避免了安全问题,因为img标签是妥妥不会执行的,这个img又是由我们实例化的,其加载相关事件由我们显式指定,所以也不存在onerror的XSS问题。

  我心想,妥了。

具体实现

不废话,上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var sendStatistic = function(url) {
// 先new一个Image对象,并且生成一个随机KEY,用于存储该对象到全局变量,防止JS垃圾回收机制,在进行较重的DOM操作时,将这个没有被二次引用的对象销毁,从而导致统计量丢失
var img = new Image(),
// 这里是生成了一个36位(0~9,A~Z,10+26=36)随机数,
// 32bit最大正整数为214783647,这里的取值范围是0到该最大值
// 目的是尽量减少KEY的重复
key = 'Baizhu_FED_log_' + Math.floor(Math.random() *
2147483648).toString(36);
window[key] = img;
// 显式指定img加载相关的事件
img.onload = img.onerror = img.onabort = function() {
// img标签加载完成、错误或终止时,解除事件绑定,销毁相关对象
img.onload = img.onerror = img.onabort = null;
window[key] = null;
img = null;
};
// 指定img的src属性,触发加载
img.src = url;
};

  用上面的代码,我们就可以轻松发起统计请求了,但是要注意,由于浏览器缓存机制,同样src的img对象,可能不会发起第二次请求,保险起见,最好每次请求都带个不一样的时间戳,这个根据业务场景各有不同,所以就不在这里单独写了。