事情是这样子的,就在前几天。我的代码跑得好好的,结果发到test上后测试同志跟我提XX功能有个bug。我第一想法就是不可能,怎么会呢缓存的视频在哪,我有自测过的,肯定是电脑问题,缓存问题,建议刷新一遍。

如何使用自定义Loader提升埋点代码的鲁棒性

两个钟头后,测试同学反电脑馈还是有问题。不愧是头发都没了一半的测试大哥,总能发现我们发缓存英文现不了的问题。经过一番操作后发现,问题的根测试仪源在于test电脑截图上的代码和我当前分支的有点不同,在test分支上加上了数据埋点相关的功能,而正是因为这个埋点,导致了相关的报错Webpack

如何使用自定义Loader提升埋点代码的鲁棒性

这题简单,直接修改报错的埋点代码就可以了。等等,下次哪位兄测试仪弟又在我们的代码中插入了埋点怎么办? 于是乎假装大佬在群里发个话
如何使用自定义Loader提升埋点代码的鲁棒性
没错,把埋点放在函数的底部可以电脑黑屏却开着机减少这种情况的发生。比如有个缓存视频在手机哪里找点击提交事件如下

function submit(object) {
  this.logger.log('submit', object.keys.length)
  console.log('do submit', object.other)
}

如果埋点放到了前面,那么当object的k测试英文eys不存在时,我们后续的代码也会发生阻断。这显然是不符合常理的,因为埋点就是缓存英文埋点,它不应该影响系统功能的正常使用。所以当我们把埋点放到底部时我们就可以确保提交功能不会因为埋点而发生报错。

但是问题来了,有时候一个操作的响应链路非常的长,所以为了方便我们必须得在函数的中间插入数据埋点,否则导致临时变量过多导致维护困难,这缓存英文时候我们就必须得使用try catch以防埋点错误引发功能异常。

function submit(object) {
  try {
    this.logger.log('submit', object.keys.length)
  } catch(error) {
    console.log('log error ', error.message)
  }
  console.log('do submit', object.other)
}

这样做解决了埋点报错的问题,但是我们需要对每个log都使用try catch,那有没有办法可以不管它,但是又像try catch一样不会引发功能异常呢。
那就是使用loader让它在编译时自动给埋点加上一个try catch不就好了。

如何使用自定义Loader提升埋点代码的鲁棒性

首先,我们大概来定一个流程

  1. 新建一个loader,导出转换函数
  2. 通过babel对源文件进行遍历
  3. 找到log的位置并使用try catch包裹
  4. 生成新的代码

loader和babel的基本原理我们都知道了(不知道的自己翻一下)。接着我们要做的就是通过ASbabel巴别塔T explorer来对源代码进行分析
没有加try catch

如何使用自定义Loader提升埋点代码的鲁棒性

加了try catchwebpack官网中文
如何使用自定义Loader提升埋点代码的鲁棒性
通过上面的AST比较我们得知,需要找到一个FunctionDeclaration,然后遍历body,然后遍历b缓存视频怎样转入相册ody将对应的Expressio测试仪nStatement转换成TryStatement即可

const LogCatchLoader = function (content, sourceMap, meta){
  const ast = parser.parse(content); // 将代码转换成为ast树
  const { callerName = 'logger', callFnName = 'log', catchAction = 'console.log(error.message)' } = this.query
  traverse(ast,{
    //遍历函数表达式
    FunctionDeclaration(functionPath){
      const functionNode = functionPath.node;
      const bodyResult = []
      for (let statement of functionNode.body.body) {
        let isLoggerCall = false
        // 如果是调用logger则直接将它转换成tryStatement
        if (isLoggerCall) {
          const tryStatement = transform(statement)
          bodyResult.push(tryStatement)
        } else {
          bodyResult.push(statement)
        }
      }
      const func = transform2NewFn();
      // 最终生成新的函数
      functionPath.replaceWithMultiple(func);
    }
  })
  //将目标ast转化成代码
  const code = generate(ast).code
  this.callback(null, code, sourceMap, meta);
  return;
}

通过展开AST我们可以找到什么情况需要对其进行转换

如何使用自定义Loader提升埋点代码的鲁棒性

const isLoggerCall = t.isExpressionStatement(statement) &&
  t.isCallExpression(statement.expression) &&
  t.isMemberExpression(statement.expression.callee) &&
  statement.expression.callee.property.name === callFnName &&
  t.isMemberExpression(statement.expression.callee.object) &&
  statement.expression.callee.object.property.name === callerName

接着我电脑黑屏却开着机们在webpack配置了对应的loader并且打包后发现,确实有增加了对应的try catch,接下来就可以安心地调用logger埋点了。
完整代码缓存清理

const LogCatchLoader = function (content, sourceMap, meta){
  const ast = parser.parse(content); // 将代码转换成为ast树
  const { callerName = 'logger', callFnName = 'log', catchAction = 'console.log(error)' } = this.query
  traverse(ast,{
    //遍历函数表达式
    FunctionDeclaration(functionPath){
      const functionNode = functionPath.node;
      const bodyResult = []
      let isCatch = false
      for (let statement of functionNode.body.body) {
        const isLoggerCall = t.isExpressionStatement(statement) &&
          t.isCallExpression(statement.expression) &&
          t.isMemberExpression(statement.expression.callee) &&
          statement.expression.callee.property.name === callFnName &&
          t.isMemberExpression(statement.expression.callee.object) &&
          statement.expression.callee.object.property.name === callerName
        // 如果是调用logger则直接将它转换成tryStatement  
        if (isLoggerCall) {
          const catchStatement = template.statement(catchAction)();
          const catchClause = t.catchClause(
            t.identifier('error'),
            t.blockStatement([catchStatement])
          );
          // 使用旧的statement生成try block
          const tryBlock = t.blockStatement([statement])
          const tryStatement = t.tryStatement(tryBlock, catchClause);
          bodyResult.push(tryStatement)
          isCatch = true
        } else {
          bodyResult.push(statement)
        }
      }
      if (!isCatch) return
      // 生成新的函数表达式并替换
      const func = t.functionExpression(
        functionNode.id,
        functionNode.params,
        t.blockStatement(bodyResult),
        functionNode.generator,
        functionNode.async
      );
      functionPath.replaceWithMultiple(func);
    }
  })
  //将目标ast转化成代码
  const code = generate(ast).code
  this.callback(null, code, sourceMap, meta);
  return;
}

当然,以上方法只适用户于一些应用比较电脑快捷键简单的场景,实际开发中还是推荐大家普遍地Webpack按照规范来进行埋点以免影响到功能的正常使用。