[译]理解AST构建Babel插件

原文:https://www.sitepoint.com/understanding-asts-building-babel-plugin/javascript

本文只选择了重要部分进行翻译java

语言介绍

咱们设计了一个插件来将正常的object和array转换为持久的数据结构Morinode

咱们想写的code是这样:git

var foo = { a: 1 };
var baz = foo.a = 2;
foo.a === 1;
baz.a === 2;
复制代码

想要转换获得的是:github

var foo = mori.hashMap('a', 1);
var baz = mori.assoc(foo, 'a', 2);
mori.get(foo, 'a') === 1;
mori.get(baz, 'a') === 2;
复制代码

Babel

Babel 主要的处理过程包括三部分: express

Babel 转换

Parse

Babylon 解析和理解Javascript代码npm

Transform

babel-traverse分析和修改AST编程

Generate

babel-generator将AST树转换回正常的代码json

AST抽象语法树

理解AST是咱们接下去内容的基础。 Javascript语言是由一串字符串生成的,每个都带有着一些可视的语义信息。这对咱们来讲都颇有用,由于它容许咱们使用匹配字符 ([], {}, ()), 成对的字符("", ''),以及缩进,让咱们更好的理解程序。 而后这对计算机来讲是无心义的。对他们来讲,每个字符在内存中只是一个数值,他们不能使用它们来问高水平的问题像“有多少变量在这个声明?相反,咱们须要妥协,找到一个方法来把代码变成可编程的和计算机能够理解的东西。数组

形以下面的代码

var a =3;
a + 5
复制代码

解析获得的AST树

全部的AST起始于一个Program的根节点,该节点包含了程序最顶级的表达。在这个例子中,咱们只有两个:

  1. 一个VariableDeclaration用来赋值给Identifier的a标识符一个NumericLiteral数值3
  2. ExpressionStatement由一个BinaryExpression组成,由Identifier的“a”标识符和操做符“+”,以及数值5

尽管它们由简单的块组成,ast的大小意味着它们至关复杂,特别是对于重要的项目。相比于直接本身去理解AST,咱们可使用astexplorer.net, 网站容许咱们在左边输入Javascript代码,右边会输出AST。咱们将使用这个工具来理解和实验代码。

为了Babel的稳定性,请选择使用"babylon6"做为一个解释器。

Setup

确保你是使用node和npm安装。建立一个工程文件,建立一个package.json文件而且安装以下依赖

mkdir moriscript && cd moriscript
npm init -y
npm install --save-dev babel-core
复制代码

咱们建立一个文件插件而且导出一个默认函数

// moriscript.js
module.exports = function(babel) {
  var t = babel.types;
  return {
    visitor: {

    }
  };
};

复制代码

babel 提供了一个visitor模式,能够用于编写各类插件,插入删除等操做来产生一个新的AST树

// run.js
var fs = require('fs');
var babel = require('babel-core');
var moriscript = require('./moriscript');

// read the filename from the command line arguments
var fileName = process.argv[2];

// read the code from this file
fs.readFile(fileName, function(err, data) {
  if(err) throw err;

  // convert from a buffer to a string
  var src = data.toString();

  // use our plugin to transform the source
  var out = babel.transform(src, {
    plugins: [moriscript]
  });

  // print the generated code to screen
  console.log(out.code);
});

复制代码

Arrays数组

MoriScript首要的任务是转换Object和Array为它们对应的Mori部分:HashMapsh和Vector。咱们首先要转换的是Array。

var bar = [1, 2, 3];
// should becom
var bar = mori.vector(1, 2, 3);
复制代码

将上述代码复制到astexplorer,而且高亮数组[1,2,3]来看对应的AST节点。

为了可读性咱们只选择了数据区域的AST节点:

// [1, 2, 3]
{
    "type": "ArrayExpression",
    "elements":[
        {
            "type": "NumericLiteral",
            "value": 1
        },
        {
            "type": "NumericLiteral",
            "value": 2
        },{
            "type": "NumericLiteral",
            "value": 3
        }
    ]
}
复制代码

而mori.vector(1,2,3)的AST以下:

{
    "type": "CallExpression",
    "callee": {
        "type": "MemberExpression",
        "object": {
            "type": "Identifier",
            "name": "mori"
        },
        "property":{
            "type": "Identifier",
            "name": "vector"
        }
    },
    "arguments":[
        {
            "type": "NumericLiteral",
            "value": 1
        },
        {
            "type": "NumericLiteral",
            "value": 2
        },
        {
            "type": "NumericLiteral",
            "value": 3
        }
    ]
}
复制代码

上述节点的可视化效果,能够清楚地看到两棵树之间的区别

如今咱们能够很清楚地看到,咱们须要更换顶级表达式,但咱们能共享在两棵树之间的数字表达。

让咱们开始添加第一个ArrayExpression到咱们的visitor中:

module.exports = function(babel) {
  var t = babel.types;
  return {
    visitor: {
      ArrayExpression: function(path) {

      }
    }
  };
};
复制代码

咱们能够从babel-types文档中找到对应的表达式类型,在这个例子咱们要去替换ArrayExpression为一个CallExpression,咱们能够生成t.callExpression(callee, arguments)。而后须要的是利用t.memberExpression(object, property)来调用MemberExpression。

ArrayExpression: function(path){
    path.replaceWith(
        t.callExpression(
            t.memberExpression(t.identifier('mori'), t.identifier('vector')), 
            path.node.elements
        )
    )    
        
}
复制代码

Object

接下来看看Object

var foo = { bar: 1};
var foo =mori.hashMap('bar',1);
复制代码

object语法跟ArrayExpression有类似的结构

高亮mori.hashMap('bar', 1)获得:

{
  "type": "ObjectExpression",
  "properties": [
    {
      "type": "ObjectProperty",
      "key": {
        "type": "Identifier",
        "name": "bar"
      },
      "value": {
        "type": "NumericLiteral",
        "value": 1
      }
    }
  ]
}
复制代码

可视化获得的AST树:

ObjectExpression: function(path){
    var props = [];
    path.node.properties.forEach(function(prop){
        props.push(
            t.stringLiteral(prop.key.name),
            prop.value
        );
    });
    path.replaceWith(
        t.callExpression(
            t.memberExpression(t.identifier('mori'), t.identifier('hasMap')),
            props
        )
    )
}
复制代码

相似的咱们有一个CallExpression包围着一个MemberExpression。跟Array的代码很相似,不一样的是咱们须要作点更复杂的来获得属性和值。

Assignment

foo.bar = 3;
mori.assoc(foo, 'bar', 3);
复制代码
AssignmentExpression: function(path){
    var lhs = path.node.left;
    var rhs = path.node.right;
    
    if(t.isMemberExpression(lhs)){
        if(t.isIdentifier(lhs.property)){
            lhs.property = t.stringLiteral(lhs.property.name);
        }
        path.replaceWith(
            t.callExpression(
                t.memberExpression(),
                [lhs.object, lhs.property, rhs]
            )
        );
    }
}
复制代码

Membership

foo.bar;
mori.get(foo, 'bar');
复制代码
MemberExpression: function(path){
    if(t.isAssignmentExpression(path.parent)) return;
    if(t.isIdentifier(path.node.property)){
        path.node.property = t.stringLiteral(path.node.property.name)
    }
    path.replaceWith(
        t.callExpression(
            t.memberExpression(),
            [path.node.object, path.node.property]
        )
    )
}
复制代码

存在的一个问题是得过的mori.get又会是一个MemberExpression,致使循环递归

// set a flag to  recognize express has been tranverse
MemberExpression: function(path){
    if(path.node.isClean) return ;
    ...
}
复制代码

Babel 只能转换Javascript 也就是Babel parser理解的

AST 在线 parse: https://astexplorer.net/

相关文章
相关标签/搜索