babel插件开发

187 阅读2分钟

说明
本文适合想要快速、粗略的了解,很多深入、细节的地方都不涉及。
如有需要,建议直接看babel插件开发手册

AST了解

含义

AST全称Abstract Syntax Tree,是指用树状结构化来表示源代码。

前端构建过程,很多工具都有用到,比如TSbabel、rollup、webpack。
如图,右侧则是左边源代码的AST Xnip2022-09-01_20-41-13.jpg
可以使用astexplorer在线体验

如何生成AST?

我们可以直接使用库,常用的有esprima(eslint的默认解析器)、acorn(rollup使用)。babel是自己开发的,@babel/parser,参考了acron。

从源码到AST,会经过两次处理:

  1. 词法分析器 Lexical analyzer (别名:scanner)
    先将源码分割成一串语法单元(token)
    Xnip2022-09-01_20-55-12.jpg 语法单元还有其他属性,但这里为了截图出更多的type就没显示出来。
    在线体验esprima

  2. 语法分析器 Syntax analyzer (别名:parser)
    分析语法单元间的关系,将词法单元转为树结构。

babel代码转换流程

我们先使用babel的基础组件来了解转化过程。
demo如下,我们来修改函数名称。

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;

const code = `function exec(message = 'hello') {
    console.log(message);
}`;

// 1. 解析——转为AST
const ast = parser.parse(code);
// console.log(ast);

// 2. 转换——修改AST数据
traverse(ast, {
    enter(nodePath){
        // console.log(nodePath.node.type);
        if(nodePath.node.type === 'FunctionDeclaration') {
            nodePath.node.id.name = 'modify_'  + nodePath.node.id.name;
        }
    }
});

// 3. 生成——使用修改后的AST生成新代码
const result = generator(ast);

console.log('\n新代码:');
console.log(result.code);

Xnip2022-09-01_09-35-45.jpg

插件开发示例

我们开发插件时,不用像上面那样需要自己引入parser等,babel已经定义了接口。

功能确认

我想开发一个能给普通的Function增加一个运行时长的日志

// 源代码
function exec(message = 'hello') {
    console.log(message);
}
// 目标代码
function exec(message = 'hello') {
    console.time("exec");
    console.log(message);
    console.timeEnd("exec");
}

开发

  1. 搭结构

    const babel = require('@babel/core');
    const addConsolePlugin = require('./plugin-add-console');
    // 源代码
    const code = `function exec(message = 'hello') {
        console.log(message);
    }`;
    // 转换
    const target = babel.transform(code, {
        // 使用插件
        plugins: [addConsolePlugin]
    });
    
    console.log('\n转换后的代码:');
    console.log(target.code);
    

    插件 plugin-add-console.js

    const babelTypes = require('@babel/types');
    
    module.exports = {
        visitor: {
            FunctionDeclaration(nodePath) {
                console.log(nodePath);
            }
        }
    }
    

    visitor的属性就是我们想要匹配的节点类型,但如何知道我应该匹配那个type
    AST结构中的type属性值就是 Xnip2022-09-01_08-46-51.jpg

  2. AST对比
    目标代码的AST Xnip2022-09-01_08-50-01.jpg 对比源代码AST和目标代码的AST,body的头部和尾部各自增加了一个ExpressionStatement

  3. 修改AST树
    上一步我们知道了如何改变AST树,现在就来处理
    开发需要用用到@babel/types,可以通过babel-types的文档查到它的使用方法

        visitor: {
            FunctionDeclaration(nodePath) {
                const funName = nodePath.node.id.name;
                const body = nodePath.node.body.body;
    
                // 每个babelTypes的函数调用,都是通过AST树的type得到的
                const obj = babelTypes.Identifier('console');
                const time = babelTypes.Identifier('time');
                const timeEnd = babelTypes.Identifier('timeEnd');
                const startMember = babelTypes.memberExpression(obj, time);
                const endMember = babelTypes.memberExpression(obj, timeEnd);
                const funLit = babelTypes.stringLiteral(funName);
    
                const startExpression = babelTypes.callExpression(startMember, [funLit]);
                const endExpression = babelTypes.callExpression(endMember, [funLit]);
                const first = babelTypes.ExpressionStatement(startExpression);
                const last = babelTypes.ExpressionStatement(endExpression);
    
                // 修改AST
                body.unshift(first);
                body.push(last);
            }
        }
    

    执行调用,得到转换后的代码 Xnip2022-09-01_09-07-04.jpg

参考文章