經過開發 Babel 插件來理解什麼是抽象語法樹(AST)

前言

說到 babel 你確定會先想到 babel 能夠將還未被瀏覽器實現的 ES6 規範轉換成可以運行 ES5 規範,或者能夠將 JSX 轉換爲瀏覽器能識別的 HTML 結構,那麼 babel 是如何進行這個轉換的步驟呢,下面我將經過開發一個簡單的 babel 插件來解釋這整個過程,但願你對 Babel 插件原理與 AST 有新的認知。node

Babel 運行階段

從上面的分析,咱們大概能猜出 Babel 的運行過程是:原始代碼 -> 修改代碼,那麼在這個轉換的過程當中,咱們須要知道如下三個重要的步驟。git

解析

首先須要將 JavaScript 字符串通過詞法分析、語法分析後,轉換爲計算機更易處理的表現形式,稱之爲「抽象語法樹(AST)」,這個步驟咱們使用了 Babylon 解析器。github

轉換

當 JavaScript 從字符串轉換爲 AST 後,咱們就能更方便地對其進行瀏覽、分析和有規律的修改,根據咱們的需求,將其轉換爲新的 AST,babel-traverse 是一個很好的轉換工具,使得咱們可以很便利的操做 AST 。npm

生成

最後,咱們將修改完的 AST 進行反向處理,生成 JavaScript 字符串,整個轉換過程也就完成了,這一步當中,咱們使用到了 babel-generator 模塊。編程

什麼是 AST

以前聽過一句話:「若是你能熟練地操做 AST ,那麼你真的能夠隨心所欲。」,當時並不理解其含義,直到真正瞭解 AST 後,才發現 AST 對編程語言的重要性是不可估量的。設計模式

在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫爲 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。樹上的每一個節點都表示源代碼中的一種結構。瀏覽器

之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。bash

JavaScript 程序通常是由一系列字符組成的,咱們可使用匹配的字符([], {}, ()),成對的字符('', "")和縮進讓程序解析起來更加簡單,可是對計算機來講,這些字符在內存中僅僅是個數值,並不能處理這些高級問題,因此咱們須要找到一種方式,將其轉換成計算機能理解的結構。babel

咱們簡單看下面的代碼:編程語言

let a = 2;
a * 8
複製代碼

將其轉換爲 AST 會是怎樣的呢,咱們使用 astexplorer 在線 AST 轉換工具,能夠獲得如下樹結構:

image

爲了更形象表述,咱們將其轉換爲更直觀的結構圖形:

image

AST 的根節點都是 Program ,這個例子中包含了兩部分:

  1. 一個變量申明(VariableDeclarator),將標識符(Identifier) a 賦值爲數值(NumericLiteral) 3。

  2. 一個二元表達式語句(BinaryExpression),描述爲標誌符(Identifier)爲 a,操做符(operator) + 和數值(NumericLiteral) 5。

這只是一個簡單的例子,在實際開發中,AST 將會是一個巨型節點樹,將字符串形式的源代碼轉換成樹狀的結構,計算機便能更方便地處理,咱們使用的 Babel 插件,也就是對 AST 進行插入/移動/替換/刪除節點,建立成新的 AST ,再將 AST 轉換爲字符串源代碼,這即是 Babel 插件的原理,之因此可以「隨心所欲」,其緣由就是能夠將原始代碼按照指定邏輯轉換爲你想要的代碼。

開發 Babel 插件 Demo

基礎概念

一個典型的 Babel 插件結構,以下代碼所示:

export default function(babel) {
  var t = babel.types;
  return {
    visitor: {
      ArrayExpression(path, state) {
          path.replaceWith(
            t.callExpression(
              t.memberExpression(t.identifier('mori'), t.identifier('vector')),
              path.node.elements
            )
          );
      },
      ASTNodeTypeHere(path, state) {}
    }
  };
};
複製代碼

咱們要關注的幾個點爲:

  • babel.types: 用來操做 AST 節點,如建立、轉換、校驗等。
  • vistor: Babel 採用遞歸的方式訪問 AST 的每一個節點,之因此叫作visitor,只是由於有個相似的設計模式叫作訪問者模式,如上述代碼中的 ArrayExpression ,當遍歷到 ArrayExpression 節點時,即觸發對應函數。
  • path: path 是指 AST 節點的對象,能夠用來獲取節點的屬性、節點之間的關聯。
  • state: 指插件的狀態,能夠用過 state 來獲取插件中的配置項。
  • ArrayExpression、ASTNodeTypeHere: 指 AST 中的節點類型。

需求分析

由於是 Demo ,咱們需求很簡單,咱們開發的 Bable 插件名稱叫 vincePlugin,在使用的時候,能配置插件的參數,使得插件能按照咱們配置的參數進行轉換。

// babel 參數配置

plugins: [
    [vincePlugin, {
        name: 'vince'
    }]
]
複製代碼

轉換效果:

var fool = [1,2,3];
// translate to =>
var fool = vince.init(1,2,3)
複製代碼

初始化項目

爲了你們更方便的閱讀代碼,源碼已經上傳到GitHub: babel-plugin-demo

瞭解了以上概念與需求後,咱們就能夠開始進行 Babel 插件開發,開始以前先建立一個項目目錄,初始化 npm ,並安裝 babel-core :

mkdir babel-plugin-demo && cd babel-plugin-demo
npm init -y
npm install --save-dev babel-core
複製代碼

建立 plugin.js babel 插件文件,咱們將會在這裏寫轉換的邏輯代碼:

// plugin.js
module.exports = function(babel) {
    var t = babel.types;
    return {
      visitor: {
        // ...
      }
    };
};
複製代碼

建立原始代碼 index.js

var fool = [1,2,3];
複製代碼

建立 test.js 測試函數,這裏咱們進行對插件的測試:

// test.js
var fs = require('fs');
var babel = require('babel-core');
var vincePlugin = require('./plugin');

// read the code from this file
fs.readFile('index.js', 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: [
        [vincePlugin, {
            name: 'vince'
        }]
    ]
  });

  // print the generated code to screen
  console.log(out.code);
});
複製代碼

咱們經過 node test.js,來測試 babel 插件的轉換輸出。

節點對比

  • 原始代碼 var fool = [1,2,3]; 經過 AST 分析出來的節點如圖:

image

  • 轉換後代碼 var bar = vince.init(1, 2, 3);,經過 AST 分析出來的節點如圖:

image

咱們經過用紅色標註來區分原始與轉換後的 AST 結構圖,如今咱們能夠很清晰的看到咱們須要替換的節點,將 ArrayExpression 替換爲 CallExpression ,在 CallExpression 節點中中增長一個 MemberExpression,而且保留原始的三個 NumericLiteral。

plugin 編寫

首先,咱們須要替換的是 ArrayExpression ,因此給 vistor 添加 ArrayExpression 方法。

// plugin.js
module.exports = function(babel) {
    var t = babel.types;
    return {
      visitor: {
        ArrayExpression: function(path, state) {
            // ...
        }
      }
    };
};
複製代碼

當 Babel 遍歷 AST 時,當發現含有 visitor 上有對呀節點方法時,即會觸發這個方法,而且將上下文傳入(path, state),在函數裏面咱們進行節點的分析和替換操做:

// plugin.js
module.exports = function(babel) {
    var t = babel.types;
    return {
      visitor: {
        ArrayExpression: function(path, state) {
            // 替換該節點
            path.replaceWith(
              // 建立一個 callExpression 
              t.callExpression(
                t.memberExpression(t.identifier(state.opts.name), t.identifier('init')),
                path.node.elements
              )
            );
        }
      }
    };
};
複製代碼

咱們須要將 ArrayExpression 替換爲 CallExpression,能夠經過 t.callExpression(callee, arguments) 來生成 CallExpression,第一個參數是 MemberExpression,經過t.memberExpression(object, property) 來生成,而後再將原有的三個 NumericLiteral 設置爲第二個參數,因而就完成了咱們的需求。

這裏咱們要注意 state.opts.name 中指的是配置 plugin 時,設置的 config 參數。

更多的轉換方式和節點屬性,能夠查閱 babel-types 的文檔

測試plugin

咱們回到test.js,運行node test.js,便會得出:

node test.js

=> var bar = vince.init(1, 2, 3);

複製代碼

到這裏,咱們簡易的 Babel 插件便完成好了,實際上的開發需求要複雜的多,可是主要的邏輯仍是離不開上面的幾個概念。

總結

仍是回到開始那句話「若是你能熟練地操做 AST ,那麼你真的能夠隨心所欲。」,咱們可以經過 AST 將原始代碼轉換成咱們所須要的任何代碼,甚至你能建立一個私人的 ESXXX,添加你創造的新規範。AST 並非一個很複雜的技術活,很大一部分能夠視爲「苦力活」,由於遇到複雜的轉換需求可能須要編寫寫不少邏輯代碼。

經過閱讀這篇文章,咱們瞭解了 Babel 插件的實現原理,而且實踐了一個 Plugin,除此以外,咱們也理解了 AST 的概念,認識到了其強大之處。

引用:

Babel 用戶手冊

Babel 插件手冊

相關文章
相關標籤/搜索