Abruzzi's Wall

Front-End Web Developer

Ghost only can help me to here...


Analytics代码延迟异步加载

这两天研究了一些网站统计代码的相关技术,做了一些实验,得到一些有趣的结果。

通常情况下,我们在自己的网站引入统计js脚本时,官方会提供同步/异步两种代码。同步代码暂且不谈,官方提供的异步代码通常如下(google & baidu):

可以看到,多数分析网站的异步脚本都是由document.write写入一个<script>标签,同时依次添加<script>标签的typesrc以及async属性。

Html5<script>标签的async与defer属性

这里的async属性是html5为<script>增加的新属性,标有这个属性的外部脚本,被浏览器解析时会立刻下载该外部脚本同时却不阻塞后续的document渲染、script加载等事件,从而实现外部脚本的异步加载。

此外html5还为<script>元素提供了一个defer属性,defer属性和async作用大体一致,并且都拥有一个附加onload事件,例如:
<script src="" defer onload="init()">
该script脚本一旦下载完成,便会执行标签中的init()函数。
asyncdefer两者唯一的区别在于,async属性会在外部脚本下载完成后无序立即执行defer属性在外部脚本下载完成后,仍然会按照document结构顺序执行该script脚本。

这里是async与defer的详细解释:

Both async and defer scripts begin to download immediately without pausing the parser and both support an optional onload handler to address the common need to perform initialization which depends on the script. The difference between async and defer centers around when the script is executed. Each async script executes at the first opportunity after it is finished downloading and before the window’s load event. This means it’s possible (and likely) that async scripts are not executed in the order in which they occur in the page. The defer scripts, on the other hand, are guaranteed to be executed in the order they occur in the page. That execution starts after parsing is completely finished, but before the document’s DOMContentLoaded event.

不支持async与defer的异步加载script方式

由于浏览器对async、defer实现并不一致,因此通常会采取另一种方式异步加载script脚本:
document.write('<script src="" async></script>');
通过这种方式写入DOM的script标签,也同样能避免对浏览器渲染DOM、加载后续script的阻塞。

上述异步加载script方式的缺陷

上述两种方式使得外部script可以异步加载,但却存有一个缺陷,即加载script完成之前,会阻塞window.onload方法,这也就使得我们写在window.onload()或者$(document).ready()中的函数只有在所有外部script脚本下载完成后才能执行。

通常一些页面初始化的代码都会写在$(document).ready()一类的函数中,而统计代码由于服务或不同地理位置网络的不稳定,很可能造成某个script pending的状态,导致页面写在$(document).ready()中的初始化函数一直无法执行,页面自然也容易bug百出。

在window.onload()后延迟加载script

一番查阅,根据Patrick Sexton的这篇文章deffer loading javascript,可以在window.onload之后append这些统计代码到document
具体代码如下:


//deffer loading js code
function downloadJSAtOnload() {
    var element = document.createElement("script");
    element.src = "defer.js";
    document.body.appendChild(element);
}
if (window.addEventListener)
    window.addEventListener("load", downloadJSAtOnload, false);
else if (window.attachEvent)
    window.attachEvent("onload", downloadJSAtOnload);
else 
    window.onload = downloadJSAtOnload;

于是我便使用这种方式改写了通用的分析代码加载函数:

统计代码获取页面数据原理

在我所使用的4个统计代码里,google、agrant、mediav采用延迟异步加载的方式都运行正常,然而baidu统计却在其分析网站中提示没有正确安装,无法获取数据进行分析,这让我重新考虑了一番统计代码的实现原理:

如上文中google提供的异步JS代码,mediav、agrant、google的统计代码都在window上声明了一些全局对象。用户访问网站时,浏览器执行这些JS脚本,异步写入一个<script>标签到document,而后下载对应的分析代码,进而执行这些分析代码。

这些分析代码通常会向其所在服务器发起一个image/gif图片类型的请求,这个请求在url?后明文后缀了一些参数,比如:

http://www.google-analytics.com/collect?v=1&_v=j23&a=1636541637&t=pageview&…0%20r0&_u=MACAAEQ~&cid=1157723590.1407228383&tid=UA-53535089-1&z=779947508

可以看出,这里后缀的参数,都是由分析代码从之前的window全局变量中取出来的,是那些已经声明在window上的属性(统计ID等),分析网站通过这样的方式确保域名和被分析者一致。

统计服务的校验机制

然而,用同样方式写出来的延迟异步加载baidu统计代码为什么会没有效果呢?从network看,浏览器也确实下载到了baidu统计的h.js代码。

在同事Nos的帮助下,我们查看nginx的access.log,发现当点击baidu分析网站中的‘代码检查’时,我的nginx服务器只收到一条get请求:

nginx log

根据猜测,baidu统计抓回了我的网站html代码,做了静态分析。

原理可能是:在请求baidu的hm.js时,baidu会根据request携带的原始地址逆向访问取回html,分析代码检测这个请求是否来自一个需要百度分析的网站,确保它不是一个带有其他目的的request请求,通过这种校验避免用户别有用心地将baidu统计代码用作它途。

绕过baidu统计静态代码分析

搬回baidu统计自己生成的代码,经过尝试修改发现,baidu统计的静态分析,仅仅是分析有没有baidu统计生成的代码段。

知道baidu检验的是这个问题就好办了,我们将baidu统计生成的代码移入网站,将type设置为test等非mime/type类型,这样浏览器便不会解析这段script,baidu统计却也能校验通过:

通过这种方式骗过浏览器和baidu统计,避免造成window.onload阻塞,从而达到完全的Analytics代码延迟异步加载。