使用webpack loader 。实现注释埋点

  • 网上有很多关于loader 实现埋点的dome。但真正用于业务一个dome 往往是不够的,还需要我们考虑很多实际场景。接下来这篇文章就是我应用于项目中的一个埋点loader,实际应用场景坑还是非常多。

转换规则是什么

  • 首先不使用loader,是这样埋点的。方法也毕竟简单
  • 从 @/services 里面 引入 buriedPoint 方法
  • 然后在某些特定事件下触发 buriedPoint 函数发起请求。
import { buriedPoint } from '@/services'
const Index = () => {
	// ...
	return (
		<View onClick={ () => buriedPoint({ code: 'A0001' }) }>点击埋点</View>
	)
}
  • import { buriedPoint } from ‘@/services’ 需要loader 自动导入
  • 特殊注释,转换为 buriedPoint({ code: ’ xxx ’ }) 的形式
// ! code-A0009
buriedPoint({ code: 'A0009' })

从 @/services 里面 导入 buriedPoint 方法

在这里插入图片描述

  • ImportDeclaration:它表示一个ES6模块的导入声明。
  • 循环ast 上的导入声明
traverse(ast, {
	// 是否有导入方法
    let isImport = false
    // 是否导入名字
    let isImportName = false
	// 当前节点的名字
    let specifierNode = null
   ImportDeclaration(path) {
     pathNode = path
     // 有当前库的顶级导入
     if(path.node.source.value === "@/services") {
       isImport = true
       // 查找是否有方法导入
       for (const specifier of path.node.specifiers) {
         if(specifier.local.name === "buriedPoint") {
           isImportName = true
           break;
         }
       }
       // 保留当前 specifier 的引用
       specifierNode = path.node.specifiers
     }
   }
 })
  • path.node.source.value 拿到所有导入节点的导入路径,判断是否有 === “@/services”
  • 如果有导入这个模块,再判断 specifier.local.name === “buriedPoint” 是否导入有当前方法
    在这里插入图片描述
    得到三个结论:没有导入模块;导入了模块,但是没有导入方法;导入模块也导入了方法(不管)
  • 没有导入模块
	// 新增的创建一个 import 节点的方法
  const createImport = () => {
    const importNode = t.importDeclaration(
      [t.importSpecifier(t.identifier("@/services"), t.identifier("buriedPoint"))],
      t.stringLiteral("@/services")
    )
    return importNode
  }
  
  // ... 接着上面的代码
  // 没有导入,就自动导入方法
  if(!isImport && pathNode) {
    const importNode = createImport()
    pathNode.insertBefore(importNode)
  }
  • 导入了模块,但是没有导入方法
 	// 导入了文件,没有导入名字,那么单独导入名字
    if(isImport && !isImportName && specifierNode) {
      const item = t.importSpecifier(t.identifier("buriedPoint"), t.identifier("buriedPoint"))
      specifierNode.push(item)
    }

转换特殊注释

// 是否要导入import 方法,如果没有要转换的注释就不导入
let isCheck = false
traverse(ast, {
    enter(path) {
    	// 代码前的注释
      const leadingComments = path.node.leadingComments
      // 代码后的注释
      const trailingComments = path.node.trailingComments
     
      if (leadingComments) {
        insertFunction( leadingComments, isCheck, path, 'before', ast)
      }

      if(trailingComments) {
        insertFunction(trailingComments, isCheck, path, 'after', ast)
      }
    
    },
  })
  
  // 创建一个函数表达式
  const createFunExpression = (code) => {
    // 函数参数是一个对象
    const obj = t.objectExpression([
      t.objectProperty(t.stringLiteral('event_id'), t.stringLiteral(code))
    ])
    const fun = t.callExpression(
      t.identifier("buriedPoint"),
      [obj]
    )
    return fun
  }
  
const insertFunction = (comments, isCheck, path, type, ast) => {
    comments.forEach(value => {
      const v = value.value.trim()
      // 判断该注释,是否是我们要替换的埋点注释
      if(
        v.startsWith('!code-') ||
        v.startsWith('! code-') ||
        v.startsWith('!code- ') ||
        v.startsWith('!code - ') ||
        v.startsWith('!code -')
      ) {
        const code = v.split('-')[1]?.trim()
        if(code) {
        	// 检查替换的方法是否导入。
          !isCheck && checkImportPoint(ast)
          isCheck = true
          if(type === 'before') {
            path.insertBefore(createFunExpression(code))
          }else {
            path.insertAfter(createFunExpression(code))
          }
        }
      }
    })
  }
  • 到这里,基本上已经完成了。但是还有个细节需要处理,就是这样的一个场景。如果当前文件已经转换了一次,进行第二次转换,那是不是每个注释下面都会多一个方法。每多执行一遍,就会多出一个方法。
  • 找到函数表达式节点 === buriedPoint 的函数
  • 然后再找出函数上下的注释节点
  • 判断注释节点的值是不是 和 buriedPoint({ code: ‘’}) 的code值一样
  • 如果不一样就把 当前注释放入 hasFunctionList
  • 在注释 traverse 哪里做一次判断
 let hasFunctionList = []
 traverse(ast, {
    ExpressionStatement(path) {
      const exp = path.node.expression
      // 名字为 buriedPoint 的函数,判断该函数的剩下注释是否有和 自己一样的code,有存入数组,插入的时候就不插入了
      if(exp.type === 'CallExpression' && exp.callee.name === "buriedPoint") {
        const fnNode = path.node.expression
        const p = fnNode.arguments[0].properties[0].value.value
        const comments = [...(path.node.leadingComments || []), ...(path.node.trailingComments || [])]
        for(let com of comments) {
          if(com.value.includes(p)) {
            hasFunctionList.push(com)
          }
        }
      }
    },
  })

修改转换特殊注释

let hasFunctionList = []
// 对 hasFunctionList 这里面的 注释进行判断
const isEqualComment = (oldComment, newComment) => {
   const curComment = newComment.filter(com => {
     return !oldComment.some(subCom => {
       return subCom.start === com.start && subCom.end === com.end && subCom.value === com.value
     })
   })
   return curComment
 }

traverse(ast, {
    enter(path) {
      const leadingComments = path.node.leadingComments
      const trailingComments = path.node.trailingComments
     
      if (leadingComments) {
        insertFunction( isEqualComment(hasFunctionList, leadingComments), isCheck, path, 'before', ast)
      }

      if(trailingComments) {
        insertFunction(isEqualComment(hasFunctionList, trailingComments), isCheck, path, 'after', ast)
      }
    },
  })
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值