如何编写一个 babel 插件

绯雨闲丸 发布于

如何编写一个 babel 插件

由于目前各浏览器对 JavaScript 语言的支持并不统一,导致很多语言的新特性在浏览器上并没有得到原生支持。在这种情况下,使用 babel 转换编写为浏览器可以识别的编码,使得使用语言的新特性来编写 JavaScript 变得可行。

babel 可以将各种高版本的的 Javascript 编码转换成低版本的编码(例如,将 es7, es6转换成 es5,甚至转换成 es3),它也允许使用者开发插件,能够在编译时自定义转换 JavaScript 的结构的规则。

抽象语法树 (AST)

理解 babel 编码转换核心流程

babel 处理转换代码的核心流程如下:

解析(Parse)

babylon 是一个解析器,它可以将 JavaScript 字符串转换为对计算机来说更加友好的表现形式,称之为抽象语法树(AST)。

转换(Transform)

babel-traverse 模块允许你浏览、分析和修改抽象语法树(AST)。

生成(Generate)

最后,babel-generator 模块用来将转换后的抽象语法树(AST)转换为 JavaScript 字符串。

理解抽象语法树 AST

JavaScript 代码是设计成便于人理解的,但是计算机并不能直接理解 JavasScript 代码。

抽象语法树不同于 JavaScript 代码,它是一种对计算机来可以理解的树结构。

我们来看一下如下代码:

var a = 1;
a + 1;

通过这段代码我们可以得到如下的 AST 树

小技巧:我们可以编写 JavaScript 代码,使用 babylon 将代码转换成 AST 树,并查看该树的结构。我们也可以通过在线工具将代码转换成 AST 树并查看(见附录)

我们可以看出 AST 语法树的特征,Program 代表一段编码程序,每一段声明定义或者表达式语句,都会被解析成一个构造对象 Declaration 或 Expresstion。VariableDeclaration表示一段变量声明,Identifier 表示标识符,它的值是 a,NumberLiteral 表示数字字面量,它的值是 1,整个VariableDeclaration 对象对应代码段 var a = 1;。 ExpressionStatement 表示一段表达式语句, BinaryExpression 表示是做运算的表达式,同样Identifier 表示标识符,它的值是 a,NumberLiteral 表示数字字面量,它的值是 1。整个 ExpressionStatement 对象对应代码段 a + 1;

我们再来看一下如下代码:

function hello() {}
hello();

通过这段代码我们可以得到如下的 AST 树

这段代码的语法树,我们可以看出:FunctionDeclaration表示一段函数声明,Identifier 表示标识符,它的值是 hello,BlockStatement 花括号,整个FunctionDeclaration 对象对应代码段 function hello() {}。 ExpressionStatement 表示一段表达式语句, CallExpression 表示是执行函数的表达式,同样Identifier 表示标识符,它的值是 hello。整个 ExpressionStatement 对象对应代码段 hello();

编写 babel 插件

babel 插件的编写方式很简单,简单实现如下:

module.exports = function(babel) {
    return {
        visitor: {
            <babel语法树类型1>: function(path, options) {
            		// 改变语法树实现1
            },
            <babel语法树类型2>: function(path, options) {
            		// 改变语法树实现2
            },
            ...
        }
    };
};

我们可以看到 babel 的插件实现,只需要导出一个函数模块, 这个函数的返回值是一个对象,必须是一个包含键名为 visitor 的对象,这个对象包含键名为任意 babel 语法树构造类型,值为一个包含改变语法树实现的函数。

我们先编写一个改变变量 a 的值的 babel 插件,使用后的效果应如下:

原始代码:

var a = 1;
var b = 2;

转换后代码

var a = 2;
var b = 1;

为此,我们创建了一个名为 babel-custom-code-filter.js 的插件文件,代码如下:

module.exports = function(babel) {
    var t = babel.types;
    return {
        visitor: {
            VariableDeclarator: function(path, settings) {
                const node = path.node;
                if (node.id.name === 'a' && node.init.value === 1) {
                    node.init.value = 2;
                } else if (node.id.name === 'b' && node.init.value === 2) {
                    node.init.value = 1;
                }
            }
        }
    };
};

为了验证这个插件,需要使用 babel-core 模块的 transform 方法,我们创建了名为 ast.js 的文件,代码如下:

const code = `
var a = 1;
var b = 2'
`;
const babel = require('babel-core');
const ast = babel.transform(code, {
    plugins: [
        [
	        require('./babel-custom-code-filter'), 
	        {
	            debug: true,
	        },
        ],
    ],
});
console.log(ast.code);

最终转换后的代码为:

var a = 2;
var b = 1;

更多示例

我们希望自定义一个转换规则,将如下形式的代码,

...
require.context('../mock-server/api', true, /\.js(on)?$/);
... 

转换成如下:

...
(function () {});
...

我们首先来看一下改变前代码,与改变后代码的语法树结构

改变前:

改变后:

在了解了语法树的差异后,我们可以开始编写插件了,我们只需要定位到符合规则的语法树, 并将其替换成我们需要的语法树即可。

代码如下:

module.exports = function(babel) {
    var t = babel.types;
    return {
        visitor: {
            CallExpression: function(path, options) {
                const node = path.node;
                const arguments = node.arguments;
                const callee = node.callee;
                if (arguments &&
                    arguments.length === 3 && callee &&
                    callee.object &&
                    callee.object.name === 'require' &&
                    callee.property &&
                    callee.property.name === 'context' &&
                    arguments[0].value === '../mock-server/api' &&
                    arguments[1].value === true &&
                    arguments[2].pattern === '\\.js(on)?$') {
                    path.replaceWith(
                        t.functionExpression(null, [], t.blockStatement([])),
                    );
                }
            }
        }
    };
};

代码示例地址可以查看附录。

附录