埋点是一个常见的需求,就是在函数里面上报一些信息。像一些性能的埋点,每一个函数都要处理,很繁琐。能不能自动埋点呢?javascript
答案是能够的。埋点只是在函数里面插入了一段代码,这段代码不影响其余逻辑,这种函数插入不影响逻辑的代码的手段叫作函数插桩。java
咱们能够基于 babel 来实现自动的函数插桩,在这里就是自动的埋点。node
好比这样一段代码:git
import aa from 'aa'; import * as bb from 'bb'; import {cc} from 'cc'; import 'dd'; function a () { console.log('aaa'); } class B { bb() { return 'bbb'; } } const c = () => 'ccc'; const d = function () { console.log('ddd'); }
咱们要实现埋点就是要转成这样:github
import _tracker2 from "tracker"; import aa from 'aa'; import * as bb from 'bb'; import { cc } from 'cc'; import 'dd'; function a() { _tracker2(); console.log('aaa'); } class B { bb() { _tracker2(); return 'bbb'; } } const c = () => { _tracker2(); return 'ccc'; }; const d = function () { _tracker2(); console.log('ddd'); };
有两方面的事情要作:api
小册《babel 插件通关秘籍》中有具体 api 的详细介绍。babel
引入模块这种功能显然不少插件都须要,这种插件之间的公共函数会放在 helper,这里咱们使用 @babel/helper-module-imports。函数
const importModule = require('@babel/helper-module-imports'); // 省略一些代码 importModule.addDefault(path, 'tracker',{ nameHint: path.scope.generateUid('tracker') })
首先要判断是否被引入过:在 Program 根结点里经过 path.traverse 来遍历 ImportDeclaration,若是引入了 tracker 模块,就记录 id 到 state,并用 path.stop 来终止后续遍历;没有就引入 tracker 模块,用 generateUid 生成惟一 id,而后放到 state。性能
固然 default import 和 namespace import 取 id 的方式不同,须要分别处理下。ui
咱们把 tracker 模块名做为参数传入,经过 options.trackerPath 来取。
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();// tracker 模块的 id } path.stop();// 找到了就终止遍历 } } }); if (!state.trackerImportId) { state.trackerImportId = importModule.addDefault(path, 'tracker',{ nameHint: path.scope.generateUid('tracker') }).name; // tracker 模块的 id state.trackerAST = api.template.statement(`${state.trackerImportId}()`)();// 埋点代码的 AST } } }
咱们在记录 tracker 模块的 id 的时候,也生成调用 tracker 模块的 AST,使用 template.statement.
函数插桩要找到对应的函数,这里要处理的有:ClassMethod、ArrowFunctionExpression、FunctionExpression、FunctionDeclaration 这些节点。
固然有的函数没有函数体,这种要包装一下,而后修改下 return 值。若是有函数体,就直接在开始插入就好了。
'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) { const bodyPath = path.get('body'); if (bodyPath.isBlockStatement()) { // 有函数体就在开始插入埋点代码 bodyPath.node.body.unshift(state.trackerAST); } else { // 没有函数体要包裹一下,处理下返回值 const ast = api.template.statement(`{${state.trackerImportId}();return PREV_BODY;}`)({PREV_BODY: bodyPath.node}); bodyPath.replaceWith(ast); } }
这样咱们就实现了自动埋点。
咱们来试下效果:
const { transformFromAstSync } = require('@babel/core'); const parser = require('@babel/parser'); const autoTrackPlugin = require('./plugin/auto-track-plugin'); const fs = require('fs'); const path = require('path'); const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.js'), { encoding: 'utf-8' }); const ast = parser.parse(sourceCode, { sourceType: 'unambiguous' }); const { code } = transformFromAstSync(ast, sourceCode, { plugins: [[autoTrackPlugin, { trackerPath: 'tracker' }]] }); console.log(code);
效果以下:
咱们实现了自动埋点!
上面实现的是一种状况,实际上可能有的函数不须要埋点,这种能够本身作一下过滤,或者在函数上写上注释,而后根据注释来过滤,就像 eslint 支持 / eslint-disable / 的设置同样。关于注释的操做,能够看另外一个案例《自动生成 API 文档》。
函数插桩是在函数中插入一段逻辑但不影响函数本来逻辑,埋点就是一种常见的函数插桩,咱们彻底能够用 babel 来自动作。
实现思路分为引入 tracker 模块和函数插桩两部分:
引入 tracker 模块须要判断 ImportDeclaration 是否包含了 tracker 模块,没有的话就用 @babel/helper-module-import 来引入。
函数插桩就是在函数体开始插入一段代码,若是没有函数体,须要包装一层,而且处理下返回值。、
代码在这里,建议本身实现一遍。
详见小册《babel 插件通关秘籍》。