原文: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的根节点,该节点包含了程序最顶级的表达。在这个例子中,我们只有两个:
- 一个VariableDeclaration用来赋值给Identifier的a标识符一个NumericLiteral数值3
- 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/