還在手動埋點麼?out 了。不到百行代碼實現自動埋點

埋點是一個常見的需求,就是在函數裏面上報一些信息。像一些性能的埋點,每一個函數都要處理,很繁瑣。能不能自動埋點呢?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

  • 引入 tracker 模塊。若是已經引入過就不引入,沒有的話就引入,而且生成個惟一 id 做爲標識符
  • 對全部函數在函數體開始插入 tracker 的代碼

代碼實現

小冊《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 插件通關祕籍》

相關文章
相關標籤/搜索