原文: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/