all articles

learn_from facebook pixel js code

2017-01-06 @sunderls

facebook pixel js

想必大家都用过Google Analytics用来统计pv,其中的基本原理就是设置一个cookie来标记身份,然后把需要统计的信息通过一个1px的beacon image的参数的方式传回google进行分析。啊,今天说的不是这个,而是Facebook Pixel,如果大家碰到GFW没法访问facebook也没关系,今天分享的是从pixel的js代码中学习到的东西,代码片段已经分享到了github

1. 什么是Facebook Pixel

Facebook Pixel是个针对网站主的工具,在自己的网站上添加pixel的代码过后,pixel会检测当前访问者的facebook账户状态并进行标记,网站主可以在facebook针对性的对这些人投放广告。简单整理逻辑如下:

  1. 用户A浏览器访问facebook.com进入登陆状态后, 访问了已经添加pixel的网站siteA,
  2. pixel会发送和facebook.com相同域名的beacon请求(一个1px 的gif),由于facebook.com相同域名,所以facebook可以通过cookie检测当前用户的facebook账号。
  3. 网站siteA的管理员,可以在facebook广告中心定投广告给用户A的facebook账号,虽然他完全不知道用户A是谁。

2. pixel 的embed代码

网站主使用的pixel代码在这里: https://gist.github.com/sunderls/dfd5293a8b8f24a4ef37189a1d8c1b46#file-usage-js


!function(f,b,e,v,n,t,s){
  if(f.fbq)
    return;

  n=f.fbq=function(){
    n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)
  };

  // _fbq is a flag to determine inited or not
  if(!f._fbq) f._fbq=n;

  // fbq.push is itself
  n.push=n;
  n.loaded=!0;
  n.version='2.0';

  // fbq.queue
  n.queue=[];
  t=b.createElement(e);
  t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');

fbq('init', '1111111111111111', {
    em: 'someone@example.com'
});
fbq('track', 'PageView');
fbq('track', 'Purchase', {currency: 'EUR', value: 15.20});
fbq('trackCustom', 'MyCustomEvent', {custom_param: 'custom_value'});

可以看出和Google Analytics一样,动态加载了一个fbevents.js。 这里没有什么特别的,直接进入fbevents.js

3. pixel的核心代码 fbevents.js

facebook提供的是uglify之后的代码,根本没法阅读,我花了些时间进行了加工,添加了有意义的变量名得到了一个基本可以阅读的版本: https://gist.github.com/sunderls/dfd5293a8b8f24a4ef37189a1d8c1b46#file-fbevents-js

具体内容就不细说了,说说值得分享的地方。

3.1 判断是否是Array

function isArray(data) {
    return Array.isArray ? Array.isArray(data) : Object.prototype.toString.call(data) === '[object Array]';
}

github

我想到的是return data instanceof Array,网上查了一下发现,这种方法不太严密,比如window.open()得到的窗口中,window.opener.data instanceof Array的时候,无论data是不是array始终得到false,因为子窗口中的Array和父窗口中的Array不是一回事。

总之,学习到了。

3.2 判断是否是 empty object {}

var isEmpty = function ua(data) {
    if (Object.keys)
        return Object.keys(data).length === 0;
    for (var wa in data)
        if (data.hasOwnProperty(wa))
            return false;
    return true;
};

github

也是一个常用的判断,JSON.stringify(data) === '{}' 貌似也可行,但是速度上不不给力,如果data很大的话会浪费时间。另外,网上查了一下,上述方法实际上不太严谨 isEmpty(new Date)会返回true,因为Date对象是没有property的,所以需要修改为:

if (Object.keys)
        return Object.keys(data).length === 0 && data.constructor === Object

虽然不严谨也没啥大问题。w

3.3 get url太长的时候fallback为post

// if length is < 2048, then use get
    // else use post with a iframe
    if (2048 > (gifPath + '?' + ya).length) {
        submitByGet(gifPath, ya);
    } else
        submitByPost(gifPath, xa);
}

// use gif beacon to collect data
function submitByGet(path, queryString) {
    var gif = new Image();
    gif.src = path + '?' + queryString;
}

// create a form & hidden iframe to post
function submitByPost(path, query) {
    var iframeName = 'fb' + Math.random().toString().replace('.', '')
      , $form = b.createElement('form');
    $form.method = 'post';
    $form.action = path;
    $form.target = iframeName;
    $form.acceptCharset = 'utf-8';
    $form.style.display = 'none';
    var isIE = !!(a.attachEvent && !a.addEventListener)
      , tag = isIE ? '<iframe name="' + iframeName + '">' : 'iframe'
      , $iframe = doc.createElement(tag);
    $iframe.src = 'javascript:false';
    $iframe.id = iframeName;
    $iframe.name = iframeName;
    $form.appendChild($iframe);
    util.listenOnce($iframe, 'load', function() {
        query.each(function(key, value) {
            var $input = b.createElement('input');
            $input.name = key;
            $input.value = value;
            $form.appendChild($input);
        });
        util.listenOnce($iframe, 'load', function() {
            $form.parentNode.removeChild($form);
        });
        $form.submit();
    });
    doc.body.appendChild($form);
}

github

嗯,浏览器不支持太长的url,这个自己的工作中也用到这个,不过我的阈值是2000。总之差别不大,在用post的时候,需要创建一个隐藏的iframe和一个隐藏的<form>,form的target需要设置为iframe的name,这样post后就不会出现默认的页面跳转问题。 这个技巧在IE9 文件上传的文章中也有提到。

3.4 facebook未登录的情况,

pixel假设用户登录了facebook,如果用户没有登录呢? pixel提供了advance matching的功能。流程如下:

  1. 用户A注册了网站A,网站A知道用户A的email,姓名,电话等信息
  2. 网站A在pixel代码中传入这些信息,比如:
    fbq('init', '1111111111111111', {
        em: 'someone@example.com'
    });
  3. pixel遇到这种配置时,会额外加载hash插件,然后hash邮箱,姓名等信息,传回facebook
  4. facebook 会根据hash匹配到自己用户库中的信息来判断facebook用户是谁,即使他/她没有登录。bingo!

facebook简直想钱想疯了呀!!! 不过上述做法倒是很聪明,值得学习。

3.5 SPA环境的pageview

SPA(单页面应用)的话,页面是不会reload的,所以pageview会出现漏记,pixel通过覆盖history的state相关api,达到即使是spa网站也不需要做特殊对应的效果,具体代码在此:

function injectMethod(target, methodName, method) {
    var originMethod = target[methodName];
    target[methodName] = function() {
        var result = originMethod.apply(this, arguments);
        method.apply(this, arguments);
        return result;
    }
    ;
}

var va = function wa() {
    oldHref = currentHref;
    currentHref = location.href;
    if (currentHref === oldHref)
        return;

    var xa = new Clone({
        allowDuplicatePageViews: true
    });

    callMethod.call(xa, 'trackCustom', 'PageView');
};
// when pushState
util.injectMethod(history, 'pushState', va);

//  when replaceState
util.injectMethod(history, 'replaceState', va);

// when popup state
win.addEventListener('popstate', va, false);

github

可以看到,pixel在pushState, replaceState中inject了pageview的统计。

3.6 iframe环境下的referer问题

查看pixel传回服务器中的参数时,发现其中包含了isIframe的flag:


var isIframe = win.top !== win;
//...
query.append('rl', referrer);
query.append('if', isIframe);

虽然不确定,这个flag估计是根referer有关,搜索了一下,iframe环境下的referer将会是iframe自身(根浏览器也有关系)。

4. 总结

以上内容不知道有没有用~ 感谢你的阅读。 pixel 的js代码不长,有兴趣的同学可以在这里继续查看。