04月27, 2018

埋点jssdk的一些笔记

这阵子做了一个埋点的jssdk,在此记录一下。

背景

各个事业部Web网页端埋点数据格式没有统一的标准规范,所以需要做一个统一的标准出来。

不同部门所使用的技术栈不同,比如我所在部门用react,有些部门用vue,或者其他的,因此这个jssdk必须是用原生来写。

实现思路

在正式写代码之前,我阅读了一些文章,还是比较不错的:

实现是比较容易的,就是规范好data,然后组装data,将这个data发送给后端。(因为涉及跨域,所以通常通过img或者script来实现)

在实现中碰到一些的问题

先来看一下埋点的格式规范:

// 这里我删减了一些不必要的字段,仅供参考
{
     "department":"部门",
     "eventType" : "click/pv/时间统计",依照类型不同,统计维度不同=>各业务部提供逻辑及规则
     "eventBiz": {
         "common" : { //共同
            "time":"事件触发时间",
            .......
         },
         "extra":{ //自定义
            ........
         }
     }
   }

我们这边后端规定了几种eventType(事件类型):

事件类型 事件缩写 说明
点击事件 click 页面上的任何点击事件
页面访问 pv 访问页面事件
持续时长 duration 视频播放观看时长
页面异常 aberrant 页面发生异常时状态数据
页面性能 performance 页面性能统计,如页面整体加载时间、dom加载时长等

jssdk对外要开放哪些东西?

按理说,jssdk只需要解析外面一个全局变量就行。类似下面的截图

alt

但有时候send这一块是需要使用者自己来调用的,比如说统计视频播放了多久,肯定是播一段时间,上报一次,这个上报就必须用户自己发起调用。

因此我的想法是没必要提供两个全局对象,就一个全局对象,里面提供:

  • initConfig
  • send

initConfig是初始化的数据,send是进行上报。

base64加密

这个是后端对前端的这边的一个要求:

alt

将上图转化成代码,就是:

helper.generateUrl = function (url, param) {
    return url + "?data=" + helper.encode(JSON.stringify(param));
}

encode方法我直接抄了base64的代码:

var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var b64tab = function (bin) {
    var t = {};
    for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i;
    return t;
}(b64chars);
var fromCharCode = String.fromCharCode;
var cb_utob = function (c) {
    if (c.length < 2) {
        var cc = c.charCodeAt(0);
        return cc < 0x80 ? c
            : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
                + fromCharCode(0x80 | (cc & 0x3f)))
                : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
                    + fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
                    + fromCharCode(0x80 | (cc & 0x3f)));
    } else {
        var cc = 0x10000
            + (c.charCodeAt(0) - 0xD800) * 0x400
            + (c.charCodeAt(1) - 0xDC00);
        return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
            + fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
            + fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
            + fromCharCode(0x80 | (cc & 0x3f)));
    }
};
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
var utob = function (u) {
    return u.replace(re_utob, cb_utob);
};
var cb_encode = function (ccc) {
    var padlen = [0, 2, 1][ccc.length % 3],
        ord = ccc.charCodeAt(0) << 16
            | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
            | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
        chars = [
            b64chars.charAt(ord >>> 18),
            b64chars.charAt((ord >>> 12) & 63),
            padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
            padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
        ];
    return chars.join('');
};
var btoa = function (b) {
    return b.replace(/[\s\S]{1,3}/g, cb_encode);
};
var _encode = function (u) { return btoa(utob(u)) }
var encode = function (u) {
    return _encode(String(u));
};

helper.encode = encode;

其实我跟后端说了,让base64的加密在后端处理,然后他觉得前端弄一下比较容易,我也是无奈。后端node来处理的话,就是一句话,像这样的:

console.log(Buffer.from("我").toString("base64"));

common及extra

common通常有以下字段:

{
   "time":"1523429722342", //事件触发时间
   "domain":"",
   "documentUrl":"", //当前文档地址
   "module":"",  //模块英文名称,以warden-开头
   "moduleName":"统计", //模块中文名称
   "title":"", //页面标题
   "referrer":"", //页面来源
   "width":100,  //屏幕宽度
   "height":200,  //屏幕
   "lang":"zh-cn", //浏览器
   "userAgent":"",//客户端浏览器
   "userUuid" : "", //识别用户唯一标识符
   "os":"windows" //操作系统
}

module及moduleName通常是click事件时需要传递的。

userAgent可以通过navigator.userAgent来得到。

os这一个获取比较有意思,我抄了一段github上的代码过来:

var clientStrings = [
    { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ },
    { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ },
    { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ },
    { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ },
    { s: 'Windows Vista', r: /Windows NT 6.0/ },
    { s: 'Windows Server 2003', r: /Windows NT 5.2/ },
    { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ },
    { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ },
    { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ },
    { s: 'Windows 98', r: /(Windows 98|Win98)/ },
    { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ },
    { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ },
    { s: 'Windows CE', r: /Windows CE/ },
    { s: 'Windows 3.11', r: /Win16/ },
    { s: 'Android', r: /Android/ },
    { s: 'Open BSD', r: /OpenBSD/ },
    { s: 'Sun OS', r: /SunOS/ },
    { s: 'Linux', r: /(Linux|X11)/ },
    { s: 'iOS', r: /(iPhone|iPad|iPod)/ },
    { s: 'Mac OS X', r: /Mac OS X/ },
    { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ },
    { s: 'QNX', r: /QNX/ },
    { s: 'UNIX', r: /UNIX/ },
    { s: 'BeOS', r: /BeOS/ },
    { s: 'OS/2', r: /OS\/2/ },
    { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ }
];

var os;

for (var id in clientStrings) {
    var cs = clientStrings[id];

    // 注意不要用ua,ua是小写化
    if (cs.r.test(navigator.userAgent)) {
        os = cs.s;
        break;
    }
}

if (/Windows/.test(os)) {
    osVersion = /Windows (.*)/.exec(os)[1];
    os = 'Windows';
}

extra就是收集的额外数据,比如说错误信息、页面加载时间等。

动态绑定事件

helper.bindEvent = function (elem, type, selector, fn) {
    if (fn == null) {
        fn = selector;
        selector = null;
    }
    elem.addEventListener(type, function (e) {
        var target;
        if (selector) {
            target = e.target;
            if (target.matches(selector)) {
                fn.call(target, e);
            }
        } else {
            fn(e);
        }
    })
}

这个是用来监听指定元素的click事件,当触发click时,主动上报。jssdk收集data-xx属性,有些放到common中,有些放到extra中。

在取data-xx的属性时,有两种写法:

  • element.getAttribute("data-xx")
  • element.dataset.xx

现在的浏览器基本能支持第二种写法。

extend继承

helper.extend = function (target, source) {
    for (key in source) {
        if (typeof source[key] === "object") {
            if (!target[key]) {
                target[key] = {};
            }
            helper.extend(target[key], source[key]);
        } else {
            target[key] = source[key]
        }
    }
    return target;
};

当然上面实现的有问题,因为没考虑数组的情况(还有将target对象修改掉了,很要命)。。但是我贴这个,是想说明在继承的时候,需要考虑深拷贝的情况。

而且上面也考虑N个继承的情况,有空了再完善一下。

页面加载时间计算

之前在讨论jssdk方案的时候,有同事给我提到了:performance

alt

通过这个可以简单地计算下页面加载时间:

helper.handleAddListener('load', getTiming)

function getTiming() {
    var now = new Date().getTime();
    var page_load_time = now - performance.timing.navigationStart;
    // console.log("页面加载时间:", page_load_time + "ms");
}

其他

剩下的主要是一些业务逻辑上的思考,怎么尽量地让使用者少传参之类的。

不过个人觉得我代码写的并不是太好,比如没有分模块。话说回来,模块也不多,没有分的意义吧。

本文链接:www.my-fe.pub/post/collection-js-sdk-note.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。