关于javascript:手摸手带大家实现一套小程序的全埋点SDK方案

9次阅读

共计 5850 个字符,预计需要花费 15 分钟才能阅读完成。

概念相干

本篇文章重点在怎么实现一个简略的微信小程序无痕埋点计划,所以对埋点的概念性常识不做过多解读,大家能够自行 Google
咱们在这里只须要来理解两个关键性的问题即可。

什么是埋点

埋点是 数据采集畛域 的一个术语,简略点讲就是用来 追踪用户的一些特定行为和事件,进行记录上报
而常见的埋点次要分为三类:

  • 手动埋点(代码埋点)
  • 主动埋点(无痕埋点)
  • 可视化埋点

埋点的作用

我感觉从用户和本身产品两点来说会清晰一些:

  • 针对用户:对采集的数据能够进行剖析,以便提供给用户精准的信息推送、个性化举荐等,当然这些用户的行为信息也能够进步产品的经营体验。
  • 针对产品:通过剖析用户在每个页面的停留时间,以及交互的节点等诸多信息,来剖析产品的现存问题,提供后续的优化思路。

    参考链接:https://xw.qq.com/partner/vivoscreen/20210520A08ATP00

其实从概念中也能够晓得,埋点时信息的追踪,和信息的上报其实是齐全独立的。所以咱们在设计时也能够将这两局部独立设计。

ok,接下来就是手摸手 time!

实现过程

总体思路

首先咱们须要晓得其实一个微信小程序就是一个 App,而后这个App 外面由多个 Page(页面)和Component(自定义组件)组合而成。

乏味的是,无论是 App 的注册,还是 PageComponent的注册都是由各自对应的办法传入对应的参数 (options) 来实现的(从上图中也能够看出这种模式)

咱们拿 Page 的注册来举个例子,将图中的注册办法换个形式写,来贴切一下咱们的形容。

const options = {
  data: {msg: 'Hello World',},
  // 用户自定义事件
  bindViewTap() {
    wx.navigateTo({url: '../logs/logs',});
  },
  // Page 自带的生命周期
  onLoad() {if (wx.getUserProfile) {
      this.setData({canIUseGetUserProfile: true,});
    }
  },
};
// 注册以后页面,由微信提供的 Page 办法传入对应的参数来实现
Page(options);

App、Component 的注册与此相似,有趣味能够看一下。

所以所以所以!咱们能够通过重写 AppPageComponent 这三个办法,来实现对其 options 身上一些办法和生命周期的监听

(恩,大略就是这么个意思,我踩你们都看懂了)

ok,理解了大抵思维,咱们先来总结一下,而后开始动手做:

  1. 设计一个 tracker 来对 AppPageComponent 办法进行重写
  2. 设计一个 reporter 将监听的的事件数据进行记录,并发送给服务端

Tracker

在开始敲代码之前咱们先来确认一下咱们实现的这个埋点要怎么应用,只有确定了一个正确的应用形式,前面才能够依据这个口子来补充咱们对应的性能。

通常状况下,咱们如果要批改微信小程序的原生办法的话,那么就要在其入口文件 app.js 处来导入咱们的重写办法,来达到该目标

// app.js
import init from './track/index';

init({
  ak: 'minapp-001',
  url: 'http://baidu.com',
  autoTrack: {
    appLaunch: false,
    appHide: false,
    appShow: false,
    pageShow: false,
    pageHide: false,
    pageUnload: false,
    onShare: false,
  },
  // other
});

其中的 config 是暂定的,后续能够依据具体需要来增加各种配置参数,这里咱们为了保障简略只定三个属性

  • ak:保障本次埋点程序的惟一值
  • url:埋点获取到的信息须要上传的服务端地址
  • autoTrack:全埋点的开启计划,对应字段设置为 true 则开启主动埋点信息的捕捉

确定了应用办法之后,咱们就能够由此来确定咱们的代码要怎么开展。

1. init

// 将原生的三个办法先暂存起来,前面会用到
const collector = {
  oldApp = App,
  oldPage = Page,
}
const init = (config) => {
  // 生成 cid
  if (!storage.get('cid')) {storage.set('cid', getUUID());
  }
  // 初始化用户自定义配置,store 是一个全局的数据仓库(不必关怀)if (config !== undefined) store.set('config', config);

  // 重写 App&Page 办法
  App = (options) => collector.oldApp(proxyAppOptions(options));
  Page = (options) => collector.oldPage(proxyPageOptions(options));
};

这里能够看到,咱们在 init 办法中,实现了最要害的一步,就是对 AppPage 办法的重写。

这里有一点须要留神的是,咱们须要明确咱们重写办法的目标 是为了获取它身上传入的 options 参数的一些属性和办法
所以在这里咱们会对他的 options 进行重写,而后将重写后的后果,再次传入原生的 AppPage 办法中执行。

咱们在上方定义的 collector 就是为了保留原生的这些办法,不便咱们在这里调用。
(当然 collector 中不止有这些属性,前面还会收集一些其余信息)

2. proxyAppOptions

接下来让咱们来看一下 proxyAppOptions 办法都做了啥操作

/**
 * 重写 App 中的 options 参数
 * @param {*} options 原始的 options 参数
 * @returns 新的 options 参数
 */
const _proxyAppOptions = (options) => {
  // 向 App 中注入手动埋点的办法
  options.$ta = {// 不便用户通过 getApp()办法间接调用 track 办法
    track: $ta.track.bind(reporter),
    // 游客拜访 uid 默认为 0,用户登录之后须要手动更新其用户 id
    login: (uid) => this.login(uid),
  };

  // onLaunch 事件监听
  options.onLaunch = useAppLaunch(options.onLaunch);
  // onShow 事件监听
  options.onShow = useAppShow(options.onShow);
  // onHide 事件监听
  options.onHide = useAppHide(options.onHide);

  return options;
};

这段代码能够看出就是对 options 中的生命周期进行重写, 而后退出本人的一些解决逻辑,比方 useAppLaunchuseAppShow 这些钩子函数。
接下来咱们先来看一下这些钩子的实现,其实这些钩子很简略就是退出一些本人埋点须要采集数据的一些逻辑

/** ====================App 事件代理 ======================== */
export const useAppLaunch = (oldOnLunch) =>
  _proxyHooks(oldOnLunch, function () {
    const data = {
      event: 'appLaunch',
      path,
      title,
      timemap,
    };
    $ta.track('devices', data);
  });

export const useAppShow = (oldOnShow) =>
  _proxyHooks(oldOnShow, function () {
    const data = {event: 'appShow',};
    $ta.track('devices', data);
  });

export const useAppHide = (oldOnHide) => {_proxyHooks(oldOnHide, function () {
    const data = {event: 'appHide',};
    $ta.track('devices', data);
  });
};

/**
 * 代理原始办法,并执行回调函数
 * @param {*} fn 须要代理的办法
 * @param {*} cb 须要执行的回调
 */
function _proxyHooks(fn = function () {}, cb) {return function () {
    // 如果回调存在
    if (cb) {cb.apply(this, arguments);
    }
    // 执行原函数
    fn.apply(this);
  };
}

其实这里也没什么好说的,逻辑也比拟清晰,能够看下 proxyHooks 这个办法,其实就是执行原始办法,而后再执行传入的回调,而咱们在回调中其实就是增加一些本人须要埋点的一些数据信息。

$ta这个办法就是 Reporter 那局部的,这里咱们只有理解它是用来发送埋点数据的就能够了,在下文会具体讲一下它的实现。

3. proxyPageOptions

其实 page 这部分和下面的 app 中做的事件都一样,都是对生命周期进行一些解决,只不过 page 页面中除了生命周期之外,还会有很多自定义事件,所以这里针对这块咱们能够一起来看一下。

这里的自定义事件,个别是就是一些点击事件。

// page 的原始申明周期汇合
const PAGE_LIFE_METHOD = [
  'onLoad',
  'onShow',
  'onReady',
  'onHide',
  'onUnload',
  'onPullDownRefresh',
  'onReachBottom',
  'onShareAppMessage',
  'onShareTimeline',
  'onAddToFavorites',
  'onPageScroll',
  'onResize',
  'onTabItemTap',
  'onSaveExitState',
];
/**
 * 重写 Page 中的 options 参数
 * @param {*} options 原始的 options 参数
 * @returns 新的 options 参数
 */
const proxyPageOptions = (options) => {
  // ...

  // 自定义事件监听
  for (let prop in options) {
    // 须要保障是函数,并且不是原生的生命周期函数
    if (typeof options[prop] == 'function' &&
      !PAGE_LIFE_METHOD.includes(prop)
    ) {
      // 重写 options 身上的自定义办法
      options[prop] = usePageClickEvent(options[prop]);
    }
  }
  return options;
};

这一部分的解决其实也比较简单,就是遍历 options 身上的属性,判断进去自定义的一些事件就行解决,咱们来看一下 pageClickEvent 这个钩子做了啥

/**
 * 监听页面的点击事件
 * @param {*} oldEvent 原生的 page 自定义事件
 */
export const pageClickEvent = (oldEvent) =>
  _proxyHooks(oldEvent, function (e) {if (e && e.type === 'tap') {
      $ta.track('event', {
        event: 'pageClick',
        // ...
      });
    }
  });

Reporter

这一部分的其实就是封装一个申请办法,将咱们追踪到的信息发送到服务端。
不过这里咱们有几个点须要考虑一下:

  1. 埋点的网络申请不应该去抢占原始事件的申请,即应该先发送业务申请
  2. 埋点信息发送的时候应该保障程序才会更利于剖析,就比方小程序 show 的信息就应该在 hide 之后去发送
  3. 网络存在稳定的时候,如果埋点信息发送失败咱们应该缓存该数据,期待下一次发送,保障信息的完整性
import store from '../store';
import {storage} from '../../utils';
import platform from '../platform';
import qs from 'qs';

class Reporter {constructor() {
    // 须要发送的追踪信息的队列
    this.queue = [];
    this.timerId;
  }
  /**
   * 追踪埋点数据
   * @param {*} data 须要上报的数据
   */
  track(type, data = {}) {
    // 增加一些公共信息字段
    data.t = type;

    this.queue.push(qs.stringify(data));

    if (!this.timerId) {
      // 为了不影响失常的业务申请,这里延时收回咱们的埋点信息
      this.timerId = setTimeout(() => {this._flush();
      }, store.get('config').delay);
    }
  }
  /**
   * 执行队列中的工作(向后盾发送追踪信息)
   */
  _flush() {const config = store.get('config');

    // 队列中有数据时进行申请
    if (this.queue.length > 0) {const data = this.queue.shift();
      platform.request({
        // 申请地址
        url: config.url,
        // 超时工夫
        timeout: config.request_timeout,
        method: 'POST',
        header: {'content-type': 'application/x-www-form-urlencoded'},
        data: {
          ak: config.ak,
          cid: storage.get('cid'),
          ns: store.get('networkType'),
          uid: storage.get('uid') || 0,
          data: Date.now(),
          data,
        },
        // TODO 发送失败的时候将该次信息保留的 storage 中
        success: () => {},
        fail: ({errMsg}) => {console.error(errMsg);
        },
        complete: () => {
          // 执行实现后发送下一个信息
          this._flush();},
      });
    } else {this.timerId = null;}
  }
}

export default new Reporter();

总结

本文只是对小程序埋点计划的一个简略实现,从整体框架上动手去形容的,很多细节都没有波及到,大家有什么问题能够一起探讨。

本文始发于我的公众号。大家能够扫码关注一下,平时会发一些奇奇怪怪的货色 hh

正文完
 0