[實踐系列]Babel原理

前言

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

[實踐系列]前端路由前端

有興趣的同窗能夠關注 [實踐系列] 。 求star求follow~java

Babel是什麼?咱們爲何要了解它?

1. 什麼是babel ?

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

爲了能用可愛的ES678910寫代碼,咱們必須瞭解它!git

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

August 27, 2018 by Henry Zhues6

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

Babel開發團隊這麼辛苦的爲開源作貢獻,爲咱們開發者提供更完美的工具,咱們爲何不去了解它呢?web

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

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的運行原理

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-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結構爲

咱們能夠看到這句聲明語句位於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結構爲

咱們能夠看到箭頭函數對應的節點是ArrowFunctionExpression。

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

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

複製代碼

咱們經過傳送門查看到上面代碼對應的AST結構爲

咱們能夠看到普通函數對應的節點是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手冊

相關文章
相關標籤/搜索