使用 Babel 進行抽象語法樹操做

什麼是抽象語法樹

wiki: 抽象語法樹( Abstract Syntax Tree,AST ),或簡稱語法樹( Syntax tree ),是源代碼語法結構的一種抽象表示。 它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。 之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。前端

在咱們前端,能夠經過 Javascript 解析器將咱們程序的源代碼映射成爲一棵語法樹,而樹的每一個節點對應着代碼裏的一種結構;好比表達式,聲明語句,賦值語句等都會被映射爲語法樹上的一個節點,進而咱們就能夠經過操做語法樹上的節點來控制咱們的源代碼;初步瞭解時可能感受這個概念自己就比較抽象,可是基於它的應用咱們卻一點都不陌生,好比:單文件組件 .vue 文件的解析、爲咱們轉碼 ES6 語法的 babeljs、爲咱們壓縮混淆代碼的 UglifyJS 等等;vue

亮劍 Babeljs

Babel is a JavaScript compiler. Use next generation JavaScript, today.node

Babel是一個Javascript編譯器,他能讓你如今就開始使用將來版的Javascript。曾經的Javascript因爲語言自己的設計缺陷,飽受程序員們的詬病,現在隨着ES語言規範的制定與發展,加上Typescript的橫空出世,Javascript開始逐年霸佔最流行語言的榜首;Babel是推進這一進程的重要推手,他能將ES6/7/8/9/10轉換爲瀏覽器可以兼容的Javascript,從如今開始,您就可使用最新的ES語法規範,甚至處於草案階段的規範;而這都是一切都是史無前例的!程序員

接下來咱們將使用Babel提供的相關AST操做的模塊來「改頭換面」咱們的代碼;編程

  • @babel/parser經過該模塊來解析咱們的代碼生成AST抽象語法樹;瀏覽器

  • @babel/traverse經過該模塊對AST節點進行遞歸遍歷;babel

  • @babel/types經過該模塊對具體的AST節點進行進行增、刪、改、查;app

  • @babel/generator經過該模塊能夠將修改後的AST生成新的代碼;框架

無中生有

Talk is cheap, show me your code!編程語言

如今假設咱們在代碼裏使用了一個叫log的方法,如:log('Hello, world!');,可是咱們並無聲明或定義該方法,通常狀況下咱們的代碼將會報一個log is not defined的錯誤,如今咱們經過操做ASTlog修改成console.log,這樣咱們的代碼就不會報錯了;

astexplorer.net網站裏咱們能夠在線將代碼轉爲AST,以下圖:log('Hello, world!')AST樹形結構圖;

通常狀況下,咱們從body層級看起,其下的每一層級都有一個type字段,該字段很是重要,直接影響咱們該如何對該語句或表達式進行操做,具體請看後面講到的@babel/types

拿到代碼,咱們首先經過@babel/parser生成如上圖所示結構的AST

const {parse} = require('@babel/parser');
const codes = "log('Hello, world!');";

const ast = parse(codes, {
  sourceType: "module"
});
複製代碼

對照AST分析咱們須要作什麼,新手強烈推薦使用astexplorer.net,好比在這裏,咱們的需求是將log函數轉換爲console.log,經過在線AST解析,咱們清楚的看到了log對應的AST節點類型爲Identifier,一樣的,咱們換成console.log('Hello, world!');,能夠看到console.log對應的AST節點類型爲MemberExpression,因此,咱們的需求變爲將此處的Identifier變爲MemberExpression,不要想固然的覺得直接把type屬性改個值就 ok 了,接下來看看,如何將Identifier類型的節點修改成MemberExpression類型的節點;

輪到@babel/types上場了,前面兩步,咱們的焦點主要在AST節點的type字段上,事實上,每個type@babel/types裏都有一個同名的方法(首字母小寫)用來建立該類型的節點,好比建立Identifier類型的節點,咱們可使用t.identifier方法;

好了,先來建立MemberExpression類型的console.log,此處對於新手會比較棘手,推薦好好觀察在線的AST樹進行反推,找到目標節點的type,接着在@babel/types文檔裏搜相應type的方法;如圖,咱們在文檔裏搜到memberExpression方法的定義,接着開始建立console.log節點;

const t = require('@babel/types');

function createMemberExpression() {
  return t.memberExpression(
    t.identifier('console'),
    t.identifier('log')
  );
}
複製代碼

如上,咱們就能夠經過createMemberExpression方法來生成console.log來替換log了,問題來了,如何替換?

固然,替換以前,咱們須要在AST樹上找到對應的節點,經過@babel/traverse咱們能夠對AST樹的節點進行遍歷;

const {default: traverse} = require('@babel/traverse');

traverse(ast, visitor);
複製代碼

visitor是一個由各類type或者是enterexit組成的對象,由此肯定在遍歷的過程當中匹配到某種類型的節點後該如何操做,如咱們的需求是將Identifier類型的log節點替換爲MemberExpression類型的console.log,咱們能夠這樣定義visitor

const visitor = {
  Identifier(path) {
    const {node} = path;
    if(node && node.name === 'log') {
      path.replaceWith(createMemberExpression());
      path.stop();
    }
  }
}
複製代碼

經過traverse方法咱們能夠定義各類類型節點的操做方式,回調函數的path參數提供了豐富的增、刪、改、查以及類型斷言的方法,好比replaceWith/remove/find/isMemberExpression

最後,咱們將修改後的AST轉換爲Javascript代碼:

const {code} = generate(ast, { /* options */ }, codes);
複製代碼

到這一步,咱們就已經能夠將代碼裏的log方法替換爲console.log了;觸類旁通,咱們是否是能夠放開一下想象力:本身定義某種有意思或者創造性的語法規範,而後經過AST操做變換成常規的Javascript

以上例子的代碼彙總爲:

const t = require('@babel/types');
const {parse} = require('@babel/parser');
const {default: traverse} = require('@babel/traverse');
const {default: generate} = require('@babel/generator');

const codes = "log('Hello, world!');";

const ast = parse(codes, {
  sourceType: "module"
});

const visitor = {
  Identifier(path) {
    const {node} = path;
    if(node && node.name === 'log') {
      path.replaceWith(createMemberExpression());
      path.stop();
    }
  }
}

traverse(ast, visitor);

const {code} = generate(ast, { /* options */ }, codes);

console.log(code);
// console.log('Hello, world!');

function createMemberExpression() {
  return t.memberExpression(
    t.identifier('console'),
    t.identifier('log')
  );
}
複製代碼

總結

以上, 經過babeljs提供的相關模塊對AST操做進行了初步的實踐; 2019 年已過一半,今年無疑跨端應用火了,好比,Taro、uni-app 等,而這些框架的成功通通離不開的就是對AST的熟練掌握;因此呢,還等什麼,如今上車還來得及,若是你對前端報有天馬行空的想象,那麼我想了解AST將會助你一臂之力!

相關文章
相關標籤/搜索