[實(shí)踐系列]Babel原理

前言

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

[實(shí)踐系列]前端路由

有興趣的同學(xué)可以關(guān)注 [實(shí)踐系列] 。 求star求follow~

Babel是什么?我為什么要了解它?

1. 什么是babel ?

Babel 是一個(gè) JavaScript 編譯器。他把最新版的javascript編譯成當(dāng)下可以執(zhí)行的版本,簡(jiǎn)言之,利用babel就可以讓我們?cè)诋?dāng)前的項(xiàng)目中隨意的使用這些新最新的es6,甚至es7的語法。

為了能用可愛的ES678910寫代碼,我必須了解它!

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

August 27, 2018 by Henry Zhu

歷經(jīng) 2 年,4k 多次提交,50 多個(gè)預(yù)發(fā)布版本以及大量社區(qū)援助,我們很高興地宣布發(fā)布 Babel 7。自 Babel 6 發(fā)布以來,已經(jīng)過了將近三年的時(shí)間!發(fā)布期間有許多要進(jìn)行的遷移工作,因此請(qǐng)?jiān)诎l(fā)布第一周與我們聯(lián)系。Babel 7 是更新巨大的版本:我們使它編譯更快,并創(chuàng)建了升級(jí)工具,支持 JS 配置,支持配置 "overrides",更多 size/minification 的選項(xiàng),支持 JSX 片段,支持 TypeScript,支持新提案等等!

Babel開發(fā)團(tuán)隊(duì)這么辛苦的為開源做貢獻(xiàn),為我們開發(fā)者提供更完美的工具,我為什么不去了解它呢?

(OS:求求你別更啦.老子學(xué)不動(dòng)啦~)

3. Babel擔(dān)任的角色

August 27, 2018 by Henry Zhu

我想再次介紹下過去幾年中 Babel 在 JavaScript 生態(tài)系統(tǒng)中所擔(dān)任的角色,以此展開本文的敘述。

起初,JavaScript 與服務(wù)器語言不同,它沒有辦法保證對(duì)每個(gè)用戶都有相同的支持,因?yàn)橛脩艨赡苁褂弥С殖潭炔煌臑g覽器(尤其是舊版本的 Internet Explorer)。如果開發(fā)人員想要使用新語法(例如 class A {}),舊瀏覽器上的用戶只會(huì)因?yàn)?SyntaxError 的錯(cuò)誤而出現(xiàn)屏幕空白的情況。

Babel 為開發(fā)人員提供了一種使用最新 JavaScript 語法的方式,同時(shí)使得他們不必?fù)?dān)心如何進(jìn)行向后兼容,如(class A {} 轉(zhuǎn)譯成 var A = function A() {})。

由于它能轉(zhuǎn)譯 JavaScript 代碼,它還可用于實(shí)現(xiàn)新的功能:因此它已成為幫助 TC39(制訂 JavaScript 語法的委員會(huì))獲得有關(guān) JavaScript 提案意見反饋的橋梁,并讓社區(qū)對(duì)語言的未來發(fā)展發(fā)表自己的見解。

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

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

Babel的運(yùn)行原理

babel

1.解析

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

1.詞法分析

詞法分析階段把字符串形式的代碼轉(zhuǎn)換為 令牌(tokens) 流。

你可以把令牌看作是一個(gè)扁平的語法片段數(shù)組:

 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: { ... } },
  ...
]

每一個(gè) 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 節(jié)點(diǎn)一樣它們也有 start,end,loc 屬性。

2.語法分析

語法分析階段會(huì)把一個(gè)令牌流轉(zhuǎn)換成 AST 的形式。 這個(gè)階段會(huì)使用令牌中的信息把它們轉(zhuǎn)換成一個(gè) AST 的表述結(jié)構(gòu),這樣更易于后續(xù)的操作。

簡(jiǎn)單來說,解析階段就是

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

Babel 使用 @babel/parser 解析代碼,輸入的 js 代碼字符串根據(jù) ESTree 規(guī)范生成 AST(抽象語法樹)。Babel 使用的解析器是 babylon。

什么是AST

2.轉(zhuǎn)換

轉(zhuǎn)換步驟接收 AST 并對(duì)其進(jìn)行遍歷,在此過程中對(duì)節(jié)點(diǎn)進(jìn)行添加、更新及移除等操作。 這是 Babel 或是其他編譯器中最復(fù)雜的過程。

Babel提供了@babel/traverse(遍歷)方法維護(hù)這AST樹的整體狀態(tài),并且可完成對(duì)其的替換,刪除或者增加節(jié)點(diǎn),這個(gè)方法的參數(shù)為原始AST和自定義的轉(zhuǎn)換規(guī)則,返回結(jié)果為轉(zhuǎn)換后的AST。

3.生成

代碼生成步驟把最終(經(jīng)過一系列轉(zhuǎn)換之后)的 AST 轉(zhuǎn)換成字符串形式的代碼,同時(shí)還會(huì)創(chuàng)建源碼映射(source maps)。

代碼生成其實(shí)很簡(jiǎn)單:深度優(yōu)先遍歷整個(gè) AST,然后構(gòu)建可以表示轉(zhuǎn)換后代碼的字符串。

Babel使用 @babel/generator 將修改后的 AST 轉(zhuǎn)換成代碼,生成過程可以對(duì)是否壓縮以及是否刪除注釋等進(jìn)行配置,并且支持 sourceMap。

babel.png

實(shí)踐前提

在這之前,你必須對(duì)Babel有了基本的了解,下面我們只簡(jiǎn)單的了解下babel的一些東西,以便于后面開發(fā)插件。

babel-core

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

transform : 用于字符串轉(zhuǎn)碼得到AST 。

傳送門

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

import babel from 'babel-core';
/*
 * @param {string} code 要轉(zhuǎn)譯的代碼字符串
 * @param {object} options 可選,配置項(xiàng)
 * @return {object} 
*/
babel.transform(code:String,options?: Object)
//返回一個(gè)對(duì)象(主要包括三個(gè)部分):
{
    generated code, //生成碼
    sources map, //源映射
    AST  //即abstract syntax tree,抽象語法樹
}

babel-types

Babel Types模塊是一個(gè)用于 AST 節(jié)點(diǎn)的 Lodash 式工具庫(譯注:Lodash 是一個(gè) JavaScript 函數(shù)工具庫,提供了基于函數(shù)式編程風(fēng)格的眾多工具函數(shù)), 它包含了構(gòu)造、驗(yàn)證以及變換 AST 節(jié)點(diǎn)的方法。 該工具庫包含考慮周到的工具方法,對(duì)編寫處理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

查看代碼對(duì)應(yīng)的AST樹結(jié)構(gòu)

Visitors (訪問者)

當(dāng)我們談及“進(jìn)入”一個(gè)節(jié)點(diǎn),實(shí)際上是說我們?cè)谠L問它們, 之所以使用這樣的術(shù)語是因?yàn)橛幸粋€(gè)訪問者模式(visitor)的概念。

訪問者是一個(gè)用于 AST 遍歷的跨語言的模式。 簡(jiǎn)單的說它們就是一個(gè)對(duì)象,定義了用于在一個(gè)樹狀結(jié)構(gòu)中獲取具體節(jié)點(diǎn)的方法。 這么說有些抽象所以讓我們來看一個(gè)例子。


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

// 你也可以先創(chuàng)建一個(gè)訪問者對(duì)象,并在稍后給它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

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

這是一個(gè)簡(jiǎn)單的訪問者,把它用于遍歷中時(shí),每當(dāng)在樹中遇見一個(gè) Identifier 的時(shí)候會(huì)調(diào)用 Identifier() 方法。

Paths(路徑)

AST 通常會(huì)有許多節(jié)點(diǎn),那么節(jié)點(diǎn)直接如何相互關(guān)聯(lián)呢? 我們可以使用一個(gè)可操作和訪問的巨大可變對(duì)象表示節(jié)點(diǎn)之間的關(guān)聯(lián)關(guān)系,或者也可以用Paths(路徑)來簡(jiǎn)化這件事情。

Path 是表示兩個(gè)節(jié)點(diǎn)之間連接的對(duì)象。

在某種意義上,路徑是一個(gè)節(jié)點(diǎn)在樹中的位置以及關(guān)于該節(jié)點(diǎn)各種信息的響應(yīng)式 Reactive 表示。 當(dāng)你調(diào)用一個(gè)修改樹的方法后,路徑信息也會(huì)被更新。 Babel 幫你管理這一切,從而使得節(jié)點(diǎn)操作簡(jiǎn)單,盡可能做到無狀態(tài)。

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

當(dāng)你有一個(gè) Identifier() 成員方法的訪問者時(shí),你實(shí)際上是在訪問路徑而非節(jié)點(diǎn)。 通過這種方式,你操作的就是節(jié)點(diǎn)的響應(yīng)式表示(譯注:即路徑)而非節(jié)點(diǎn)本身。

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};

Babel插件規(guī)則

Babel的插件模塊需要我們暴露一個(gè)function,function內(nèi)返回visitor對(duì)象。

//函數(shù)參數(shù)接受整個(gè)Babel對(duì)象,這里將它進(jìn)行解構(gòu)獲取babel-types模塊,用來操作AST。

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

    return {
        visitor:{
            
        }
    }
    
}

擼一個(gè)Babel ...插件 !!!

做一個(gè)簡(jiǎn)單的ES6轉(zhuǎn)ES3插件:
1. let,const 聲明 -> var 聲明  
2. 箭頭函數(shù) -> 普通函數(shù)

文件結(jié)構(gòu)

|-- index.js  程序入口
|-- plugin.js 插件實(shí)現(xiàn)
|-- before.js 轉(zhuǎn)化前代碼
|-- after.js  轉(zhuǎn)化后代碼
|-- package.json  

首先,我們先創(chuàng)建一個(gè)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作為我們的開發(fā)依賴,然后配置了npm run babel作為開發(fā)命令。

index.js


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

const fs = require('fs');

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

//使用babel-core的transform API 和插件進(jìn)行字符串->AST轉(zhuǎn)化。
const res = transform(`${before}`, {
  plugins: [require('./plugin')]
});

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


我們首先來實(shí)現(xiàn) 功能 1. let,const 聲明 -> var 聲明

let code = 1;

我們通過傳送門查看到上面代碼對(duì)應(yīng)的AST結(jié)構(gòu)為

image

我們可以看到這句聲明語句位于VariableDeclaration節(jié)點(diǎn),我們接下來只要操作VariableDeclaration節(jié)點(diǎn)對(duì)應(yīng)的kind屬性就可以啦~

before.js

const a = 123;

let b = 456;

plugin.js

module.exports = function({ types: t }) {
  return {
   //訪問者
    visitor: {
     //我們需要操作的訪問者方法(節(jié)點(diǎn))
      VariableDeclaration(path) {
        //該路徑對(duì)應(yīng)的節(jié)點(diǎn)
        const node = path.node;
        //判斷節(jié)點(diǎn)kind屬性是let或者const,轉(zhuǎn)化為var
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      }
    }
  };
};

ok~ 我們來看看效果!

npm run babel

after.js

var a = 123;

var b = 456;

沒錯(cuò),就是這么吊!!!功能1搞定,接下來實(shí)現(xiàn)功能2. 箭頭函數(shù) -> 普通函數(shù) (this指向暫不做處理~)

我們先來看看箭頭函數(shù)對(duì)應(yīng)的節(jié)點(diǎn)是什么?

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

我們通過傳送門查看到上面代碼對(duì)應(yīng)的AST結(jié)構(gòu)為

image

我們可以看到箭頭函數(shù)對(duì)應(yīng)的節(jié)點(diǎn)是ArrowFunctionExpression。

接下來我們?cè)賮砜纯雌胀ê瘮?shù)對(duì)應(yīng)的節(jié)點(diǎn)是什么?


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

我們通過傳送門查看到上面代碼對(duì)應(yīng)的AST結(jié)構(gòu)為

image

我們可以看到普通函數(shù)對(duì)應(yīng)的節(jié)點(diǎn)是FunctionExpression。

所以我們的實(shí)現(xiàn)思路只要進(jìn)行節(jié)點(diǎn)替換(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');
      },
      //箭頭函數(shù)對(duì)應(yīng)的訪問者方法(節(jié)點(diǎn))
      ArrowFunctionExpression(path) {
       //該路徑對(duì)應(yīng)的節(jié)點(diǎn)信息  
        let { id, params, body, generator, async } = path.node;
        //進(jìn)行節(jié)點(diǎn)替換 (arrowFunctionExpression->functionExpression)
        path.replaceWith(t.functionExpression(id, params, body, generator, async));
      }
    }
  };
};

滿懷激動(dòng)的

npm run babel

after.js

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

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

我們經(jīng)常會(huì)這樣寫箭頭函數(shù)來省略return。

let add = (x,y) =>x + y;

我們來試試 這樣能不能轉(zhuǎn)義

npm run babel

GG.控制臺(tái)飄紅~

下面我直接貼下最后的實(shí)現(xiàn),具體原因我覺得讀者自己研究或許更有趣~

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;
        //箭頭函數(shù)我們會(huì)簡(jiǎn)寫{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));
      }
    }
  };
};

小功告成

源碼地址

如果覺得有幫助到你,請(qǐng)給個(gè)star或者follow 支持下作者哈~接下來還會(huì)有很多干貨哦!!!

參考文獻(xiàn)

很棒的Babel手冊(cè)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容