通过构建自己的Babel插件了解AST

本文由Tim Severien进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

每天,成千上万的JavaScript开发人员都使用浏览器供应商尚未实现的语言版本。 他们中的许多人使用的语言功能仅是提案,无法保证它们会被纳入规范。 Babel项目使所有这些成为可能。

Babel以将ES6代码转换为我们今天可以安全运行的ES5代码而闻名,但是它也允许开发人员编写可在编译时转换JavaScript程序结构的插件。

今天,我们将研究如何编写Babel插件以默认不可变数据添加到JavaScript。 可以从我们的GitHub存储库下载本教程的代码。

语言概述

我们想要设计一个插件,使我们可以使用常规的对象和数组文字,并使用Mori将其转换为持久性数据结构。

我们要编写这样的代码:

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;

让我们开始使用MoriScript

Babel概述

如果我们在Babel的表面下看,就会发现三个可以处理大部分过程的重要工具。

巴别塔法

解析

Babylon是解析器,它了解如何获取JavaScript代码字符串并将其转换为称为抽象语法树(AST)的计算机友好表示形式。

转变

babel-traverse模块允许您浏览,分析和潜在地修改AST。

生成

最后,使用babel-generator模块将转换后的AST转换回常规代码。

什么是AST?

在继续本教程之前,我们了解AST的目的是很基本的。 因此,让我们深入了解它们是什么以及我们为什么需要它们。

JavaScript程序通常由一系列字符组成,每个字符对我们的人脑都有一些视觉意义。 这对我们来说确实非常好,因为它允许我们使用匹配的字符( []{}() ),成对的字符( ''"" )和缩进使我们的程序更易于解释。

但是,这对计算机不是很有帮助。 对于他们来说,这些字符中的每一个都只是内存中的一个数字值,因此他们不能使用它们来问诸如“此声明中有多少个变量?”之类的高级问题。 相反,我们需要妥协并找到一种方法来将我们的代码转换为我们可以编程计算机可以理解的东西

看下面的代码。

var a = 3;
a + 5

当我们为该程序生成AST时,最终得到的结构如下所示:

AST范例

所有AST均以树的根部的Program节点开头,该节点包含程序中的所有顶级语句。 在这种情况下,我们只有两个:

  1. VariableDeclaration与一个VariableDeclarator来分配Identifiera ”到NumericLiteral3 ”。
  2. 依次由一个BinaryExpression组成的ExpressionStatementBinaryExpression ,它被称为Identifiera ”,运算符“ + ”和另一个NumericLiteral5 ”。

尽管它们由简单的构建基块组成,但AST的大小意味着它们通常非常复杂,尤其是对于简单的程序而言。 可以尝试使用astexplorer.net而不是自己弄清楚AST,而可以在左侧输入JavaScript,然后在右侧输出AST的可解释表示。 我们将继续使用此工具来专门理解和试验代码。

为了与Babel保持一致,请确保选择“ babylon6”作为解析器。

编写Babel插件时,我们的工作是先获取AST,然后插入/移动/替换/删除一些节点,以创建可用于生成代码的新AST。

设定

在开始之前,请确保已安装了nodenpm 。 然后为项目创建一个文件夹,创建一个package.json文件并安装以下dev依赖项。

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: {

    }
  };
};

此函数公开了访问者模式的接口,稍后我们将再介绍。

最后,我们将创建一个运行器,我们可以使用它来测试我们的插件。

// 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);
});

我们可以使用示例MoriScript文件的名称调用此脚本,以检查它是否生成了我们期望的JavaScript。 例如, node run.js example.ms

数组

MoriScript的首要目标是将Object和Array文字转换为与Mori对应的文字:HashMaps和Vectors。 我们将首先处理数组,因为它们稍微简单一些。

var bar = [1, 2, 3];
// should become
var bar = mori.vector(1, 2, 3);

从上面将代码粘贴到astexplorer中,并突出显示数组文字[1, 2, 3]以查看相应的AST节点。

为了便于阅读,我们将省略不需要担心的元数据字段。

{
  "type": "ArrayExpression",
  "elements": [
    {
      "type": "NumericLiteral",
      "value": 1
    },
    {
      "type": "NumericLiteral",
      "value": 2
    },
    {
      "type": "NumericLiteral",
      "value": 3
    }
  ]
}

现在让我们对mori.vector(1, 2, 3)进行调用。

{
  "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
    }
  ]
}

如果我们直观地表达这一点,我们将更好地了解两棵树之间需要更改的内容。

阵列AST

现在我们可以很清楚地看到我们需要替换顶级表达式,但是我们将能够在两棵树之间共享数字文字。

首先,将ArrayExpression方法添加到访问者对象上。

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

      }
    }
  };
};

当Babel遍历AST时,它会查看每个节点,如果它在插件的visitor对象中找到了相应的方法,则会将上下文传递给该方法,以便我们可以对其进行分析或操作。

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

我们可以使用babel-types包找到每种表达式的文档 。 在这种情况下,我们将用ArrayExpression替换CallExpression ,可以使用t.callExpression(callee, arguments)创建它。 我们要调用的东西MemberExpression我们可以创建t.memberExpression(object, property)

您也可以在astexplorer中实时尝试此操作, 方法是单击“转换”下拉菜单并选择“ babelv6”。

对象

接下来,让我们看一下对象。

var foo = { bar: 1 };
// should become
var foo = mori.hashMap('bar', 1);

对象文字与我们之前看到的ArrayExpression具有相似的结构。

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

这很简单。 有一组属性,每个属性都有一个键和一个值。 现在,让我们突出显示对mori.hashMap('bar', 1)的相应Mori调用mori.hashMap('bar', 1)并进行比较。

{
  "type": "CallExpression",
  "callee": {
    "type": "MemberExpression",
    "object": {
      "type": "Identifier",
      "name": "mori"
    },
    "property": {
      "type": "Identifier",
      "name": "hashMap"
    }
  },
  "arguments": [
    {
      "type": "StringLiteral",
      "value": "bar"
    },
    {
      "type": "NumericLiteral",
      "value": 1
    }
  ]
}

同样,让我们​​也看一下这些AST的可视化表示。

对象AST

像以前一样,我们有一个CallExpression缠着MemberExpression ,我们可以从我们的阵列码借用,但我们将不得不做一些更复杂,以获得属性和值成扁平阵列。

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('hashMap')),
      props
    )
  );
}

这与数组的实现非常相似,除了我们必须将Identifier转换为StringLiteral以防止我们最终得到如下所示的代码:

// before
var foo = { bar: 1 };
// after
var foo = mori.hashMap(bar, 1);

最后,我们将创建一个帮助函数,以创建将继续使用的Mori MemberExpressions

function moriMethod(name) {
  return t.memberExpression(
    t.identifier('mori'),
    t.identifier(name)
  );
}

// now rewrite
t.memberExpression(t.identifier('mori'), t.identifier('methodName'));
// as
moriMethod('methodName');

现在,我们可以创建一些测试用例并运行它们,以查看我们的插件是否正常工作:

mkdir test
echo -e "var foo = { a: 1 };\nvar baz = foo.a = 2;" > test/case.ms
node run.js test/case.ms

您应该在终端上看到以下输出:

var foo = mori.hashMap("a", 1);
var baz = foo.a = 2;

分配

为了使我们的新Mori数据结构有效,我们还必须重写本机语法以为它们分配新属性。

foo.bar = 3;
// needs to become
mori.assoc(foo, 'bar', 3);

不再继续包含简化的AST,我们现在仅使用图表和插件代码,但可以随时通过astexplorer继续运行这些示例。

分配AST

我们必须从AssignmentExpression每一端提取并转换节点,以创建所需的CallExpression

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(
        moriMethod('assoc'),
        [lhs.object, lhs.property, rhs]
      )
    );
  }
}

我们的AssignmentExpressions处理程序进行了初步检查,以查看左侧的表达式是否为MemberExpression (因为我们不想弄乱var a = 3类的东西)。 然后,我们使用森的assoc方法将替换为新的CallExpression

像以前一样,我们还必须处理使用Identifier情况并将其转换为StringLiteral

现在创建另一个测试用例,并运行代码以查看其是否有效:

echo -e "foo.bar = 3;" >> test/case.ms
node run.js test/case.ms

$ mori.assoc(foo, "bar", 3);

会员资格

最后,我们还必须重写本机语法以访问对象的成员。

foo.bar;
// needs to become
mori.get(foo, 'bar');

这是两个AST的视觉表示。

AST会员

我们几乎可以直接使用MemberExpression的属性,但是属性部分将作为Identifier ,因此我们需要对其进行转换。

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(
      moriMethod('get'),
      [path.node.object, path.node.property]
    )
  );
}

要注意的第一个重要区别是,如果此节点的父级是AssignmentExpression ,我们将尽早退出该函数。 这是因为我们想让我们的AssignmentExpression访问者方法处理这些情况。

看起来不错,但是如果运行此代码,您实际上会发现自己遇到了堆栈溢出错误。 这是因为当我们替换给定的MemberExpressionfoo.bar )时,我们将其替换为另一个( mori.get )。 然后Babel遍历这个新节点,并将其递归地传递回我们的visitor方法。

为了解决这个问题,我们可以标记moriMethod的返回值,并选择在MemberExpression方法中忽略它们。

function moriMethod(name) {
  var expr = t.memberExpression(
    t.identifier('mori'),
    t.identifier(name)
  );

  expr.isClean = true;
  return expr;
}

标记完之后,我们可以在函数中添加另一个return子句。

MemberExpression: function(path) {
  if(path.node.isClean) return;
  if(t.isAssignmentExpression(path.parent)) return;

  // ...
}

创建一个最终的测试用例,并编译您的代码以检查其是否有效。

echo -e "foo.bar" >> test/case.ms
node run.js test/case.ms

$ mori.get(foo, "bar");

一切都很好,您现在有了一种类似于JavaScript的语言,但是默认情况下具有不变的数据结构,而不会损害原始的表达语法。

结论

这是一篇繁重的代码,但是我们已经介绍了设计和构建Babel插件的所有基础知识,这些插件可用于以有用的方式转换JavaScript文件。 您可以MoriScript在REPL玩在这里 ,你可以找到完整的源代码在GitHub上

如果您有兴趣进一步发展,并且想了解有关Babel插件的更多信息,请查阅出色的Babel手册,并参阅GitHub上的babel-plugin-hello-world存储库。 或者只是阅读npm上已有700多个Babel插件的源代码。 还有一个Yeoman生成器,用于搭建新插件。

希望本文能启发您编写Babel插件! 但是,在您开始实施下一种出色的转换语言之前,需要注意一些基本规则。 Babel是一个JavaScript到JavaScript的编译器。 这意味着我们不能将CoffeeScript之类的语言实现为Babel插件。 我们只能转换Babel解析器可以理解的JavaScript的微小超集

这是一个新颖的插件的入门指南。 您可能会滥用按位| 或运算符来创建功能性管道,就像在F#,Elm和LiveScript中可以找到的那样。

2 | double | square

// would become

square(double(2))

或例如,在箭头函数内:

const doubleAndSquare = x => x | double | square

// would become

const doubleAndSquare = x => square(double(x));

// then use babel-preset-es2015

var doubleAndSquare = function doubleAndSquare(x) {
  return square(double(x));
};

一旦了解了规则,解析器和您的想象力便是唯一的限制。

您是否制作了要共享的Babel插件? 在评论中让我知道。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值