使用babel实现前端埋点
埋点是一个常见的需求,就是在函数里面上报一些信息。像一些性能的埋点,每个函数都要处理,很繁琐。能不能自动埋点呢?
答案是可以的。埋点只是在函数里面插入了一段代码,这段代码不影响其他逻辑,这种函数插入不影响逻辑的代码的手段叫做函数插桩。
我们可以基于 babel 来实现自动的函数插桩,在这里就是自动的埋点。
思路分析
比如antd form校验方法转换代码:
const submit = () => {
this.props.form.validateFields().then(value => {
//##$tracker(this.props) //转换前的代码
dispath({
type: "home/login",
payload: value
});
}).catch(err => {
message.error(err);
});
};
我们要实现埋点就是要转成这样:
const submit = () => {
this.props.form.validateFields().then(value => {
tracker(this.props) //转换后的代码
dispath({
type: "home/login",
payload: value
});
}).catch(err => {
message.error(err);
});
};
有两方面的事情要做:
引入 tracker 模块。如果已经引入过就不引入,没有的话就引入,并且生成个唯一 id 作为标识符
对所有函数进行遍历,寻找我们的注释插槽##$
,然后插入 tracker 的代码
代码实现代码放在了这里
模块引入 引入模块这种功能显然很多插件都需要,这种插件之间的公共函数会放在 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 的方式不一样,需要分别处理下。
我们把 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 函数埋点
函数埋点要找到对应的函数,以ArrowFunctionExpression
为例
ArrowFunctionExpression(path) {
let parentName = path.parentPath.node.callee && path.parentPath.node.callee.property.name == 'validateFields';
if (parentName) {
let con = path.node.body.body[0].consequent,
alt = path.node.body.body[0].alternate;
let comment = con.body[0].leadingComments;
if (comment.length) {
let newComment = comment.map(item => generateCommentNode(item));
con.body.unshift(...newComment);
con.body.map(i => delete i.leadingComments);
}
let { params } = path.node;
const propsTemp = props_temp({
FP: t.arrowFunctionExpression([params[1]], con),
EP: t.arrowFunctionExpression([params[0]], alt)
})
path.parentPath.replaceWithMultiple(propsTemp)
}
}
const generateCommentNode = (node) => {
if (node.value.startsWith('##$')) {
let value = node.value.substring(3);
let begin = value.indexOf('(');
let end = value.indexOf(')');
let arg = value.substring(begin + 1, end);
let oldArg = value.substring(begin, end + 1);
value = value.replace(oldArg, '')
let args = t.identifier(arg);
let comment = t.callExpression(t.identifier(value), [args])
return comment;
}
}
这样我们就实现了自动埋点。