💡黑科技::小程序自动埋点的一个思路

211 阅读1分钟

黑科技::小程序自动埋点

答题思路:

  1. 通过标签 data-log-* 属性声明 埋点key&data
  2. 为标签添加 bind/catch/mut-bindtap="__logtap" 方法
  3. 重写 Page/Component 函数, 给所有 Page/Component 实例注入 __logtap 方法
  4. 处理原来的 tap 方法(如有), 这也是为什么不能在全局, 而是必须要在实例上注入方法的原因, 因为原始方法里面大概率会用到 this
  5. 写个 插件来处理 上述标签处理的转化
  6. 正则我不行, 借助 @vue/dom-compiler 将 wxml -> ast, 这样能够将标签的处理精确到 单独节点上
  7. 感谢vue
  8. 理论上讲, 将 logKey 转换成 class, 添加 wx.createIntersectionObserver 方法, 应该也能处理掉节点是否展示的埋点, 等弄完贴过来, 留个TODO 吧
  9. 理论上讲也适用于 vue

input -> output

input

<view
  logtap
  bindtap="原始的tap方法"
  bind:tap="bind:tap也行"
  catchtap="catchtap也行"
  mut-bindtap="mut-bindtap也行"
  data-log-key="logkey"
  data-log-key="{{表达式也可以}}"
  data-log-data="字符串, 或者"
  data-log-data="{{ {表达式: '也可以' } }}"
  ...
>
  你的内容
  <view
    logtap
    data-log-key="一定要有 logtap 和 data-log-key 两个属性,不然会被忽略"
  />
    支持嵌套
  </view>
</view>

output

<view
  data-tap-fn="原始的tap方法"
  bindtap="__logtap"
  data-log-key="logkey"
  data-log-data="{{可以表达式取值}}"
>
  你的内容
  <view data-log-key="一定要有 logtap 和 data-log-key 两个属性,不然会被忽略" />
  支持嵌套
</view>

其中, __logtap 方法, 通过 src/third-utils/adaptor 中注入, 详情看源码

const { logger } = require('./log/logger');

const autoAdaptorPage = () => {
  const oPage = Page;
  function XPage(config) {

    function logTap(e) {
      const { tapFn: tapFnName, logKey: logTapKey, logData } = e.currentTarget.dataset || {};
      if (!logTapKey) return;
      if (tapFnName && this[tapFnName]) {
        this[tapFnName](e);
      }
      console.log('send tap', logTapKey, logData);
      logger.send(logTapKey, logData, { event_type: 'click' });
    }

    config.__logtap = logTap;
  
    return oPage(config);
  }

  Page = XPage;
};

const autoAdaptorComponent = () => {
  const oComp = Component;

  function XComp(config) {
    function logTap(e) {
      const { tapFn: tapFnName, logKey: logaTapKey, logData } = e.currentTarget.dataset || {};
      if (tapFnName && this[tapFnName]) {
        this[tapFnName](e);
      }
      console.log('send log', logTapKey, logData);
      logger.send(logTapKey, logData, { event_type: 'click' });
    }

    config.methods = config.methods || {};
    config.methods.__logtap = logTap;
    return oComp(config);
  }

  Component = XComp;
};

export const autoAdaptor = at => {
  console.log('auto adaptor...', at);
  autoAdaptorPage();
  autoAdaptorComponent();
};
autoAdaptor();

不要忘了在 app.js 中引入

webpack 配置

// webpack config loaders
...
 {
    test: /\.wxml$/,
    use: [
      fileLoader('[path][name].[ext]'),
      path.resolve(__dirname, './wxml-autolog-loader.js'),
      'mini-program-webpack-loader'
    ]
  }
...

wxml-autolog-loader.js 源码

留神一下嵌套的处理

// wxml-autolog-laoder.js
const compiler = require('@vue/compiler-dom');

const visitor = (node, list = []) => {
  const haslog =
    node.props &&
    node.props.find(x => x.name == 'logtap') &&
    node.props.find(x => x.name === 'data-log-key');

  if (haslog) {
    const hastap = ['bind', 'bind:', 'catch', 'catch:', 'mut-bind', 'mut-bind:'].find(prefix =>
      node.props.find(p => p.name === prefix + 'tap')
    );
    let replaceto = 'bindtap="__logtap"';
    let oreplace = '';
    if (hastap) {
      const ofn = node.props.find(x => x.name == hastap + 'tap');
      const fnName = ofn.value.content;
      replaceto = `data-tap-fn="${fnName}" ${hastap}tap="__logtap"`;
      oreplace = `${hastap}tap="${fnName}"`;
    }
    const source = node.loc.source;
    let neo = source.replace('logtap="{{ true }}"', replaceto);
    if (oreplace) {
      neo = neo.replace(oreplace, '');
    }
    list.unshift({ neo, old: source });
  }
  if (node.children) {
    node.children.forEach(item => {
      visitor(item, list);
    });
  }
  return list;
}; //loader函数
module.exports = function(content) {
  const ast = compiler.parse(content);
  // console.log('ast', ast);
  const replaceList = visitor(ast);
  let neo = content;
  replaceList.forEach((item, idx) => {
    neo = neo.replace(item.old, item.neo);
    // 处理嵌套
    replaceList.forEach((iitem, iidx) => {
      if (iidx <= idx) return;
      iitem.old.replace(item.old, item.neo);
      iitem.neo.replace(item.old, item.neo);
    });
  });
  if (replaceList.length > 0) {
    // console.log('wxml log content', { content, neo });
    console.log('ohhhh');
  }
  return neo;
};