[译]理解AST构建Babel插件

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

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

语言介绍

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

我们想写的code是这样:

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

想要转换得到的是:

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 主要的处理过程包括三部分:

Parse

Babylon 解析和理解Javascript代码

Transform

babel-traverse分析和修改AST

Generate

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

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/

转载于:https://juejin.im/post/5b03caea518825428630d407

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值