写点什么

基于 babel 的埋点工具简单实现及思考

  • 2022 年 2 月 10 日
  • 本文字数:4547 字

    阅读完需:约 15 分钟

相关知识点什么是 AST 抽象语法树程序的编译过程 AST 的用途 Babel 的原理个人实现的基于 babel 的埋点实例及思考



什么是 AST 抽象语法树程序的编译过程什么是程序的编译呢?我们都知道,在传统的编译语言流程中,程序中的一段代码在它被执行之前都会经历三个步骤,这个步骤的执行过程也就是程序的编译过程。


分词(词法分析)词法分析的过程也就是第一步,我们写的代码本质上就是一串串字符串,而词法分析这个过程则会把这些由字符组成的字符串去分解成有意义的代码块。比如:


 let a = 1 // let   a   =   1复制代码
复制代码


在这个程序中就会把 let、 a、 =、 1、 拆分开来,对于某些特殊占位符(如空格)是否需要拆分则会取决于这个占位符是否有实际的意义。


解析(语法分析)语法分析的过程就是将词法分析后的结果按照一定的规则进行组合,将散列的代码块进行关联并形成一个代表程序语法结构的树,也被称为是抽象语法树(AST)。抽象语法树是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,抽象表示把 js 代码进行了结构化的转化,转化为一种数据结构。这种数据结构其实就是一个大的 json 对象,json 我们都熟悉,他就像一颗枝繁叶茂的树。有树根,有树干,有树枝,有树叶,无论多小多大,都是一棵完整的树。简单理解,就是把我们写的代码按照一定的规则转换成一种树形结构如:



具体的 ast 内容大家也可以通过这里自行去输入查看,另外,对于这个工具来说 分别有选择语言以及拆解工具的地方,大家也可以根据自己的语言去选择相应的环境




代码生成代码生成环节也是编译过程的最后一节,它会将语法分析阶段的 AST 抽象语法树转换为可执行的代码,然后在交换个我们。至于生成什么样的代码,也是可以由我们自己去决定的,理论上符合语言的规则就可以。


AST 的用途在了解了什么是 AST 之后,关于 AST 的用途有哪些,想必我们心中都有了一定的答案。AST 的作用不仅仅是用来在 JavaScript 引擎的编译上,我们在实际的开发过程中也是经常使用的,比如我们常用的 babel 插件将 ES6 转化成 ES5、使用 UglifyJS 来压缩代码 、css 预处理器、开发 WebPack 插件、Vue-cli 前端自动化工具等等,这些底层原理都是基于 AST 来实现的,AST 能力十分强大, 能够帮助开发者理解 JavaScript 这门语言的精髓。有了这些,我们可以准确的操控代码的运行时以及编译时的相关处理。


例如:大傻之前在逛 GIthub 时候偶然发现了关于 Vue3.x 的 issue。 具体情况是这样的,在使用 Vue3.0 时候,猛然间发现了如果使用 jsx 写法会导致有些例如 v-once 这些不支持,不支持怎么办呢?百度谷歌搜起来,搜完后觉得还没明白就来到了 issue,这里有个思路,因为是 jsx 语法,所以我们去的肯定不是 Vue 的 issue,肯定是转换工具的,在这里我们用的是 babel-plugin-jsx,并且发现了如下 issue



在一番激烈的狡(交)辩(流)后,大傻输了,静下心发现了在代码编译的这两处(1,2)并没有对 v-once 等一些指令做相应的处理。


这个小例子,也说明了 AST 扮演的角色,比如我们在某些报错后怀疑是某个库或者框架的错误,其实也有可能是在编译阶段由于规则不一致或者没提供暴露的错误。如果我们能准确分析出来错误原因,那么妈妈再也不用担心我乱提 issue 了。



Babel 的原理随着前端工程化的兴起,让我们接触了更多的语言工具,babel 在这就是一种特有的工具。


我们通常对 babel 的理解就是它可以帮助我们去处理兼容性,也就是有些 JavaScript 的新特性,可能我们想去使用,但对于某些浏览器来说还并未支持,此时我们就可以通过 babel 将我们的代码降级处理为浏览器兼容的执行版本从而达到开发和生产环境两套代码,一次操作的便捷开发。


Babel 插件就是作用于抽象语法树 Babel 三个主要的处理步骤就是解析(parse),转换(transform),生成(generate)。解析解析就相当于我们的编译过程中的词法分析和语法分析的结合版,将代码解析成抽象语法树(AST),每个 js 引擎(比如 Chrome 浏览器中的 V8 引擎)都有自己的 AST 解析器,而 Babel 是通过 Babylon 实现的。解析过程有两个阶段:词法分析和语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于 AST 中节点;而语法分析阶段则会把一个令牌流转换成 AST 的形式,同时这个阶段会把令牌中的信息转换成 AST 的表述结构。


转换转换这个步骤一般来说就是暴漏给我们的处理步骤,将在此阶段对节点进行添加、更新以及移除操作。通过 traverse 进行深度优先遍历,维护 AST 树的整体状态,并且可完成对其的替换、删除或者增加节点。返回的结果就是我们处理后的 AST。


生成生成阶段就是将我们二阶段的最终 AST 进行转换,转换成我们的字符串形式的代码,并且创建代码映射,也就是 source-map。代码生成就是,先对整个 AST 进行深度遍历,再通过 generate 转换为可以表示转换后代码的字符串。



个人实现的基于 babel 的埋点实例及思考我们一般埋点时候都是通过函数的形式,传入指定参数进而实现埋点,那么我们在开发过程中如果对需要埋点的地方给一些特殊标识(在这我用的是 console.log),那么当我们代码在执行前是不是可以通过工具去批量化的处理这些埋点的地方进而实现统一埋点.整个流程建议大家参考前面的 ast 生成器网站去边看边写


首先是 tacker.js,这个文件主要就是对我们源代码生成 AST 后的 AST 进行处理,主要两个方面


在此模块中导入我们的埋点函数遍历查找我们的标识区域进行替换操作


const { declare } = require('@babel/helper-plugin-utils');const importModule = require('@babel/helper-module-imports');const {default: template} = require("@babel/template");
const autoTrackPlugin = declare((api, options, dirname) => { api.assertVersion(7); // 表示是版本7
return { visitor: { Program: { enter (path, state) { path.traverse({ ImportDeclaration (curPath) { const requirePath = curPath.get('source').node.value; if (requirePath === options.trackerPath) { const specifierPath = curPath.get('specifiers.0'); if (specifierPath.isImportSpecifier()) { state.trackerImportId = specifierPath.toString(); } else if(specifierPath.isImportNamespaceSpecifier()) { state.trackerImportId = specifierPath.get('local').toString(); } path.stop(); } } }); if (!state.trackerImportId) { state.trackerImportId = importModule.addDefault(path, 'tracker',{ nameHint: path.scope.generateUid('tracker') }).name; } } }, 'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) //关于这块知识 大家可以看下官方文档把 比较多 这是为了找符合函数的AST节点{ const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`); // TODO 找子节点 const bodyPath = path.get('body'); if (bodyPath.isBlockStatement()) {// 先去找块级的作用域 const bodyPath2 = bodyPath.get('body.0') // 目前找的是第一个块级的 body内容 console.log(bodyPath.get('body').type) if(bodyPath2.isExpressionStatement()){// 这个找的是console对应的ast语句 const calleeName = bodyPath2.get('expression').get('callee').toString()// const bodyPath3 = bodyPath2.get('expression') if (targetCalleeName.includes(calleeName)) { let arg = [] bodyPath3.node.arguments.forEach((item,index,array)=>{ if(array[0].value==='tracker'){ // 如果我们console的第一个值为tracker时候 说明是埋点 否则就是我们普通的一个console.log if(index>0){ let ret = item.value || item.name arg.push(ret) } } }) if(arg.length>0){ state.trackerAST = template.expression(`${state.trackerImportId}(${arg.join(',')})`)(); bodyPath3.remove()// 移除原来的console代码 bodyPath.node.body.unshift(state.trackerAST);// 插入最新的我们自己的代码 } }
} } } } }});module.exports = autoTrackPlugin;
复制代码
复制代码


然后是我们的 startTracker.js,这个文件就是我们的入口函数,我们在本地测试时候可以通过 node startTracker.js 指令去运行这段代码,它的主要作用就是,转化为 AST 交给我们 tracker 函数去处理 AST,拿到处理后的 AST 并且生成新的代码


const { transformFromAstSync } = require('@babel/core');const  parser = require('@babel/parser');const autoTrackPlugin = require('./tracker');const fs = require('fs');const path = require('path');
const sourceCode = fs.readFileSync(path.join(__dirname, './code.js'), { encoding: 'utf-8'});
const ast = parser.parse(sourceCode, { sourceType: 'unambiguous'});
const { code } = transformFromAstSync(ast, sourceCode, { plugins: [[autoTrackPlugin, { trackerPath: 'tracker' }]] // /* * 调用函数转化 * 1 传入ast 内容 * 2 传入map ast错误问题映射到map文件中 * 3 一个对象 是配置相关 * 1 传入plugins 是一个数组 数组是不同的插件 也可以用数组标识 * 插件的数组 * 第一个是用的插件 * 第二个是对这个插件提供的配置 可以自定义的常量 一并放进接收的options中 * */});
console.log(code);复制代码
复制代码


最后就是我们的测试用的 Code 代码(目前只模拟做了一个块级作用域的内容,多个块级作用域并没有去写,大家可以看着 AST 自己完善下)


const obj={  a:111}function a () {  console.log(obj);}
class B { bb() { console.log('tracker',232) return 'bbb'; }}
const c = () => 'ccc';
const d = function () { console.log('tracker','1818',11);}复制代码
复制代码



最后 通过运行我们可以看到输出后的结果以及和源代码的对比结果.怎么样?是不是感觉很有意思. 希望大家在看过文章后有一个初步的了解,也可以找一些资料来巩固下学习下.并做一些自己的小工具来增加印象, 最后祝大家新的一年里,工作生活都虎虎生威!!!


最后

如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑


如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/defu不胜感激 !


PHP 学习手册:https://doc.crmeb.com

技术交流论坛:https://q.crmeb.com

用户头像

还未添加个人签名 2021.11.02 加入

CRMEB就是客户关系管理+营销电商系统实现公众号端、微信小程序端、H5端、APP、PC端用户账号同步,能够快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护、网络营销的一款企业应用

评论

发布
暂无评论
基于babel的埋点工具简单实现及思考