vue埋点 减少代码侵入思考

767 阅读2分钟

先看一个传统在vue项目中的埋点例子

点击菜单栏中的某个菜单,发送埋点。

methods: {
  // 点击某一个菜单
  clickMenu(menuData) {
    const { name, id } = menuData;
    const customData = {
      MenuId: id,
      MenuName: name
    };
    // 封装的埋点方法,第一个参数是场景名称,第2个参数是自定义数据
    this.$track('HomeMenuClick', customData);
    
    // 记录用户最近点击,然后跳转到对应页面
    this.addMenuToRecent(menuData).then(() => {
      this.goMenuDetailPage(menuData);
    });
  }
}

以上代码中点击菜单的逻辑就是记录用户最近点击的菜单并访问菜单页面。

前面的一大段都是埋点的逻辑(组装自定义数据),由此可见埋点对业务代码的侵入,影响代码阅读。

思考:如何降低侵入性,增强代码可读性。

我想到的方案是:使用注释。

methods: {
  // 点击某一个菜单
  clickMenu(menuData) {
    /**! @track
      const { id, name } = menuData;
      const customData = {
        MenuId: id,
        MenuName: name
      };
      // 封装的埋点方法,第一个参数是场景名称,第2个参数是自定义数据
      this.$track('HomeMenuClick', customData);
    */
    
    // 记录用户最近点击,然后跳转到对应页面
    this.addMenuToRecent(menuData).then(() => {
      this.goMenuDetailPage(menuData);
    });
  }
}

埋点的代码变成了注释,代码主流程一目了然。

如何实现

封装一个track函数。

methods: {
  clickMenu: track(function(menuData) {
    // ...
  });
}

track函数的作用,返回一个新函数,新函数需要执行注释埋点的代码和主流程代码。

// 埋点注释开始
const trackNoteBeginRE = /[^\S|\n]*\/\*\*\! @track[^\n]*\n/g;
// 埋点注释结束
const trackNoteEndRE = /\s*\/\//;

function track(fn) {
  return function(...args) {
    // 拿到原方法的代码(包含埋点注释)
    let code = fn.toString();
    // 还原埋点注释代码
    let r;
    while (r = trackNoteBeginRE.exec(code)) {
      code = code.slice(0, r.index) + 
        code.slice(r.index + r[0].length).replace(trackNoteEndRE, '');
    }
    fn = evel('(' + code + ')');
    return fn.call(this, ...args);
  }
}

仅仅这样吗?

使用注释的形式,虽然代码可读性增加了;但是代码量一点没少,而且写法更加繁琐,还有可能被当做注释代码被删除。如何更进一步?

结合埋点配置文件。

为了方便管理埋点需求,一般会有一个埋点配置文件(全局或页面级别),会配置埋点需要上送的自定义数据,可以对数据字段和字段类型进行校验。

一个页面级别的埋点配置文件。

Home 页面。

export default {
  CLICK: {
    HomeMenu: {
      name: '首页-菜单-点击',
      customData: ['MenuId', 'MenuName']
    }
  }
};

既然埋点配置文件中已经确定了需要上送的自定义字段,那么发送埋点请求时,我们就可以根据配置文件自动构建customData了。还是交给track函数。

methods: {
  // 点击某一个菜单
  clickMenu: track(function(menuData) {
    /**! @track
      const { id: MenuId, name: MenuName } = menuData;
      #track(CLICK.HomeMenu);
    */
    
    // 记录用户最近点击,然后跳转到对应页面
    this.addMenuToRecent(menuData).then(() => {
      this.goMenuDetailPage(menuData);
    });
  })
}

设置一个标志位:#track(CLICK.HomeMenu),在track函数中找到这个标志位,将其替换为this.$track(trackConfig.CLICK.HomeMenu, { MenuId, MenuName })

track函数功能扩展

// #track标志位
const trackFlagRE = /#track\(([\w\.]+)\)/g;

// 埋点注释开始
const trackNoteBeginRE = /[^\S|\n]*\/\*\*\! @track[^\n]*\n/g;
// 埋点注释结束
const trackNoteEndRE = /\s*\/\//;

function track(fn) {
  return function(...args) {
    // 拿到原方法的代码(包含埋点注释)
    let code = fn.toString();
    
    // 替换所有#track标志位
    code = code.repalce(trackFlagRE, function(_, configPath) {
      const segs = configPath.split('.');
      const config = segs.reduce((conf, seg) => conf[seg], trackConfig);
      const { customData } = config;
      // this.$track('CLICK.HomeMenu', { MenuId, MenuName })
      return `this.$track('${configPath}', {${customData.join(',')}})`
    });
    
    // 还原埋点注释代码
    let r;
    while (r = trackNoteBeginRE.exec(code)) {
      code = code.slice(0, r.index) + 
        code.slice(r.index + r[0].length).replace(trackNoteEndRE, '');
    }
    fn = evel('(' + code + ')');
    return fn.call(this, ...args);
  }
}

更多思考

  1. track函数的功能放在vue脚手架层级,打包时自动替换原代码。
  2. 如何防止被当做注释删除?track函数包装的方法中必须有track埋点注释,否则报错(多个注释时,不能保证。修改方法为不包含埋点的方法时,必须去掉track修饰)。