专栏首页前端小作坊GA源代码里的小技巧之cookie篇

GA源代码里的小技巧之cookie篇

GA源代码里的小技巧之cookie篇

作者前段时间在做类似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文档。通过domainpath参数,可以将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.coma.example.com域名下面分别设置了A=1A=2两个cookie。此时我们在http://a.example.com页面下执行document.cookie得到的结果是:A=1;A=2。如下图:

开发者是没办法从结果中知道这个cookie是设置在哪个域名上的。同样这个情况也适用于不同的父子路径上。

这在一般情况下对开发者不会有影响,但是对于GA来说确实致命的。GA会在当前网站域名下面设置一个全局唯一的cookie _ga,用于标志相同的用户。现在大型的公司都会分不同的网站域名,例如:baidu.comditu.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时加上domainpath的长度前缀,然后在取出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:

参考文档:

  1. RFC 2109
  2. RFC 2965
  3. RFC 6265
  4. What does the dot prefix in the cookie domain mean?
  5. MDN document.cookie

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Web Animations的命名简化

    最早支持Web Animation的浏览器是Chrome 36,在Chrome 39中又更新了对播放的控制。在Javascript中我们可以调用Element....

    mmzhou
  • 原型链上的DOM Attributes

    Chrome开发小组最近发表声明他们正在將DOM properties移动到原型链中。这个更新将会在Chrome 43(2015年4月发布beta版本)中实现。...

    mmzhou
  • CSS3着重符及其fallback

    在中文里面,我们一般会在文字下方加上圆形符号。在日语中会在文字上方加上小顿号。在CSS3中如下属性可以控制着重符号:

    mmzhou
  • 14.Django基础之jQuery操作cookie

      定义:让网站服务器把少量数据储存到客户端的硬盘或内存,从客户端的硬盘读取数据的一种技术;

    changxin7
  • cookie面面观

    在前端面试中,有一个必问的问题:请你谈谈cookie和localStorage有什么区别啊?

    前端林子
  • 操作cookie信息

    说道cookie,我们都知道他是存储在浏览器客户端的一种数据存储方式,避免了大量与服务器进行数据交互造成的延迟效果,使页面浏览起来很流畅,但是不建议大量的使用c...

    无邪Z
  • 登录之记住用户与自动登录

           如题,大家在使用各种网站时,为了更好的用户体验,网站往往会提供这两种功能之一,以便下次登录方便。 ? ?        今天要讲述的是用java操...

    高爽
  • JQuery之cookie增删改查操作

    在Java Web开发中cookie一般有两种新建方法,一种是在Java中创建维护,另一种是在前端中创建和维护。 二者之间最大的区别就是:Java中的cooki...

    林老师带你学编程
  • JS 中 cookie 的使用

    1、cookie 是什么?   ①、cookie 是存储于访问者计算机中的变量。每当一台计算机通过浏览器来访问某个页面时,那么就可以通过 JavaScript ...

    IT可乐
  • 一文看懂Cookie奥秘

    Cookie是什么?cookies是你访问网站时创建的数据片段文件,通过保存浏览信息,它们使你的在线体验更加轻松。 使用cookies,可以使你保持在线登录状态...

    小码甲

扫码关注云+社区

领取腾讯云代金券