作者前段时间在做类似Google Analytics(以下简称GA)的第三方监控脚本。所以对GA的前端代码做过调研,对GA的压缩后代码做了一定程度上的人肉美化。这里美化的是analytics.js的j41版本,本文提到的小技巧也是基于这个版本的js。
cookie的本质是存储在浏览器端的一段简单数据(多个键值对),浏览器会从服务器接受或者发送给服务器cookie。这样便可以为没有状态的HTTP协议提供了记录状态信息的方法,知道多个不同的HTTP请求是否来自同一个浏览器。
在浏览器中cookie是支持范围最广的存储数据的手段了,前端工程师一般都曾经或多或少的使用过cookie来存储数据或变量。浏览器提供了document.cookie
这个接口来增删改查cookie。但是只能一次设置一个cookie。使用方法如下:
// 设置名为key的cookie,值为value
document.cookie = 'key=value';
代码中省略了其他可选配置,具体使用方法可以参考MDN文档。通过domain
和path
参数,可以将cookie设置在不同的域名或者路径下,例如:
// 设置名为key的cookie,值为value
document.cookie = 'A=1;path=/;domain=map.baidu.com;';
上面的代码在域名map.baidu.com
下的根路径(/
)设置了cookie A的值为1
。我们可以通过document.cookie
来获取当前域名和路径下的所有cookie。当我们访问http://map.baidu.com
页面时,执行document.cookie
获得的结果是A=1
。
大家知道域名是有父域名和子域名的区别的,键名相同的两个cookie可以分别设置在父子域名上。例如如下代码:
document.cookie = 'A=1;path=/;domain=.example.com;';
document.cookie = 'A=2;path=/;domain=a.example.com;';
顺带提到一个冷门的知识点:以
.
开头的域名表示cookie设置在此域名及其子域上,否则不适用于其子域名。不过从RFC 2965标准开始浏览器便会自动为domain属性值自动添加一个.
前缀,所以在设置domain
时加不加.
前缀已经没有区别了。但是为了兼容一些旧浏览还是加.
为好。另外如果不设置domain
浏览器会默认用当前页面的域名,这时浏览器不会自动添加.
前缀,自然也就不会包含子域了。
在.example.com
和a.example.com
域名下面分别设置了A=1
和A=2
两个cookie。此时我们在http://a.example.com
页面下执行document.cookie
得到的结果是:A=1;A=2
。如下图:
开发者是没办法从结果中知道这个cookie是设置在哪个域名上的。同样这个情况也适用于不同的父子路径上。
这在一般情况下对开发者不会有影响,但是对于GA来说确实致命的。GA会在当前网站域名下面设置一个全局唯一的cookie _ga
,用于标志相同的用户。现在大型的公司都会分不同的网站域名,例如:baidu.com
和ditu.baidu.com
。假设百度使用了GA~ 那么GA会分别在两个域名下设置不同的_ga
cookie值,这样在baidu.com
下GA便会拿到两个_ga
值。不知道该用哪个,傻傻分不清楚。
为了解决这个问题,GA在cookie的值上面做文章。可以看到冲突只会发生在父子域名和父子路径上。因为cookie本身的特殊性:所有http请求会带着该域名下的所有cookie。如果cookie值太长太多会消耗太多带宽。GA通过计算域名和路径的“长度”来唯一标示这个cookie所设置在的域名和路径。计算“长度”的方法如下:
/**
* normalize domain.
* remove the first '.' if exist.
* @param {string} domain domain String
* @return {string} normalized domain
*/
var normalizeDomain = function (domain) {
return domain.indexOf('.') === 0 ? domain.substr(1) : domain;
};
/**
* get count of domain.
* getDomainCount('qq.com') === 2
* getDomainCount('.qq.com') === 2
* getDomainCount('b.qq.com') === 3
* getDomainCount('e.qidian.qq.com') === 4
*
* @param {string} domain domain String
* @return {string} normalized domain
*/
var getDomainCount = function (domain) {
return normalizeDomain(domain).split('.').length;
};
/**
* normalize path
* normalizePath('') === '/'
* normalizePath('/') === '/'
* normalizePath('/ping') === '/ping'
* normalizePath('/ping/pv') === '/ping/pv'
* normalizePath('ping/pv') === '/ping/pv'
* normalizePath('ping/pv/') === '/ping/pv'
*
* @param {string} path path
* @return {string} path
*/
var normalizePath = function (path) {
if (!path) {
return '/';
}
if (path.length > 1 && path.lastIndexOf('/') === path.length - 1) {
// remove last '/'
path = path.substr(0, path.length - 1);
}
if (path.indexOf('/') !== 0) {
// fill up first '/'
path = '/' + path;
}
return path;
};
/**
* get count of path
* getPathCount('') === 1
* getPathCount('/') === 1
* getPathCount('/ping') === 2
* getPathCount('/ping/pv') === 3
* getPathCount('ping/pv') === 3
* getPathCount('ping/pv/') === 3
*
* @param {string} path path
* @return {number} count of path
*/
var getPathCount = function (path) {
path = normalizePath(path);
return path === '/' ? 1 : path.split('/').length;
};
/**
* Get domain and path count string.
* domainCount + '-' + pathCount + '$'
*
* @param {Window=} win window context.
* @param {string=} domain cookie domain.
* @param {string=} path cookie path.
* @return {string} count string.
*/
var getDomainAndPathCount = function (win, domain, path) {
win = win || window;
var location = win.location;
var pathCount = getPathCount(path != null ? path : location.pathname);
var domainCount = getDomainCount(domain != null ? domain : location.hostname);
return domainCount + (pathCount > 1 ? '-' + pathCount : '') + '-';
};
GA在设置cookie时加上domain
和path
的长度前缀,然后在取出cookie时遍历所有的名字相同的cookie找到指定域名和路径下的cookie。示例代码如下:
/**
* Set cookie.
* @param {string} key cookie name.
* @param {string|number} value cookie value.
* @param {Window=} win window context.
* @param {number=} expires cookie expired time in milliseconds.
* @param {string=} domain cookie domain.
* @param {string=} path cookie path.
* @return {boolean} success or not.
*/
var setCookie = function (key, value, win, expires, domain, path) {
var domainAndPathCount = getDomainAndPathCount(win, domain, path);
value = value + '';
return setCookieRaw(key, domainAndPathCount + value.replace(/\-/g, '%2d'), win, expires, domain, path);
};
/**
* Get cookie value.
*
* @param {string} key key name.
* @param {Window=} win window context.
* @param {string=} domain cookie domain.
* @param {string=} path cookie path.
* @return {string} cookie value.
*/
var getCookie = function (key, win, domain, path) {
var results = getCookieRaw(key, win);
var domainAndPathCount = getDomainAndPathCount(win, domain, path);
for (var i = 0; i < results.length; i++) {
var r = results[i];
if (r.indexOf(domainAndPathCount) === 0) {
return r.slice(domainAndPathCount.length).replace(/%2d/g, '-');
}
}
return '';
};
魔鬼?藏于细节。GA对细节的追求并没有止步于此。cookie名_ga
虽然一般很少有开发者会用到,但是不怕一万就怕万一。如果cookie名_ga
冲突了并被改写成了别的值,那么GA很可能会发送一个不符合要求的值过去。这对服务器端解析也非常不利。
GA在cookie的值之前又加了前缀GA1.
,下次使用这个cookie时都会检查是否带有GA1.
前缀。如果不存在前缀则直接覆盖生成新的,如果存在则继续复用原cookie值。
除了防止_ga
被开发者覆盖之外,猜想还有一个作用,就是“版本号”。多个GA版本之间如果_ga
的cookie值生成算法不一样需要特殊处理,那么可以依据GA1.
前缀来判断应该是哪个版本,采取正确的操作。
总结下_ga
cookie值的格式如下:
// GA{version}.{domainCount}[-{pathCount}].{randomNumber}.{time}
// path是'/'时
document.cookie = '_ga=GA1.3.494346849.1446193077';
// 没有path时
document.cookie = '_ga=GA1.3-2.494346849.1446193077';
细微之处见真章:+1:
参考文档:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有