[實踐系列]Babel原理

webfansplz發表於2019-01-14

前言

[實踐系列] 主要是讓我們通過實踐去加深對一些原理的理解。

[實踐系列]前端路由

有興趣的同學可以關注 [實踐系列] 。 求star求follow~

Babel是什麼?我們為什麼要了解它?

1. 什麼是babel ?

Babel 是一個 JavaScript 編譯器。他把最新版的javascript編譯成當下可以執行的版本,簡言之,利用babel就可以讓我們在當前的專案中隨意的使用這些新最新的es6,甚至es7的語法。

為了能用可愛的ES678910寫程式碼,我們必須瞭解它!

2. 可靠的工具來源於可怕的付出

August 27, 2018 by Henry Zhu

歷經 2 年,4k 多次提交,50 多個預釋出版本以及大量社群援助,我們很高興地宣佈釋出 Babel 7。自 Babel 6 釋出以來,已經過了將近三年的時間!釋出期間有許多要進行的遷移工作,因此請在釋出第一週與我們聯絡。Babel 7 是更新巨大的版本:我們使它編譯更快,並建立了升級工具,支援 JS 配置,支援配置 "overrides",更多 size/minification 的選項,支援 JSX 片段,支援 TypeScript,支援新提案等等!

Babel開發團隊這麼辛苦的為開源做貢獻,為我們開發者提供更完美的工具,我們為什麼不去了解它呢?

(OS:求求你別更啦.老子學不動啦~)

3. Babel擔任的角色

August 27, 2018 by Henry Zhu

我想再次介紹下過去幾年中 Babel 在 JavaScript 生態系統中所擔任的角色,以此展開本文的敘述。

起初,JavaScript 與伺服器語言不同,它沒有辦法保證對每個使用者都有相同的支援,因為使用者可能使用支援程度不同的瀏覽器(尤其是舊版本的 Internet Explorer)。如果開發人員想要使用新語法(例如 class A {}),舊瀏覽器上的使用者只會因為 SyntaxError 的錯誤而出現螢幕空白的情況。

Babel 為開發人員提供了一種使用最新 JavaScript 語法的方式,同時使得他們不必擔心如何進行向後相容,如(class A {} 轉譯成 var A = function A() {})。

由於它能轉譯 JavaScript 程式碼,它還可用於實現新的功能:因此它已成為幫助 TC39(制訂 JavaScript 語法的委員會)獲得有關 JavaScript 提案意見反饋的橋樑,並讓社群對語言的未來發展發表自己的見解。

Babel 如今已成為 JavaScript 開發的基礎。GitHub 目前有超過 130 萬個倉庫依賴 Babel,每月 npm 下載量達 1700 萬次,還擁有數百個使用者,其中包括許多主要框架(React,Vue,Ember,Polymer)以及著名公司(Facebook,Netflix,Airbnb)等。它已成為 JavaScript 開發的基礎,許多人甚至不知道它正在被使用。即使你自己沒有使用它,但你的依賴很可能正在使用 Babel。

即使你自己沒有使用它,但你的依賴很可能正在使用 Babel。怕不怕 ? 瞭解不瞭解 ?

Babel的執行原理

[實踐系列]Babel原理

1.解析

解析步驟接收程式碼並輸出 AST。 這個步驟分為兩個階段:詞法分析(Lexical Analysis) 和 語法分析(Syntactic Analysis)。

1.詞法分析

詞法分析階段把字串形式的程式碼轉換為 令牌(tokens) 流。

你可以把令牌看作是一個扁平的語法片段陣列:

 n * n;
複製程式碼
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]
複製程式碼

每一個 type 有一組屬性來描述該令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}
複製程式碼

和 AST 節點一樣它們也有 start,end,loc 屬性。

2.語法分析

語法分析階段會把一個令牌流轉換成 AST 的形式。 這個階段會使用令牌中的資訊把它們轉換成一個 AST 的表述結構,這樣更易於後續的操作。

簡單來說,解析階段就是

code(字串形式程式碼) -> tokens(令牌流) -> AST(抽象語法樹)
複製程式碼

Babel 使用 @babel/parser 解析程式碼,輸入的 js 程式碼字串根據 ESTree 規範生成 AST(抽象語法樹)。Babel 使用的解析器是 babylon

什麼是AST

2.轉換

轉換步驟接收 AST 並對其進行遍歷,在此過程中對節點進行新增、更新及移除等操作。 這是 Babel 或是其他編譯器中最複雜的過程。

Babel提供了@babel/traverse(遍歷)方法維護這AST樹的整體狀態,並且可完成對其的替換,刪除或者增加節點,這個方法的引數為原始AST和自定義的轉換規則,返回結果為轉換後的AST。

3.生成

程式碼生成步驟把最終(經過一系列轉換之後)的 AST 轉換成字串形式的程式碼,同時還會建立原始碼對映(source maps)。

程式碼生成其實很簡單:深度優先遍歷整個 AST,然後構建可以表示轉換後程式碼的字串。

Babel使用 @babel/generator 將修改後的 AST 轉換成程式碼,生成過程可以對是否壓縮以及是否刪除註釋等進行配置,並且支援 sourceMap。

[實踐系列]Babel原理

實踐前提

在這之前,你必須對Babel有了基本的瞭解,下面我們簡單的瞭解下babel的一些東西,以便於後面開發外掛。

babel-core

babel-core是Babel的核心包,裡面存放著諸多核心API,這裡說下transform。

transform : 用於字串轉碼得到AST 。

傳送門

//安裝
npm install  babel-core -D;

import babel from 'babel-core';
/*
 * @param {string} code 要轉譯的程式碼字串
 * @param {object} options 可選,配置項
 * @return {object} 
*/
babel.transform(code:String,options?: Object)
//返回一個物件(主要包括三個部分):
{
    generated code, //生成碼
    sources map, //源對映
    AST  //即abstract syntax tree,抽象語法樹
}
複製程式碼

babel-types

Babel Types模組是一個用於 AST 節點的 Lodash 式工具庫(譯註:Lodash 是一個 JavaScript 函式工具庫,提供了基於函數語言程式設計風格的眾多工具函式), 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯非常有用。 傳送門

npm install babel-types -D;  

import traverse from "babel-traverse";

import * as t from "babel-types";

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});
複製程式碼

JS CODE -> AST

檢視程式碼對應的AST樹結構

Visitors (訪問者)

當我們談及“進入”一個節點,實際上是說我們在訪問它們, 之所以使用這樣的術語是因為有一個訪問者模式(visitor)的概念。

訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個物件,定義了用於在一個樹狀結構中獲取具體節點的方法。 這麼說有些抽象所以讓我們來看一個例子。


const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// 你也可以先建立一個訪問者物件,並在稍後給它新增方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

複製程式碼

注意: Identifier() { ... } 是 Identifier: { enter() { ... } } 的簡寫形式

這是一個簡單的訪問者,把它用於遍歷中時,每當在樹中遇見一個 Identifier 的時候會呼叫 Identifier() 方法。

Paths(路徑)

AST 通常會有許多節點,那麼節點直接如何相互關聯呢? 我們可以使用一個可操作和訪問的巨大可變物件表示節點之間的關聯關係,或者也可以用Paths(路徑)來簡化這件事情。

Path 是表示兩個節點之間連線的物件。

在某種意義上,路徑是一個節點在樹中的位置以及關於該節點各種資訊的響應式 Reactive 表示。 當你呼叫一個修改樹的方法後,路徑資訊也會被更新。 Babel 幫你管理這一切,從而使得節點操作簡單,儘可能做到無狀態。

Paths in Visitors(存在於訪問者中的路徑)

當你有一個 Identifier() 成員方法的訪問者時,你實際上是在訪問路徑而非節點。 通過這種方式,你操作的就是節點的響應式表示(譯註:即路徑)而非節點本身。

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};
複製程式碼

Babel外掛規則

Babel的外掛模組需要我們暴露一個function,function內返回visitor物件。

//函式引數接受整個Babel物件,這裡將它進行解構獲取babel-types模組,用來操作AST。

module.exports = function({types:t}){

    return {
        visitor:{
            
        }
    }
    
}
複製程式碼

擼一個Babel ...外掛 !!!

做一個簡單的ES6轉ES3外掛:
1. let,const 宣告 -> var 宣告  
2. 箭頭函式 -> 普通函式
複製程式碼

檔案結構

|-- index.js  程式入口
|-- plugin.js 外掛實現
|-- before.js 轉化前程式碼
|-- after.js  轉化後程式碼
|-- package.json  
複製程式碼

首先,我們先建立一個package.json。

npm init
複製程式碼

package.json

{
  "name": "babelplugin",
  "version": "1.0.0",
  "description": "create babel plugin",
  "main": "index.js",
  "scripts": {
    "babel": "node ./index.js"
  },
  "author": "webfansplz",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.2.2"
  }
}

複製程式碼

可以看到,我們首先下載了@babel/core作為我們的開發依賴,然後配置了npm run babel作為開發命令。

index.js


const { transform } = require('@babel/core');

const fs = require('fs');

//讀取需要轉換的js字串
const before = fs.readFileSync('./before.js', 'utf8');

//使用babel-core的transform API 和外掛進行字串->AST轉化。
const res = transform(`${before}`, {
  plugins: [require('./plugin')]
});

// 存在after.js刪除
fs.existsSync('./after.js') && fs.unlinkSync('./after.js');
// 寫入轉化後的結果到after.js
fs.writeFileSync('./after.js', res.code, 'utf8');


複製程式碼

我們首先來實現 功能 1. let,const 宣告 -> var 宣告

let code = 1;

複製程式碼

我們通過傳送門檢視到上面程式碼對應的AST結構為

[實踐系列]Babel原理

我們可以看到這句宣告語句位於VariableDeclaration節點,我們接下來只要操作VariableDeclaration節點對應的kind屬性就可以啦~

before.js

const a = 123;

let b = 456;
複製程式碼

plugin.js

module.exports = function({ types: t }) {
  return {
   //訪問者
    visitor: {
     //我們需要操作的訪問者方法(節點)
      VariableDeclaration(path) {
        //該路徑對應的節點
        const node = path.node;
        //判斷節點kind屬性是let或者const,轉化為var
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      }
    }
  };
};
複製程式碼

ok~ 我們來看看效果!

npm run babel
複製程式碼

after.js

var a = 123;

var b = 456;
複製程式碼

沒錯,就是這麼吊!!!功能1搞定,接下來實現功能2. 箭頭函式 -> 普通函式 (this指向暫不做處理~)

我們先來看看箭頭函式對應的節點是什麼?

let add = (x, y) => {
  return x + y;
};
複製程式碼

我們通過傳送門檢視到上面程式碼對應的AST結構為

[實踐系列]Babel原理

我們可以看到箭頭函式對應的節點是ArrowFunctionExpression。

接下來我們再來看看普通函式對應的節點是什麼?


let add = function(x, y){
  return x + y;
};

複製程式碼

我們通過傳送門檢視到上面程式碼對應的AST結構為

[實踐系列]Babel原理

我們可以看到普通函式對應的節點是FunctionExpression。

所以我們的實現思路只要進行節點替換(ArrowFunctionExpression->FunctionExpression)就可以啦。

plugin.js


module.exports = function({ types: t }) {
  return {
    visitor: {
      VariableDeclaration(path) {
        const node = path.node;
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      },
      //箭頭函式對應的訪問者方法(節點)
      ArrowFunctionExpression(path) {
       //該路徑對應的節點資訊  
        let { id, params, body, generator, async } = path.node;
        //進行節點替換 (arrowFunctionExpression->functionExpression)
        path.replaceWith(t.functionExpression(id, params, body, generator, async));
      }
    }
  };
};

複製程式碼

滿懷激動的

npm run babel
複製程式碼

after.js

var add = function (x, y) {
  return x + y;
};
複製程式碼

驚不驚喜 ? 意不意外 ? 你以為這樣就結束了嗎 ? 那你就太年輕啦。

我們經常會這樣寫箭頭函式來省略return。

let add = (x,y) =>x + y;
複製程式碼

我們來試試 這樣能不能轉義

npm run babel
複製程式碼

GG.控制檯飄紅~

下面我直接貼下最後的實現,具體原因我覺得讀者自己研究或許更有趣~

plugin.js

module.exports = function({ types: t }) {
  return {
    visitor: {
      VariableDeclaration(path) {
        const node = path.node;
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      },
      ArrowFunctionExpression(path) {
        let { id, params, body, generator, async } = path.node;
        //箭頭函式我們會簡寫{return a+b} 為 a+b    
        if (!t.isBlockStatement(body)) {    
          const node = t.returnStatement(body);
          body = t.blockStatement([node]);
        }
        path.replaceWith(t.functionExpression(id, params, body, generator, async));
      }
    }
  };
};

複製程式碼

小功告成

原始碼地址

如果覺得有幫助到你,請給個star或者follow 支援下作者哈~接下來還會有很多幹貨哦!!!

參考文獻

很棒的Babel手冊

相關文章