好玩的JS系列--小程序數(shù)據(jù)埋點(diǎn)

前言

  最近負(fù)責(zé)的一個(gè)項(xiàng)目送爸,項(xiàng)目的客戶端是微信小程序铛嘱。客戶提了需求袭厂,需要進(jìn)行數(shù)據(jù)埋點(diǎn)墨吓,收集用戶行為數(shù)據(jù),為后續(xù)提升用戶體驗(yàn)及產(chǎn)品優(yōu)化提供基礎(chǔ)數(shù)據(jù)收集纹磺。于是在一個(gè)風(fēng)和日麗的日子帖烘,擼起袖子動(dòng)手干。

思路

  先看看客戶端常見(jiàn)的數(shù)據(jù)埋點(diǎn)方案如下圖橄杨。

數(shù)據(jù)埋點(diǎn)方案.png

  侵入式埋點(diǎn)這個(gè)如果在頁(yè)面少秘症,數(shù)據(jù)收集需求變化不大的情況下,還是挺經(jīng)濟(jì)實(shí)惠的式矫。鑒于當(dāng)前項(xiàng)目頁(yè)面較多历极,數(shù)據(jù)收集的需求變化也在不斷的調(diào)整,所以侵入式埋點(diǎn)的方案不適用本項(xiàng)目衷佃,只有松耦合這個(gè)方案了。

小程序的特點(diǎn)

  根據(jù)松耦合埋點(diǎn)方案蹄葱,首先要解決的問(wèn)題是攔截小程序所有控件的操作數(shù)據(jù)氏义。在Web中有BOM對(duì)象及DOM對(duì)象可以給我們進(jìn)行全方位的操作,特別是BOM對(duì)象图云,可以進(jìn)行全局事件攔截惯悠。而小程序類似BOM對(duì)象的主要有下面三個(gè)對(duì)象:App、Page竣况、Component克婶。

  • App:App對(duì)象有且只有一個(gè),屬于小程序的總控對(duì)象丹泉,App下面包含著n個(gè)Page對(duì)象情萤。
  • Page:Page則通過(guò)字面理解就是頁(yè)面的總控對(duì)象,一個(gè)小程序可以有多個(gè)Page摹恨,每一個(gè)Page對(duì)象對(duì)應(yīng)一個(gè)頁(yè)面筋岛,Page下面可以包含n個(gè)Component及其他普通的小程序組件。
  • Component:Component則是自定義組件對(duì)象晒哄,類似于Page睁宰,只明生命周期及一些特性上有所區(qū)別肪获,自定義組件對(duì)象在實(shí)際項(xiàng)目中比較常用,主要用來(lái)解決小程序頁(yè)面堆棧太小的問(wèn)題柒傻,這個(gè)后續(xù)再寫(xiě)另外一篇來(lái)講孝赫。

  小程序的事件機(jī)制也是采用與JavaScript一樣的事件機(jī)制,捕獲->執(zhí)行->冒泡的機(jī)制红符,JavaScript在BOM及DOM對(duì)象中都提供了addEventListener這種訂閱/發(fā)布機(jī)制青柄,使我們可以輕松的攔截所有的事件,又不影響現(xiàn)有的流程及代碼违孝,從而實(shí)現(xiàn)松耦合埋點(diǎn)方案刹前。但是小程序沒(méi)有提供addEventListener的訂閱/發(fā)布機(jī)制,沒(méi)有辦法通過(guò)這樣子的方案來(lái)實(shí)現(xiàn)雌桑。

1.攔截器方式

翻了官網(wǎng)喇喉,發(fā)現(xiàn)沒(méi)有辦法統(tǒng)一的處理,那么只能退而求其次校坑,減少對(duì)現(xiàn)有的代碼的侵入拣技,于是想到了攔截器的機(jī)制來(lái)實(shí)現(xiàn)。

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

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事件進(jìn)行示例膏斤,其他事件都差不多這樣處理即可。

var appFilter = function(config){
  if(config.onLaunch){
    let _onLaunch = config.onLaunch;
    config.onLaunch = function(ops){
         //在這里干埋點(diǎn)的事
        //例如存儲(chǔ)數(shù)據(jù)邪驮、上送數(shù)據(jù)
         _onLaunch.call(this);//調(diào)用原來(lái)的執(zhí)行邏輯
    }
  }
  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'
})
)

  這個(gè)也是JS好玩的地方莫辨,一切皆為對(duì)象。通過(guò)新增一個(gè)Function對(duì)象毅访,增強(qiáng)方法之后沮榜,把舊的Function對(duì)象進(jìn)行替換。上面的App的onLaunch方法喻粹,經(jīng)過(guò)appFilter過(guò)濾器的處理后蟆融,就替換成過(guò)濾器里面加了埋點(diǎn)事件的新方法了,對(duì)于開(kāi)發(fā)人員來(lái)講守呜,不需要去改動(dòng)原來(lái)App.onLaunch事件里面的代碼型酥,目的達(dá)到了,但是不夠完美查乒,在寫(xiě)過(guò)濾器的實(shí)現(xiàn)邏輯時(shí)柔逼,給了我一個(gè)靈感蓖谢,我是否可以用這種方式來(lái)實(shí)現(xiàn)不侵入代碼的埋點(diǎn)?答案是可以的。

2.增強(qiáng)擴(kuò)展方式

  利用新增一個(gè)Function對(duì)象岸更,增強(qiáng)方法之后溺蕉,把舊的Function對(duì)象進(jìn)行替換這樣的原理,依次將App、Page蔑舞、Component這三個(gè)對(duì)象進(jìn)行增強(qiáng)擴(kuò)展,示例代碼如下嘹屯。

//先把原生的三個(gè)對(duì)象保存起來(lái)
const originalApp = App,
      originalPage = Page,
      originalComponent = Component;
//在原生的事件函數(shù)里面攻询,添加數(shù)據(jù)埋點(diǎn),并替換成新的事件函數(shù)
const _extendsApp = function (conf, method) {
  const _o_method = conf[method];
  conf[method] = function (ops) {
    //在此處進(jìn)行數(shù)據(jù)埋點(diǎn)
    if (typeof _o_method === 'function') {
      _o_method.call(this, ops);
    }
  }
}

//重新定義App這個(gè)對(duì)象州弟,將原來(lái)的App對(duì)象覆蓋
App = function(conf){
  //定義需要增強(qiáng)的方法
  const methods = ['onLaunch', 'onShow', 'onHide', 'onError']

  methods.map(function (method) {
    _extendsApp(conf, method);
  })
  //另外增強(qiáng)擴(kuò)展埋點(diǎn)上送的方法
  conf.william = {
    addActionData: function (ops) {
      console.log('addActionData');
    },
    addVisitLog: function (ops) {
      console.log('addVisitLog');
    }
  }
  return originalApp(conf);
}
//Page及Component對(duì)象類似App的處理即可

  至此钧栖,整個(gè)小程序的生命周期都在掌握之中了,可以按需采集對(duì)應(yīng)的數(shù)據(jù)婆翔,并且對(duì)于開(kāi)發(fā)人員來(lái)講拯杠,還不需要去修改及調(diào)整代碼,松耦合埋點(diǎn)方案搞定啃奴。

3.增強(qiáng)擴(kuò)展+訂閱/發(fā)布

  上面的實(shí)現(xiàn)方案雖然實(shí)現(xiàn)了松耦合潭陪,但是個(gè)人覺(jué)得還不夠完美,埋點(diǎn)的動(dòng)作必須要寫(xiě)在增加的方法里面最蕾,這樣子可維護(hù)性較差依溯,也不夠靈活。解耦這事現(xiàn)在對(duì)我來(lái)說(shuō)信手拈來(lái)瘟则,祭出了我的EventHub(基于訂閱/發(fā)布模式實(shí)現(xiàn)的消息總線)黎炉,完美。

//引用EventHub
import EventHub from '../../utils/eventhub.min';
//先把原生的三個(gè)對(duì)象保存起來(lái)
const originalApp = App,
      originalPage = Page,
      originalComponent = Component;
//在原生的事件函數(shù)里面醋拧,添加數(shù)據(jù)埋點(diǎn)慷嗜,并替換成新的事件函數(shù)
const _extendsApp = function (conf, method) {
  const _o_method = conf[method];
  conf[method] = function (ops) {
    //在此處進(jìn)行數(shù)據(jù)埋點(diǎn)
    //此處改成消息發(fā)布
    if (typeof EventHub != "undefined") {
      EventHub.emit('app' + method, ops);
    }
    if (typeof _o_method === 'function') {
      _o_method.call(this, ops);
    }
  }
}

//重新定義App這個(gè)對(duì)象,將原來(lái)的App對(duì)象覆蓋
App = function(conf){
  //定義需要增強(qiáng)的方法
  const methods = ['onLaunch', 'onShow', 'onHide', 'onError']

  methods.map(function (method) {
    _extendsApp(conf, method);
  })
  //另外增強(qiáng)擴(kuò)展埋點(diǎn)上送的方法
  conf.william = {
    addActionData: function (ops) {
      console.log('addActionData');
    },
    addVisitLog: function (ops) {
      console.log('addVisitLog');
    }
  }
  return originalApp(conf);
}
//Page及Component對(duì)象類似App的處理即可

  這樣子就可以把埋點(diǎn)處理的邏輯抽離到另外一個(gè)JS文件中去實(shí)現(xiàn)丹壕。

EventHub.on('apponLaunch',function(ops){
    //在這里可以處理數(shù)據(jù)埋點(diǎn)的事
})

延展性思考

  基于上述的實(shí)現(xiàn)方案庆械,我們除了增強(qiáng)生命周期,也可以像上面那樣去增加公用的方法雀费,例如App.william.addActionData,更可以通過(guò)增強(qiáng)setData方法來(lái)進(jìn)行數(shù)據(jù)溯源或者進(jìn)行差量比較來(lái)提升性能等方式痊焊。

分享一個(gè)簡(jiǎn)單自寫(xiě)的訂閱發(fā)布模型盏袄,有需要的同學(xué)請(qǐng)自便

(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 () {
    // 頁(yè)面加載完成事件
    var EVENT_RENDER_COMPLETE = 'EVENT_RENDER_COMPLETE';

    var _handlers = {};

    /**
     * 添加訂閱事件,返回事件對(duì)象
     * @param {string} e 事件
     * @param {function} fn 回調(diào)函數(shù)
     * @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;
    };

    /**
     * 發(fā)布消息
     * @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請(qǐng)求狀態(tài)事件
        PLATFORM_LOADING:'PLATFORM_LOADING',//Ajax請(qǐng)求加載事件

        /**
         * 添加訂閱事件,返回事件對(duì)象
         * @param {string} e 事件
         * @param {function} fn 回調(diào)函數(shù)
         * @return {Object} event_object 事件類型
         */
        on: _on,

        /**
         * 發(fā)布消息
         * @param {string} e 事件類型
         */
        emit: _emit,

        /**
         * 取消訂閱
         * @param {Object} event_object
         */
        off: _off,

        /**
         * 頁(yè)面完成加載
         */
        ready: _ready
    };
})();

module.exports = EventHub;


/***/ })
/******/ ]);
});
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市薄啥,隨后出現(xiàn)的幾起案子辕羽,更是在濱河造成了極大的恐慌,老刑警劉巖垄惧,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刁愿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡到逊,警方通過(guò)查閱死者的電腦和手機(jī)铣口,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)滤钱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人脑题,你說(shuō)我怎么就攤上這事件缸。” “怎么了叔遂?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵他炊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我已艰,道長(zhǎng)痊末,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任哩掺,我火速辦了婚禮凿叠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疮丛。我一直安慰自己幔嫂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布誊薄。 她就那樣靜靜地躺著履恩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呢蔫。 梳的紋絲不亂的頭發(fā)上切心,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音片吊,去河邊找鬼绽昏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俏脊,可吹牛的內(nèi)容都是我干的全谤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼爷贫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼认然!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起漫萄,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卷员,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后腾务,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體毕骡,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了未巫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窿撬。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖橱赠,靈堂內(nèi)的尸體忽然破棺而出尤仍,到底是詐尸還是另有隱情,我是刑警寧澤狭姨,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布宰啦,位于F島的核電站,受9級(jí)特大地震影響饼拍,放射性物質(zhì)發(fā)生泄漏赡模。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一师抄、第九天 我趴在偏房一處隱蔽的房頂上張望漓柑。 院中可真熱鬧,春花似錦叨吮、人聲如沸辆布。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)锋玲。三九已至,卻和暖如春涵叮,著一層夾襖步出監(jiān)牢的瞬間惭蹂,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工割粮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盾碗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓舀瓢,卻偏偏與公主長(zhǎng)得像廷雅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子京髓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • title: 小程序教程之wepy 參考文檔 中文文檔:https://tencent.github.io/wep...
    采香行處蹙連錢(qián)閱讀 11,898評(píng)論 8 24
  • 魚(yú)叔最喜吃魚(yú)了航缀,在家吃貫了清蒸,紅燒的做法朵锣,改變一下口感谬盐,用烤的方法是不錯(cuò)的選擇甸私。今天魚(yú)叔介紹一下魚(yú)氏烤白鯧的制作...
    游不停的魚(yú)閱讀 446評(píng)論 0 2
  • 在開(kāi)始一個(gè)產(chǎn)品設(shè)計(jì)之前,我們往往會(huì)做用戶角色需求分析诬烹、需求調(diào)研砸烦。需求調(diào)研是為我們項(xiàng)目設(shè)計(jì)階段而準(zhǔn)備的,需求調(diào)研的質(zhì)...
    產(chǎn)品喵dandan米娜閱讀 3,973評(píng)論 1 5
  • 在要排序的一組數(shù)中绞吁,對(duì)當(dāng)前還未排好序的范圍內(nèi)的全部數(shù)幢痘,自上而下對(duì)相鄰的兩個(gè)數(shù)依次進(jìn)行比較和調(diào)整,讓較大的數(shù)往下沉...
    儲(chǔ)僖僧僳儒閱讀 455評(píng)論 0 0