本文主要介绍了babel根据注释自动生成埋点方法的插件源码,以日常开发的埋点需求为导向,处理了埋点的常用场景。其他比较特殊的场景,也可以在此基础上进行拓展。
参考文章:juejin.cn/post/696621… 【# 神说要有光 -- 还在手动埋点么?out 了。不到百行代码实现自动埋点】
插件的主要功能有:
- 根据注释自动在函数首行添加埋点方法;
- 根据注释中的参数,向埋点代码中进行传参;
- 根据函数内的
this._trackerParam变量,可以自定义需要传入的参数,并在this._trackerParam后面添加埋点方法,并将自定义的参数传入:_tracker(this._trackerParam)。这样我们就可以自己组装生产埋点数据。
测试数据sourceCode用例(各种函数声明用例 & 传参方式)
import './index.css';
// _tracker ##函数表达式
const test0 = function () {
}
// _tracker ##箭头函数
const test1 = () => {
}
// _tracker&_trackerParam={id: 'xxx', value: 'zzz'} ##向埋点方法传参
const test2 = () => {
}
const test3 = () => {
this._trackerParam = { // ##自己组装埋点参数
id: 'xxx',
value: 'zzz'
}
}
// ## 函数内部的方法埋点
function test4 () {
// _tracker
const test4_0 = () => {
}
// _tracker
function test4_1 () {
}
}
// ## class内部的方法埋点
class bbb {
// _tracker
constructor() {
}
// _tracker
test5 = () => {
}
// _tracker
test6 = function () {
}
}
结果展示,转换后的code源码(根据我们想要的方式插入了埋点方法)
import _tracker from "tracker";
import './index.css'; // _tracker
const test0 = function () {
_tracker();
}; // _tracker
const test1 = () => {
_tracker();
}; // _tracker&_trackerParam={id: 'xxx', value: 'zzz'}
const test2 = () => {
_tracker({
id: 'xxx',
value: 'zzz'
});
};
const test3 = () => {
this._trackerParam = {
id: 'xxx',
value: 'zzz'
};
_tracker(this._trackerParam);
};
function test4() {
// _tracker
const test4_0 = () => {
_tracker();
}; // _tracker
function test4_1() {
_tracker();
}
}
class bbb {
// _tracker
constructor() {
_tracker();
} // _tracker
test5 = () => {
_tracker();
}; // _tracker
test6 = function () {
_tracker();
};
}
入口文件:
const { transformFromAstSync } = require('@babel/core');
const parser = require('@babel/parser');
const autoTrackPlugin = require('./src/auto-track-plugin');
const fs = require('fs');
const path = require('path');
// 读取 sourceCode
const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.js'), {
encoding: 'utf-8'
});
// 生成 ast
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous'
});
// 将转换结果生成 code
const { code } = transformFromAstSync(ast, sourceCode, {
plugins: [[autoTrackPlugin, {
trackerPath: 'tracker'
}]]
});
// 打印结果代码
console.log(code);
插件核心代码
const { declare } = require('@babel/helper-plugin-utils');
const { addDefault } = require('@babel/helper-module-imports');
const TACKER_FUNC_NAME = '_tracker';
const TRACKER_IMP_NAME = 'tracker';
const TRACKER_PARAM_NAME = '_trackerParam';
// 将注释中的 trackerParam 提取出来
const getTrackerParam = (trackerComment = '') => {
const isIncludeTrackerComment = trackerComment.includes(TRACKER_PARAM_NAME);
if (isIncludeTrackerComment) {
const leftTag = trackerComment.indexOf('{');
const rightTag = trackerComment.indexOf('}');
if (leftTag !== -1 && rightTag !== -1 ) {
const paramString = trackerComment.substring(leftTag, rightTag + 1);
return paramString;
}
}
return '';
}
// 生成 tracker 方法,插入到目标函数中去
const addTrackerFuncNode = (path, state, api, trackerComment) => {
const bodyPath = path.get('body');
const trackerParam = getTrackerParam(trackerComment);
const trackerAST = api.template.statement(`${state.trackerName}(${trackerParam})`)();
if (bodyPath.isBlockStatement()) {
bodyPath.node.body.unshift(trackerAST);
} else {
const ast = api.template.statement(
`{${state.trackerName}(${trackerParam});return PREV_BODY;}`
)({PREV_BODY: bodyPath.node});
bodyPath.replaceWith(ast);
}
}
// 查重注释中是否有 TACKER_FUNC_NAME,如果有,则将注释提取出来
const getTrackerCommon = (leadingComments = []) => {
const trackerCommon = leadingComments.find(item => item.value.includes(TACKER_FUNC_NAME));
return trackerCommon && trackerCommon.value;
}
const autoTrackPlugin = declare((api, options) => {
api.assertVersion(7);
return {
name: "plugin-tracker",
visitor: {
Program: {
enter(path, state) {
const isHasTrackerComments = !!path.container.comments.find(
item => item.value.includes(TACKER_FUNC_NAME)
);
if (isHasTrackerComments) {
state.isHasTrackerComments = isHasTrackerComments;
state.trackerName = TACKER_FUNC_NAME;
let hadImportTracker = false;
path.traverse({
ImportDeclaration (curPath) {
const requirePath = curPath.get('source').node.value;
if (requirePath === options.trackerPath) {
hadImportTracker = true;
}
}
});
if (!hadImportTracker) {
addDefault(path, TRACKER_IMP_NAME, { nameHint: TRACKER_IMP_NAME });
}
} else {
state.hasTrackerComments = false;
}
},
exit(path, state) {
if (!state.isHasTrackerComments && state.hasThisTrackerParam) {
addDefault(path, TRACKER_IMP_NAME, { nameHint: TRACKER_IMP_NAME });
}
}
},
// 查看函数表达式前是否有打点的注释,如果有,则将注释提取出来
'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) {
// 该文件内没有打点注释,终止后续遍历
if (state.isHasTrackerComments) {
const Comments = path.node && path.node.leadingComments;
let trackerComment = getTrackerCommon(Comments);
if (trackerComment) {
addTrackerFuncNode(path, state, api, trackerComment);
} else {
const parent = path.parent || {};
if (parent.type === 'ClassProperty') {
trackerComment = parent.leadingComments
&& getTrackerCommon(parent.leadingComments)
}
const parentNode = path.parentPath || {};
if (parentNode.type === 'VariableDeclarator') {
const ppNode = parentNode.parentPath || {};
trackerComment = ppNode.node
&& ppNode.node.leadingComments
&& getTrackerCommon(ppNode.node.leadingComments);
}
if (trackerComment) {
addTrackerFuncNode(path, state, api, trackerComment);
}
}
}
},
// 查看函数内部是否有 this._trackerParam 的对象,如果有,则在下方添加打点方法,并传入该对象
'AssignmentExpression'(path, state) {
const leftNode = path.node.left;
if (leftNode.type === 'MemberExpression'
&& leftNode.object
&& leftNode.object.type === 'ThisExpression'
&& leftNode.property.type === 'Identifier'
&& leftNode.property.name === TRACKER_PARAM_NAME
) {
state.hasThisTrackerParam = true;
const trackerAST = api.template.statement(`${TACKER_FUNC_NAME}(this.${TRACKER_PARAM_NAME})`)();
path.insertAfter(trackerAST);
}
}
}
}
});
module.exports = autoTrackPlugin;
package.json文件
{
"dependencies": {
"@babel/core": "^7.13.14",
"@babel/generator": "^7.13.9",
"@babel/helper-module-imports": "^7.14.5",
"@babel/helper-plugin-utils": "^7.14.5",
"@babel/parser": "^7.13.13",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.13.13",
"@babel/types": "^7.13.14",
"babel-plugin-tester": "^10.0.0",
"jest": "^26.6.3"
},
"devDependencies": {
"@types/jest": "^26.0.22"
}
}