好玩的JS系列--小程序数据埋点

前言

  最近负责的一个项目,项目的客户端是微信小程序。客户提了需求,需要进行数据埋点,收集用户行为数据,为后续提升用户体验及产品优化提供基础数据收集。于是在一个风和日丽的日子,撸起袖子动手干。

思路

  先看看客户端常见的数据埋点方案如下图。

数据埋点方案.png

  侵入式埋点这个如果在页面少,数据收集需求变化不大的情况下,还是挺经济实惠的。鉴于当前项目页面较多,数据收集的需求变化也在不断的调整,所以侵入式埋点的方案不适用本项目,只有松耦合这个方案了。

小程序的特点

  根据松耦合埋点方案,首先要解决的问题是拦截小程序所有控件的操作数据。在Web中有BOM对象及DOM对象可以给我们进行全方位的操作,特别是BOM对象,可以进行全局事件拦截。而小程序类似BOM对象的主要有下面三个对象:App、Page、Component。

  • App:App对象有且只有一个,属于小程序的总控对象,App下面包含着n个Page对象。
  • Page:Page则通过字面理解就是页面的总控对象,一个小程序可以有多个Page,每一个Page对象对应一个页面,Page下面可以包含n个Component及其他普通的小程序组件。
  • Component:Component则是自定义组件对象,类似于Page,只明生命周期及一些特性上有所区别,自定义组件对象在实际项目中比较常用,主要用来解决小程序页面堆栈太小的问题,这个后续再写另外一篇来讲。

  小程序的事件机制也是采用与JavaScript一样的事件机制,捕获->执行->冒泡的机制,JavaScript在BOM及DOM对象中都提供了addEventListener这种订阅/发布机制,使我们可以轻松的拦截所有的事件,又不影响现有的流程及代码,从而实现松耦合埋点方案。但是小程序没有提供addEventListener的订阅/发布机制,没有办法通过这样子的方案来实现。

1.拦截器方式

翻了官网,发现没有办法统一的处理,那么只能退而求其次,减少对现有的代码的侵入,于是想到了拦截器的机制来实现。

  看看下面小程序原生的代码

App({
  onLaunch(options) {
    // Do something initial when launch.
  },
  onShow(options) {
    // Do something when show.
  },
  onHide() {
    // Do something when hide.
  },
  onError(msg) {
    console.log(msg)
  },
  globalData: 'I am global data'
})

  提供拦截器,这里只拿App的onLaunch事件进行示例,其他事件都差不多这样处理即可。

var appFilter = function(config){
  if(config.onLaunch){
    let _onLaunch = config.onLaunch;
    config.onLaunch = function(ops){
         //在这里干埋点的事
        //例如存储数据、上送数据
         _onLaunch.call(this);//调用原来的执行逻辑
    }
  }
  return config;
}


//App使用拦截器示例
App(appFilter({
  onLaunch(options) {
    // Do something initial when launch.
  },
  onShow(options) {
    // Do something when show.
  },
  onHide() {
    // Do something when hide.
  },
  onError(msg) {
    console.log(msg)
  },
  globalData: 'I am global data'
})
)

  这个也是JS好玩的地方,一切皆为对象。通过新增一个Function对象,增强方法之后,把旧的Function对象进行替换。上面的App的onLaunch方法,经过appFilter过滤器的处理后,就替换成过滤器里面加了埋点事件的新方法了,对于开发人员来讲,不需要去改动原来App.onLaunch事件里面的代码,目的达到了,但是不够完美,在写过滤器的实现逻辑时,给了我一个灵感,我是否可以用这种方式来实现不侵入代码的埋点?答案是可以的。

2.增强扩展方式

  利用新增一个Function对象,增强方法之后,把旧的Function对象进行替换这样的原理,依次将App、Page、Component这三个对象进行增强扩展,示例代码如下。

//先把原生的三个对象保存起来
const originalApp = App,
      originalPage = Page,
      originalComponent = Component;
//在原生的事件函数里面,添加数据埋点,并替换成新的事件函数
const _extendsApp = function (conf, method) {
  const _o_method = conf[method];
  conf[method] = function (ops) {
    //在此处进行数据埋点
    if (typeof _o_method === 'function') {
      _o_method.call(this, ops);
    }
  }
}

//重新定义App这个对象,将原来的App对象覆盖
App = function(conf){
  //定义需要增强的方法
  const methods = ['onLaunch', 'onShow', 'onHide', 'onError']

  methods.map(function (method) {
    _extendsApp(conf, method);
  })
  //另外增强扩展埋点上送的方法
  conf.william = {
    addActionData: function (ops) {
      console.log('addActionData');
    },
    addVisitLog: function (ops) {
      console.log('addVisitLog');
    }
  }
  return originalApp(conf);
}
//Page及Component对象类似App的处理即可

  至此,整个小程序的生命周期都在掌握之中了,可以按需采集对应的数据,并且对于开发人员来讲,还不需要去修改及调整代码,松耦合埋点方案搞定。

3.增强扩展+订阅/发布

  上面的实现方案虽然实现了松耦合,但是个人觉得还不够完美,埋点的动作必须要写在增加的方法里面,这样子可维护性较差,也不够灵活。解耦这事现在对我来说信手拈来,祭出了我的EventHub(基于订阅/发布模式实现的消息总线),完美。

//引用EventHub
import EventHub from '../../utils/eventhub.min';
//先把原生的三个对象保存起来
const originalApp = App,
      originalPage = Page,
      originalComponent = Component;
//在原生的事件函数里面,添加数据埋点,并替换成新的事件函数
const _extendsApp = function (conf, method) {
  const _o_method = conf[method];
  conf[method] = function (ops) {
    //在此处进行数据埋点
    //此处改成消息发布
    if (typeof EventHub != "undefined") {
      EventHub.emit('app' + method, ops);
    }
    if (typeof _o_method === 'function') {
      _o_method.call(this, ops);
    }
  }
}

//重新定义App这个对象,将原来的App对象覆盖
App = function(conf){
  //定义需要增强的方法
  const methods = ['onLaunch', 'onShow', 'onHide', 'onError']

  methods.map(function (method) {
    _extendsApp(conf, method);
  })
  //另外增强扩展埋点上送的方法
  conf.william = {
    addActionData: function (ops) {
      console.log('addActionData');
    },
    addVisitLog: function (ops) {
      console.log('addVisitLog');
    }
  }
  return originalApp(conf);
}
//Page及Component对象类似App的处理即可

  这样子就可以把埋点处理的逻辑抽离到另外一个JS文件中去实现。

EventHub.on('apponLaunch',function(ops){
    //在这里可以处理数据埋点的事
})

延展性思考

  基于上述的实现方案,我们除了增强生命周期,也可以像上面那样去增加公用的方法,例如App.william.addActionData,更可以通过增强setData方法来进行数据溯源或者进行差量比较来提升性能等方式。

分享一个简单自写的订阅发布模型,有需要的同学请自便

(function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory();
    else if(typeof define === 'function' && define.amd)
        define([], factory);
    else if(typeof exports === 'object')
        exports["EventHub"] = factory();
    else
        root["EventHub"] = factory();
})(this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};
/******/
/******/    // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }
/******/
/******/
/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;
/******/
/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;
/******/
/******/    // identity function for calling harmony imports with the correct context
/******/    __webpack_require__.i = function(value) { return value; };
/******/
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, {
/******/                configurable: false,
/******/                enumerable: true,
/******/                get: getter
/******/            });
/******/        }
/******/    };
/******/
/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };
/******/
/******/    // Object.prototype.hasOwnProperty.call
/******/    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";
/******/
/******/    // Load entry module and return exports
/******/    return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

/**
     * 2016-11-21 William
     * Publish/Subscribe
     * 用于解决组件间的通信耦合
     */
var EventHub = (function () {
    // 页面加载完成事件
    var EVENT_RENDER_COMPLETE = 'EVENT_RENDER_COMPLETE';

    var _handlers = {};

    /**
     * 添加订阅事件,返回事件对象
     * @param {string} e 事件
     * @param {function} fn 回调函数
     * @return {Object} event_object 事件类型
     */
    var _on = function (e, fn) {
        if (!(e in _handlers)) {
            _handlers[e] = [];
        }
        var _event_object = {
            fn: fn,
            e: e
        };
        _handlers[e].push(_event_object);
        return _event_object;
    };

    /**
     * 发布消息
     * @param {string} e 事件类型
     */
    var _emit = function (e) {
        if (!_handlers[e]) {return;}
        var args = Array.prototype.slice.call(arguments, 1);
        for (var i = 0; i < _handlers[e].length; i++) {
            (function () {
                var _handlerFn = _handlers[e][i].fn;
                // var _handler = function() {
                _handlerFn.apply(this, args);
                // }.bind(this);
                // setTimeout(_handler, 0);
            })();
        }
    };

    /**
     * 取消订阅
     * @param {Object} event_object
     */
    var _off = function (event_object) {
        if (_handlers[event_object]) {
            for (var i = _handlers[event_object].length - 1; i > -1; i--) {
                if (event_object === _handlers[event_object][i].e) {
                    _handlers[event_object].splice(i, 1);
                }
            }
        }
    };

    var _ready = function (fn) {
        _on(EVENT_RENDER_COMPLETE, fn);
    };

    return {
        PLATFORM_AJAX_STATUS:'PLATFORM_AJAX_STATUS',//Ajax请求状态事件
        PLATFORM_LOADING:'PLATFORM_LOADING',//Ajax请求加载事件

        /**
         * 添加订阅事件,返回事件对象
         * @param {string} e 事件
         * @param {function} fn 回调函数
         * @return {Object} event_object 事件类型
         */
        on: _on,

        /**
         * 发布消息
         * @param {string} e 事件类型
         */
        emit: _emit,

        /**
         * 取消订阅
         * @param {Object} event_object
         */
        off: _off,

        /**
         * 页面完成加载
         */
        ready: _ready
    };
})();

module.exports = EventHub;


/***/ })
/******/ ]);
});

你可能感兴趣的:(好玩的JS系列--小程序数据埋点)