3

完整专题:http://www.codefrom.com/p/Babel.js

上一篇已经介绍了编写babel.js插件所需要了解的基础知识,这篇我们就开始编写babel.js插件了。

第一篇传送门: Babel.js插件开发之一 - Babel与AST

开始

新建一个新的ES6项目,创建目录结构如下的项目:

YourProject/
    |- src/
    |   |- index.es6
    |- js/
    |
    |- app.js

进入到 YourProject 并安装babel开发模块 babel-core

$ cd path/to/YourProject/
$ npm install babel-core

之后目录结构如下:

YourProject/
    |- src/
    |- js/
    |- node_modules/
    |   |- babel-core/
    |       |- ...
    |- app.js

新建插件目录

cd node_modules/
mkdir babel-plugin-test

并且新建目录下的nodejs模块文件,之后目录结构如下:

fileYourProject/
    |- src/
    |- js/
    |- node_modules/
    |   |- babel-core/
    |   |- babel-plugin-test/
    |       |- index.js
    |       |- package.json
    |- app.js

接下来我们就可以在 index.js 中编写插件了。

转换

由于AST中节点类型众多,我在这里就讲如何通过如上文档中的个别常用的类型进行转换,其他的都是类似的。

PS: 最近Babel.js更新了5.6+的API,支持用ES6编写,也换了新的转换器接口= = 可素他们自己官方的栗子都跑不起来= =,放弃,之后弄明白再换上新借口的版本,现在依然按照可用的例子进行讲解。

首先 创建一个入口: 在新创建的 index.js 中添加:

javascript'use strict';

module.exports = function(babel) {
    var t = babel.types; // AST模块
    var imports = {}; 
    // 如果你有自己的模块组织方式,用这里把模块名和路径记录下来。

    var moduleRoot = ''; // 你其他的自定义变量

    // module_name写你的插件名称,但并不在调用时使用
    return new babel.Transformer('module_name', {
        // 这里写你的转换逻辑 [逻辑区域]
    });
};

在AST中,我们可以把整个程序看成一个 Program 例如

var a = 42;
var b = 5;
var c = a + b;

其AST树长这样:(图 1)

AST

program 是它的根节点。

于是我们可以在上面的逻辑区域添加以下代码:

Program: function(node) {
},

除了 Program 常用的还有:

  • Class (对类的处理方法)
  • NewExpression (对new新建对象表达式的处理)
  • CallExpression (对调用函数表达式的处理)
  • MethodDefinition (对类成员方法的处理)
  • ... (等等)

它们都有三个传入参数: nodeparentscope

注: 你可以通过调试查看他们的具体构造。

如字面意思,他们分别代表了: 节点数据、 父节点、群数据。其中,节点数据 node 是操作一条语句主要的参数~。

节点数据就是该节点的属性以及内容,其具体的数据格式可以看我在第一篇相关概念中最后提到的两篇文档:

ES5: https://github.com/estree/estree/blob/master/spec.md - 文档 1
ES6: https://github.com/estree/estree/blob/master/es6.md - 文档 2

例如,上面图1中的那颗树的 Program 的节点数据,文档 1中相关描述如下:

Program

interface Program <: Node {
    type: "Program";
    body: [ Statement ];
}

我们可以看到其 node 参数是一个 Node 类型的数据,包含了两个属性:公共属性 type 代表节点类别,body 代表其内容,这里是一个子节点的列表,列表中有三个VariableDeclaration 代表程序中的三条语句,其类型也是 Node

假设我们定义了一种模块化的方法(类似AMD的requirejs),我们将整个程序包裹在一个 test.defineModule(function(){/* block */}) 方法中。

那我们可以这样构建Program:

'use strict';

module.exports = function(babel) {
    var t = babel.types; // AST模块
    return new babel.Transformer('module_name', {
        // 这里写你的转换逻辑 [逻辑区域]
        Program: function(node) {
            var moduleFunction = t.functionExpression( // [1]
                t.identifier(''), // [2]
                [],  // [3]
                t.blockStatement(node.body) // [4]
            );
            var moduleDefine = t.callExpression( // [5]
                t.identifier('test.defineModule'),  // [6]
                [moduleFunction] // [7]
            );
            var moduleExpression = t.expressionStatement(moduleDefine); // [8]
            node.body = [moduleExpression]; // [9]
            return node; // [10]
        }
    });
};

这里你只定义了Program的转换机制,因此其他的转换还是会按照默认的方式进行转换。

按照这种机制,上面的AST树的示例程序就被转换成下面这样了:

"use strict";

test.defineModule(function () {
  var a = 42;
  var b = 5;
  var c = a + b;
});

下面我们来逐行分析一下(以逻辑的顺序):

[5] 新建一个函数调用 moduleDefine
[6] 这个被调用函数的名字叫做 'test.defineModule' 即: test.defineModule()
[8] 整个函数调用是一个完整的表达式 moduleExpression ,即: test.defineModule();
接下来我们需要向这个函数调用中填入参数列表
[7] 这个参数列表中有一个参数 moduleFunction
1 moduleFunction 是一个函数
[2] 这个函数的名称是 '',即: 这是一个匿名函数
[3] 这个函数的参数列表为空
[4] 这个函数的内容块的内容是原本Program节点的内容
[9] 把原有的Program节点的内容替换成新的
[10] 返回这个改动,当然你是直接在原本的对象实例上改动的,不返回也可以

同样你也可以重新定义 ImportDeclarationExportDeclaration,其结构略微与普通节点有所不同,例如:

ImportDeclaration: function(node) {
    node.specifiers.forEach(function(specifier){
        if(specifier.type == 'ImportDefaultSpecifier'){
            imports[specifier.local.name] = node.source.value;
        }
    });
    this.dangerouslyRemove();
},

ExportDeclaration: function(){
    this.dangerouslyRemove();
}

其作用为:将export 和import的相关转换都删掉,并且将import的值和路径都记录下来,可以在其他的转换中用到,又或者直接在ImportDeclaration 中直接对import的变量进行操作,例如:

import $ from 'jquery'

我们希望转化成

var $ = test.requireModule('jquery');

并将其放入模块内:

ImportDeclaration: function(node) {
    var self = this;
    node.specifiers.forEach(function(specifier){
        if(specifier.type == 'ImportDefaultSpecifier'){
            //imports[specifier.local.name] = node.source.value;
            var requireCall = t.callExpression(
                t.identifier('test.requireModule'), 
                [t.literal(node.source.value)]
            );
            var assignEx = t.assignmentExpression(
                '=', 
                t.identifier(specifier.local.name), 
                requireCall
            );
            self.insertAfter(t.expressionStatement(assignEx));
        }
    });
    this.dangerouslyRemove();
},

将其假如之前的test.defineModule的转换中,则我们发现

import $ from 'jquery'

var a = 42;
var b = 5;
var c = a + b;

被转换为了:

'use strict';

test.defineModule(function () {
    $ = test.requireModule('jquery');

    var a = 42;
    var b = 5;
    var c = a + b;
});

ImportDeclaration 在上述文档2中的描述为:

interface ImportDeclaration <: Node {
    type: "ImportDeclaration";
    specifiers: [ ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier ];
    source: Literal;
}

specifiers 列表中的 specifiers 可能有三种类型,在文档中都有很详细的描述,这里就不多说了。

按照这样的理解,所有的方法都可以通过查看上面的文档 1和文档 2的说明进行改动。

看到这里你已经可以动手开始尝试写一个babel.js插件了。

使用

上述文件目录结构为:

YourProject/
    |- src/
    |- js/
    |- node_modules/
    |   |- babel-core/
    |   |- babel-plugin-test/
    |       |- index.js
    |       |- package.json
    |- app.js

src 中编写es6程序 test.es6

YourProject/
    |- src/
    |   |- test.es6
    |- js/
    |- node_modules/
    |   |- babel-core/
    |   |- babel-plugin-test/
    |       |- index.js
    |       |- package.json
    |- app.js

到YourProject目录下。执行

$ babel src/ -d js/ --plugins babel-plugin-test

则在 js 文件夹中就是你转化好的js文件啦~。

还有第三篇是有关英文文档的翻译。


已注销
1.3k 声望48 粉丝