babel插件--根据注释自动埋点

2,236 阅读3分钟

本文主要介绍了babel根据注释自动生成埋点方法的插件源码,以日常开发的埋点需求为导向,处理了埋点的常用场景。其他比较特殊的场景,也可以在此基础上进行拓展。

参考文章:juejin.cn/post/696621… 【# 神说要有光 -- 还在手动埋点么?out 了。不到百行代码实现自动埋点】

插件的主要功能有:

  1. 根据注释自动在函数首行添加埋点方法;
  2. 根据注释中的参数,向埋点代码中进行传参;
  3. 根据函数内的 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"
  }
}

文件目录(可以自己跑一下代码试一试)

image.png