抽象語法樹(Abstract Syntax Tree
)簡稱 AST
,是源代碼的抽象語法結構的樹狀表現形式。webpack
、eslint
等不少工具庫的核心都是經過抽象語法書這個概念來實現對代碼的檢查、分析等操做。今天我爲你們分享一下 JavaScript 這類解釋型語言的抽象語法樹的概念javascript
咱們經常使用的瀏覽器就是經過將 js 代碼轉化爲抽象語法樹來進行下一步的分析等其餘操做。因此將 js 轉化爲抽象語法樹更利於程序的分析。html
如上圖中變量聲明語句,轉換爲 AST 以後就是右圖中顯示的樣式vue
左圖中對應的:java
var
是一個關鍵字AST
是一個定義者=
是 Equal 等號的叫法有不少形式,在後面咱們還會看到is tree
是一個字符串;
就是 Semicolon首先一段代碼轉換成的抽象語法樹是一個對象,該對象會有一個頂級的 type 屬性 Program
;第二個屬性是 body
是一個數組。node
body
數組中存放的每一項都是一個對象,裏面包含了全部的對於該語句的描述信息webpack
type: 描述該語句的類型 --> 變量聲明的語句 kind: 變量聲明的關鍵字 --> var declaration: 聲明內容的數組,裏面每一項也是一個對象 type: 描述該語句的類型 id: 描述變量名稱的對象 type: 定義 name: 變量的名字 init: 初始化變量值的對象 type: 類型 value: 值 "is tree" 不帶引號 row: "\"is tree"\" 帶引號
JavaScript
是解釋型語言,通常經過 詞法分析 -> 語法分析 -> 語法樹,就能夠開始解釋執行了git
詞法分析:也叫掃描
,是將字符流轉換爲記號流(tokens
),它會讀取咱們的代碼而後按照必定的規則合成一個個的標識github
好比說:var a = 2
,這段代碼一般會被分解成 var、a、=、2
web
[ { type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: '2' }, ];
當詞法分析源代碼的時候,它會一個一個字符的讀取代碼,因此很形象地稱之爲掃描 - scans
。當它遇到空格、操做符,或者特殊符號的時候,它會認爲一個話已經完成了。express
語法分析:也稱解析器
,將詞法分析出來的數組轉換成樹的形式,同時驗證語法。語法若是有錯的話,拋出語法錯誤。
{ ... "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, ... }
語法分析成 AST ,咱們能夠在這裏在線看到效果 http://esprima.org
好比說,有個函數 function a() {}
我想把它變成 function b() {}
好比說,在 webpack
中代碼編譯完成後 require('a') --> __webapck__require__("*/**/a.js")
下面來介紹一套工具,能夠把代碼轉成語法樹而後改變節點以及從新生成代碼
準備工具:
在推薦一個經常使用的 AST 在線轉換網站:https://astexplorer.net/
好比說一段代碼 function getUser() {}
,咱們把函數名字更改成 hello
,看代碼流程
看如下代碼,簡單說明 AST
遍歷流程
const esprima = require('esprima'); const estraverse = require('estraverse'); const code = `function getUser() {}`; // 生成 AST const ast = esprima.parseScript(code); // 轉換 AST,只會遍歷 type 屬性 // traverse 方法中有進入和離開兩個鉤子函數 estraverse.traverse(ast, { enter(node) { console.log('enter -> node.type', node.type); }, leave(node) { console.log('leave -> node.type', node.type); }, });
輸出結果以下:
由此能夠獲得 AST 遍歷的流程是深度優先,遍歷過程以下:
此時咱們發現函數的名字在 type
爲 Identifier
的時候就是該函數的名字,咱們就能夠直接修改它即可實現一個更改函數名字的 AST
工具
// 轉換樹 estraverse.traverse(ast, { // 進入離開修改都是能夠的 enter(node) { console.log('enter -> node.type', node.type); if (node.type === 'Identifier') { node.name = 'hello'; } }, leave(node) { console.log('leave -> node.type', node.type); }, }); // 生成新的代碼 const result = escodegen.generate(ast); console.log(result); // function hello() {}
提到 AST 咱們確定會想到 babel,自從 Es6 開始大規模使用以來,babel 就出現了,它主要解決了就是一些瀏覽器不兼容 Es6 新特性的問題,其實就把 Es6 代碼轉換爲 Es5 的代碼,兼容全部瀏覽器,babel 轉換代碼其實就是用了 AST,babel 與 AST 就有着很一種特別的關係。
那麼咱們就在 babel 的中來使用 AST,看看 babel 是如何編譯代碼的(不講源碼啊)
須要用到兩個工具包 @babel/core
、@babel/preset-env
當咱們配置 babel 的時候,不論是在 .babelrc
或者 babel.config.js
文件裏面配置的都有 presets
和 plugins
兩個配置項(還有其餘配置項,這裏不作介紹)
// .babelrc { "presets": ["@babel/preset-env"], "plugins": [] }
當咱們配置了 presets
中有 @babel/preset-env
,那麼 @babel/core
就會去找 preset-env
預設的插件包,它是一套
babel 核心包並不會去轉換代碼,核心包只提供一些核心 API,真正的代碼轉換工做由插件或者預設來完成,好比要轉換箭頭函數,會用到這個 plugin,@babel/plugin-transform-arrow-functions
,當須要轉換的要求增長時,咱們不可能去一一配置相應的 plugin,這個時候就能夠用到預設了,也就是 presets。presets 是 plugins 的集合,一個 presets 內部包含了不少 plugin。
如今咱們有一個箭頭函數,要想把它轉成普通函數,咱們就能夠直接這麼寫:
const babel = require('@babel/core'); const code = `const fn = (a, b) => a + b`; // babel 有 transform 方法會幫咱們自動遍歷,使用相應的預設或者插件轉換相應的代碼 const r = babel.transform(code, { presets: ['@babel/preset-env'], }); console.log(r.code); // 打印結果以下 // "use strict"; // var fn = function fn() { return a + b; };
此時咱們能夠看到最終代碼會被轉成普通函數,可是咱們,只須要箭頭函數轉通函數的功能,不須要用這麼大一套包,只須要一個箭頭函數轉普通函數的包,咱們實際上是能夠在 node_modules
下面找到有個叫作 plugin-transform-arrow-functions
的插件,這個插件是專門用來處理 箭頭函數的,咱們就能夠這麼寫:
const r = babel.transform(code, { plugins: ['@babel/plugin-transform-arrow-functions'], }); console.log(r.code); // 打印結果以下 // const fn = function () { return a + b; };
咱們能夠從打印結果發現此時並無轉換咱們變量的聲明方式仍是 const 聲明,只是轉換了箭頭函數
此時,咱們就能夠本身來寫一些插件,來實現代碼的轉換,中間處理代碼的過程就是使用前面提到的 AST 的處理邏輯
如今咱們來個實戰把 const fn = (a, b) => a + b
轉換爲 const fn = function(a, b) { return a + b }
首先咱們在在線分析 AST 的網站上分析 const fn = (a, b) => a + b
和 const fn = function(a, b) { return a + b }
看二者語法樹的區別
根據咱們分析可得:
ArrowFunctionExpression
,而是函數表達式了 FunctionExpression
箭頭函數表達式(ArrowFunctionExpression)
轉換爲 函數表達式(FunctionExpression)
二進制表達式(BinaryExpression)
包裹在 返回語句中(ReturnStatement)
而後 push 到 代碼塊中(BlockStatement)
,在 babel 中,咱們開發 plugins 的時候要用到訪問者模式,就是說在訪問到某一個路徑的時候進行匹配,而後在對這個節點進行修改,好比說上面的當咱們訪問到 ArrowFunctionExpression
的時候,對 ArrowFunctionExpression
進行修改,變成普通函數
那麼咱們就能夠這麼寫:
const babel = require('@babel/core'); const code = `const fn = (a, b) => a + b`; // 轉換後 const fn = function(a, b) { return a + b } const arrowFnPlugin = { // 訪問者模式 visitor: { // 當訪問到某個路徑的時候進行匹配 ArrowFunctionExpression(path) { // 拿到節點 const node = path.node; console.log('ArrowFunctionExpression -> node', node); }, }, }; const r = babel.transform(code, { plugins: [arrowFnPlugin], }); console.log(r);
此時咱們拿到的結果是這樣的節點結果是 這樣的,其實就是 ArrowFunctionExpression
的 AST,此時咱們要作的是把 ArrowFunctionExpression
的結構替換成 FunctionExpression
的結構,可是須要咱們組裝相似的結構,這麼直接寫很麻煩,可是 babel 爲咱們提供了一個工具叫作 @babel/types
@babel/types
有兩個做用:
而後咱們使用的時候,須要常常查文檔,由於裏面的節點類型特別多,不是作編譯相關工做的是記不住怎麼多節點的
那麼接下來咱們就開始生成一個 FunctionExpression
,而後把以前的 ArrowFunctionExpression
替換掉,咱們能夠看 types
文檔,找到 functionExpression
,該方法接受相應的參數咱們傳遞過去便可生成一個 FunctionExpression
t.functionExpression(id, params, body, generator, async);
BlockStatement
咱們須要生成一個還須要生成一個 BlockStatement
,咱們接着看文檔找到 BlockStatement
接受的參數
t.blockStatement(body, directives);
看文檔說明,blockStatement
接受一個 body,那咱們把以前的 body 拿過來就能夠直接用,不過這裏 body 接受一個數組
咱們在看 AST 結構,函數表達式中的 BlockStatement
中的 body
是一個 ReturnStatement
組成的集合,因此還須要生成一個 ReturnStatement
如今咱們就能夠改寫 AST 了
ArrowFunctionExpression(path) { // 拿到節點而後替換節點 const node = path.node; // 拿到函數的參數 const params = node.params; const returnStatement = t.returnStatement(node.body); const blockStatement = t.blockStatement([returnStatement]); const functionExpression = t.functionExpression(null, params, blockStatement); // 替換原來的函數 path.replaceWith(functionExpression); }, // 結果 const fn = function (a, b) { return a + b; };
固然若是沒有返回語句的話咱們也能夠生成一個 ExpressionStatement
,只須要把 returnStatement
改成 ExpressionStatement
其餘邏輯不變
ArrowFunctionExpression(path) { // 拿到節點而後替換節點 const node = path.node; // 拿到函數的參數 const params = node.params; // 把 returnStatement 換成 expressionStatement 便可 const expressionStatement = t.expressionStatement(node.body); const blockStatement = t.blockStatement([expressionStatement]); const functionExpression = t.functionExpression(null, params, blockStatement); // 替換原來的函數 path.replaceWith(functionExpression); }, // 結果 const fn = function (a, b) { a + b; };
在開發中,咱們引入 UI 框架,好比 vue 中用到的 element-ui
,vant
或者 React
中的 antd
都支持全局引入和按需引入,默認是全局引入,若是須要按需引入就須要安裝一個 babel-plugin-import
的插件,將全局的寫法變成按需引入的寫法。
就拿我最近開發移動端用的 vant 爲例, import { Button } from 'vant'
這種寫法通過這個插件以後會變成 import Button from 'vant/lib/Button'
這種寫法,引用整個 vant 變成了我只用了 vant 下面的某一個文件,打包後的文件會比所有引入的文件大小要小不少
import { Button, Icon } from 'vant'
寫法轉換爲import Button from 'vant/lib/Button'; import Icon from 'vant/lib/Icon'
看一下兩個語法樹的區別
根據兩張圖分析咱們能夠獲得一些信息:
specifiers
)是兩個 ImportSpecifier
,第二張圖裏面是分開的,並且都是 ImportDefaultSpecifier
source
也不同ImportDeclaration
變成多個 ImportDeclaration
, 而後把單個 import 解構引入的 specifiers
部分 ImportSpecifier
轉換成多個 ImportDefaultSpecifier
並修改對應的 source
便可爲了方便傳遞參數,此次咱們寫到一個函數裏面,能夠方便傳遞轉換後拼接的目錄
這裏咱們須要用到的幾個類型,也須要在 types 官網上找對應的解釋
importDeclaration
類型/** * @param {Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>} specifiers (required) * @param {StringLiteral} source (required) */ t.importDeclaration(specifiers, source);
importDeclaration
中須要生成 ImportDefaultSpecifier
/** * @param {Identifier} local (required) */ t.importDefaultSpecifier(local);
importDeclaration
中還須要生成一個 StringLiteral
/** * @param {string} value (required) */ t.stringLiteral(value);
按照上面的分析,咱們開始上代碼
const babel = require('@babel/core'); const t = require('@babel/types'); const code = `import { Button, Icon } from 'vant'`; // import Button from 'vant/lib/Button' // import Icon from 'vant/lib/Icon' function importPlugin(opt) { const { libraryDir } = opt; return { visitor: { ImportDeclaration(path) { const node = path.node; // console.log("ImportDeclaration -> node", node) // 獲得節點的詳細說明,而後轉換成多個的 import 聲明 const specifiers = node.specifiers; // 要處理這個咱們作一些判斷,首先判斷不是默認導出咱們才處理,要考慮 import vant, { Button, Icon } from 'vant' 寫法 // 還要考慮 specifiers 的長度,若是長度不是 1 而且不是默認導出咱們才須要轉換 if (!(specifiers.length === 1 && t.isImportDefaultSpecifier(specifiers[0]))) { const result = specifiers.map(specifier => { const local = specifier.local; const source = t.stringLiteral( `${node.source.value}/${libraryDir}/${specifier.local.name}` ); // console.log("ImportDeclaration -> specifier", specifier) return t.importDeclaration([t.importDefaultSpecifier(local)], source); }); console.log('ImportDeclaration -> result', result); // 由於此次要替換的 AST 不是一個,而是多個的,因此須要 `path.replaceWithMultiple(result)` 來替換,可是一執行發現死循環了 path.replaceWithMultiple(result); } }, }, }; } const r = babel.transform(code, { plugins: [importPlugin({ libraryDir: 'lib' })], }); console.log(r.code);
看打印結果和轉換結果彷佛沒什麼問題,這個插件幾乎就實現了
可是咱們考慮一種狀況,若是用戶不所有按需加載了,按需加載只是一種選擇,若是用戶這麼寫了 import vant, { Button, Icon } from 'vant'
,那麼咱們這個插件就出現問題了
若是遇到這種寫法,那麼默認導入的他的 source
應該是不變的,咱們要把原來的 source
拿出來
因此還須要判斷一下,每個 specifier
是否是一個 ImportDefaultSpecifier
而後處理不一樣的 source
,完整處理邏輯應該以下
function importPlugin(opt) { const { libraryDir } = opt; return { visitor: { ImportDeclaration(path) { const node = path.node; // console.log("ImportDeclaration -> node", node) // 獲得節點的詳細說明,而後轉換成多個的 import 聲明 const specifiers = node.specifiers; // 要處理這個咱們作一些判斷,首先判斷不是默認導出咱們才處理,要考慮 import vant, { Button, Icon } from 'vant' 寫法 // 還要考慮 specifiers 的長度,若是長度不是 1 而且不是默認導出咱們才須要轉換 if (!(specifiers.length === 1 && t.isImportDefaultSpecifier(specifiers[0]))) { const result = specifiers.map(specifier => { let local = specifier.local, source; // 判斷是否存在默認導出的狀況 if (t.isImportDefaultSpecifier(specifier)) { source = t.stringLiteral(node.source.value); } else { source = t.stringLiteral( `${node.source.value}/${libraryDir}/${specifier.local.name}` ); } return t.importDeclaration([t.importDefaultSpecifier(local)], source); }); path.replaceWithMultiple(result); } }, }, }; }
在 babel 官網上有一句話 Babylon is a JavaScript parser used in Babel.
babel
使用的引擎是 babylon
,Babylon
並不是 babel
團隊本身開發的,而是 fork 的 acorn
項目,acorn
的項目本人在很早以前在興趣部落 1.0 在構建中使用,爲了是作一些代碼的轉換,是很不錯的一款引擎,不過 acorn
引擎只提供基本的解析 ast
的能力,遍歷還須要配套的 acorn-travesal
, 替換節點須要使用 acorn-,而這些開發,在 Babel 的插件體系開發下,變得一體化了(摘自 AlloyTeam 團隊的剖析 babel)
使用 babylon 編寫一個數組 rest 轉 Es5 語法的插件
把 const arr = [ ...arr1, ...arr2 ]
轉成 var arr = [].concat(arr1, arr2)
咱們使用 babylon 的話就不須要使用 @babel/core
了,只須要用到他裏面的 traverse
和 generator
,用到的包有 babylon、@babel/traverse、@babel/generator、@babel/types
先來看一下兩棵語法樹的區別
根據上圖咱們分析得出:
這段代碼的核心生成一個 callExpression 調用表達式,因此對應官網上的類型,咱們分析須要用到的 api
/** * @param {Expression} callee (required) * @param {Array<Expression | SpreadElement | JSXNamespacedName>} source (required) */ t.callExpression(callee, arguments);
/** * @param {Expression} object (required) * @param {if computed then Expression else Identifier} property (required) * @param {boolean} computed (default: false) * @param {boolean} optional (default: null) */ t.memberExpression(object, property, computed, optional);
/** * @param {Array<null | Expression | SpreadElement>} elements (default: []) */ t.arrayExpression(elements);
/** * @param {LVal} id (required) * @param {Expression} init (default: null) */ t.variableDeclarator(id, init); /** * @param {"var" | "let" | "const"} kind (required) * @param {Array<VariableDeclarator>} declarations (required) */ t.variableDeclaration(kind, declarations);
const babylon = require('babylon'); // 使用 babel 提供的包,traverse 和 generator 都是被暴露在 default 對象上的 const traverse = require('@babel/traverse').default; const generator = require('@babel/generator').default; const t = require('@babel/types'); const code = `const arr = [ ...arr1, ...arr2 ]`; // var arr = [].concat(arr1, arr2) const ast = babylon.parse(code, { sourceType: 'module', }); // 轉換樹 traverse(ast, { VariableDeclaration(path) { const node = path.node; const declarations = node.declarations; console.log('VariableDeclarator -> declarations', declarations); const kind = 'var'; // 邊界斷定 if ( node.kind !== kind && declarations.length === 1 && t.isArrayExpression(declarations[0].init) ) { // 取得以前的 elements const args = declarations[0].init.elements.map(item => item.argument); const callee = t.memberExpression(t.arrayExpression(), t.identifier('concat'), false); const init = t.callExpression(callee, args); const declaration = t.variableDeclarator(declarations[0].id, init); const variableDeclaration = t.variableDeclaration(kind, [declaration]); path.replaceWith(variableDeclaration); } }, });
異步終極解決方案:async + await
以同步的寫法處理異步代碼。一切都好,惟一有問題的就是要想捕獲代碼出現的問題須要使用 try/catch
包裹 await 代碼片斷。爲了程序的健壯性,就可能須要在 async 中頻繁的書寫 try/catch
邏輯,此時咱們能夠就可使用 ast 捕獲到相應的代碼而後處理沒有被 try/catch
的 await
語句
// 轉換前 async function func() { await asyncFn(); }
// 轉換後 async function func() { try { await asyncFn(); } catch (e) {} }
咱們發現咱們要作的就是在 AwaitExpression
await 表達式外層包裹一層 TryStatement
try 語句
那咱們要作的就是生成一個 tryStatement,查看對應的 api
/** * @param {BlockStatement} block (required) * @param {CatchClause} handler (default: null) * @param {BlockStatement} finalizer (default: null) */ t.tryStatement(block, handler, finalizer);
暫時先不考慮 CatchClause,先生成 try
/** * @param {Array<Statement>} body (required) * @param {Array<Directive>} directives (default: []) */ t.blockStatement(body, directives);
再根據 ast 樹結構中獲得,body 是由表達式語句(ExpressionStatement)組成
/** * @param {Expression} expression (required) */ t.expressionStatement(expression);
在 expressionStatement 中須要的 expression 就是咱們的當前捕獲到的節點,那麼咱們就能夠開始寫代碼了
咱們要在 AwaitExpression 中捕獲代碼,還須要判斷該代碼段的父節點沒有被 try/catch 包裹,能夠利用 path 參數的 findParent 方法向上遍歷全部父節點,判斷是否被 try/catch 的 Node 包裹
AwaitExpression(path) { // 首先保證 await 語句沒有被 try/catch 包裹 if (path.findParent(path => t.isTryStatement(path.node))) return; const expression = t.expressionStatement(path.node); const tryBlock = t.blockStatement([expression]); // 生成 catch --> console.log(e) const paramsE = t.identifier('e'); const memberExpression = t.MemberExpression(t.identifier('console'), t.identifier('log')); const consoleExpression = t.expressionStatement(t.callExpression(memberExpression, [paramsE])); const catchClause = t.catchClause(paramsE, t.blockStatement([consoleExpression])); const tryStatement = t.tryStatement(tryBlock, catchClause); // 數組 path.replaceWithMultiple([tryStatement]); } // 獲得的結果: // async function func() { // try { // await asyncFn(); // } catch (e) { // console.log(e); // } // }
另外咱們要考慮到 await 表達式可能出現其餘狀況,能夠直接聲明變量賦值,能夠直接賦值,而後就是剛剛處理的直接一個表達式
// 聲明變量賦值 const r = await asyncFn(); // 賦值 r = await asyncFn(); // 就是一個表達式 await asyncFn();
此時咱們能夠區分不一樣的狀況作不一樣的處理,再次觀察語法樹,發現他們的區別在 blockStatement 節點下面,那麼咱們就能夠直接替換這一級就能夠,順便把 catch 語句補充完整
此時咱們輸入的代碼以下:
async function func() { const r = await asyncFn1(); res = await asyncFn2(); await asyncFn3(); }
處理過程:
AwaitExpression(path) { // 首先保證 await 語句沒有被 try/catch 包裹 if (path.findParent(path => t.isTryStatement(path.node))) return; const parent = path.parent; let replacePath = null; if (t.isVariableDeclarator(parent) || t.isAssignmentExpression(parent)) { // 賦值和聲明的方式結構相似,都是在 AwaitExpression 中 path 的 parentPath.parentPath 上的節點就是 blockStatement 所須要的的參數,能夠直接這麼替換 replacePath = path.parentPath.parentPath; } else { // 若是隻是表達式的話,path.parentPath.node 就是 blockStatement 參數 replacePath = path.parentPath; } const tryBlock = t.blockStatement([replacePath.node]); // 生成 catch --> new Error(e) const paramsE = t.identifier('e'); const throwStatement = t.throwStatement(t.newExpression(t.identifier('Error'), [paramsE])); const catchClause = t.catchClause(paramsE, t.blockStatement([throwStatement])); const tryStatement = t.tryStatement(tryBlock, catchClause); replacePath.replaceWithMultiple([tryStatement]); }, // 獲得結果 // async function func() { // try { // const r = await asyncFn1(); // } catch (e) { // throw new Error(e); // } // try { // res = await asyncFn2(); // } catch (e) { // throw new Error(e); // } // try { // await asyncFn3(); // } catch (e) { // throw new Error(e); // } // }
和抽象語法樹相對的是具體語法樹(Concrete Syntax Tree
)簡稱 CST
(一般稱做分析樹)。通常的,在源代碼的翻譯和編譯過程當中,語法分析器建立出分析樹。一旦 AST 被建立出來,在後續的處理過程當中,好比語義分析階段,會添加一些信息。可參考抽象語法樹和具體語法樹有什麼區別?
關於 node 類型,全集大體以下:
(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier
Babel 有文檔對 AST 樹的詳細定義,可參考這裏
代碼以存放到 GitHub,地址