【實戰(zhàn)】還在手動埋點(diǎn)么?out 了。不到百行代碼實現(xiàn)自動埋點(diǎn)
埋點(diǎn)是一個常見的需求,就是在函數(shù)里面上報一些信息。像一些性能的埋點(diǎn),每個函數(shù)都要處理,很繁瑣。能不能自動埋點(diǎn)呢?
答案是可以的。埋點(diǎn)只是在函數(shù)里面插入了一段代碼,這段代碼不影響其他邏輯,這種函數(shù)插入不影響邏輯的代碼的手段叫做函數(shù)插樁。
我們可以基于 babel 來實現(xiàn)自動的函數(shù)插樁,在這里就是自動的埋點(diǎn)。
思路分析
比如這樣一段代碼:
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');
}
我們要實現(xiàn)埋點(diǎn)就是要轉(zhuǎn)成這樣:
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');
};
有兩方面的事情要做:
引入 tracker 模塊。如果已經(jīng)引入過就不引入,沒有的話就引入,并且生成個唯一 id 作為標(biāo)識符 對所有函數(shù)在函數(shù)體開始插入 tracker 的代碼
代碼實現(xiàn)
掘金小冊《babel 插件通關(guān)秘籍》中有具體 api 的詳細(xì)介紹。
模塊引入
引入模塊這種功能顯然很多插件都需要,這種插件之間的公共函數(shù)會放在 helper,這里我們使用 @babel/helper-module-imports。
const importModule = require('@babel/helper-module-imports');
// 省略一些代碼
importModule.addDefault(path, 'tracker',{
nameHint: path.scope.generateUid('tracker')
})
首先要判斷是否被引入過:在 Program 根結(jié)點(diǎn)里通過 path.traverse 來遍歷 ImportDeclaration,如果引入了 tracker 模塊,就記錄 id 到 state,并用 path.stop 來終止后續(xù)遍歷;沒有就引入 tracker 模塊,用 generateUid 生成唯一 id,然后放到 state。
當(dāng)然 default import 和 namespace import 取 id 的方式不一樣,需要分別處理下。
我們把 tracker 模塊名作為參數(shù)傳入,通過 options.trackerPath 來取。
Program: {
enter (path, state) {
path.traverse({
ImportDeclaration (curPath) {
const requirePath = curPath.get('source').node.value;
if (requirePath === options.trackerPath) {// 如果已經(jīng)引入了
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}()`)();// 埋點(diǎn)代碼的 AST
}
}
}
我們在記錄 tracker 模塊的 id 的時候,也生成調(diào)用 tracker 模塊的 AST,使用 template.statement.
函數(shù)插樁
函數(shù)插樁要找到對應(yīng)的函數(shù),這里要處理的有:ClassMethod、ArrowFunctionExpression、FunctionExpression、FunctionDeclaration 這些節(jié)點(diǎn)。
當(dāng)然有的函數(shù)沒有函數(shù)體,這種要包裝一下,然后修改下 return 值。如果有函數(shù)體,就直接在開始插入就行了。
'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) {
const bodyPath = path.get('body');
if (bodyPath.isBlockStatement()) { // 有函數(shù)體就在開始插入埋點(diǎn)代碼
bodyPath.node.body.unshift(state.trackerAST);
} else { // 沒有函數(shù)體要包裹一下,處理下返回值
const ast = api.template.statement(`{${state.trackerImportId}();return PREV_BODY;}`)({PREV_BODY: bodyPath.node});
bodyPath.replaceWith(ast);
}
}
這樣我們就實現(xiàn)了自動埋點(diǎn)。
效果演示
我們來試下效果:
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);
效果如下:

我們實現(xiàn)了自動埋點(diǎn)!
總結(jié)
函數(shù)插樁是在函數(shù)中插入一段邏輯但不影響函數(shù)原本邏輯,埋點(diǎn)就是一種常見的函數(shù)插樁,我們完全可以用 babel 來自動做。
實現(xiàn)思路分為引入 tracker 模塊和函數(shù)插樁兩部分:
引入 tracker 模塊需要判斷 ImportDeclaration 是否包含了 tracker 模塊,沒有的話就用 @babel/helper-module-import 來引入。
函數(shù)插樁就是在函數(shù)體開始插入一段代碼,如果沒有函數(shù)體,需要包裝一層,并且處理下返回值。
代碼在這里,建議自己實現(xiàn)一遍。
