babel插件實(shí)踐(一)babel編譯原理分析

前言

我們都知道在前端編譯構(gòu)建工具出現(xiàn)之前,前端項(xiàng)目基本都是用es5瀏覽器識(shí)別的語法來實(shí)現(xiàn)的。(jquery,es5...)。隨著前端技術(shù)的發(fā)展(es6甚至更新語法的問世),瀏覽器是不能識(shí)別這些新語法的。那么就出現(xiàn)了編譯構(gòu)建工具,其中babel扮演著舉足輕重的角色。那么下邊我們來探索一下babel究竟是什么?

小編推薦福利,精彩內(nèi)容請(qǐng)點(diǎn)擊鏈接,點(diǎn)擊這里

babel是什么?

官方介紹

Babel 是一個(gè) JavaScript 編譯器。

Babel 是一個(gè)工具鏈,主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中。

簡(jiǎn)單的說就是為了保證javascript在瀏覽器上正常運(yùn)行,需要把瀏覽器不識(shí)別的語法轉(zhuǎn)換成瀏覽器識(shí)別的預(yù)發(fā),其中轉(zhuǎn)換這一步驟就是babel做的事情,在計(jì)算機(jī)編程中這一步驟也會(huì)被叫做編譯。

其實(shí)編譯涉及的東西很多,有興趣的同學(xué)可以了解一下《編譯原理》,編譯原理主要包括詞法分析,語法分析,語義分析,中間代碼生成,代碼優(yōu)化,目標(biāo)代碼生成這幾大步驟,這里就不做過多介紹了,此處省略一百萬字...

其實(shí)babel的工作流程和編譯原理中的編譯流程相對(duì)簡(jiǎn)單。我們可以歸納如下幾個(gè)步驟:

  • 詞法分析
  • 語法分析
  • 代碼轉(zhuǎn)換
  • 代碼生成

babel的整體工作流程如下圖:

未命名文件.png

其中分為詞法分析和預(yù)發(fā)分析兩步可以合并成解析(parse)過程

從上圖可以看到編譯從開始到結(jié)束有一個(gè)最重要的東西,抽象語法樹/AST的知識(shí),以下簡(jiǎn)稱AST,babel編譯代碼的整個(gè)流程都離不開它。

抽象語法樹(AST)

抽象語法樹是高級(jí)編程語言(Java、JavaScript等)轉(zhuǎn)換成機(jī)器語言的橋梁。解析器會(huì)根據(jù)ECMAScript 標(biāo)準(zhǔn)「JavaScript語言規(guī)范」來對(duì)代碼字符串進(jìn)行詞法分析,拆分成一個(gè)個(gè)詞法單元,再遍歷各個(gè)詞法單元進(jìn)行語法分析構(gòu)造出AST。我們通過如下代碼來分析原理:

let age = 10;
age = age + 20;

詞法分析

詞法分析階段是對(duì)源代碼進(jìn)行“分詞”,它接收一段源代碼,然后執(zhí)行一段tokenize函數(shù),把代碼分割成被稱為tokens的東西。tokens是一個(gè)數(shù)組,由一些代碼的碎片組成,比如數(shù)字、標(biāo)點(diǎn)符號(hào)、運(yùn)算符號(hào)等等等等,例如這樣:

這里我們利用在線工具把上述代碼進(jìn)行詞法分析的結(jié)果如下:
詞法分析工具

[
    { "type": "Keyword", "value": "let"},
    { "type": "Identifier", "value": "age"},
    { "type": "Punctuator", "value": "="},
    { "type": "Numeric", "value": "10"},
    { "type": "Punctuator", "value": ";"},
    { "type": "Identifier", "value": "age"},
    { "type": "Punctuator", "value": "="},
    { "type": "Identifier", "value": "age"},
    { "type": "Punctuator", "value": "+"},
    { "type": "Numeric", "value": "20"},
    { "type": "Punctuator", "value": ";"}
]

從詞法分析結(jié)果可以看出,最終結(jié)果就是把代碼解析成各個(gè)單詞(let,age,+,=等等)
babel-tokenizer方法實(shí)現(xiàn)

語法分析

在詞法分析之后,語法分析會(huì)把詞法分析得到的tokens轉(zhuǎn)化為AST,有興趣的可以閱讀一下babel源碼babel轉(zhuǎn)化AST源碼

AST抽象語法樹是babel插件的核心概念,在編寫自定義babel插件也會(huì)用到,因?yàn)樵诖a轉(zhuǎn)換其實(shí)就是針對(duì)AST語法樹各個(gè)節(jié)點(diǎn)進(jìn)行的操作

下邊推薦一個(gè)在線生成AST語法樹工具

生成的AST太長(zhǎng),這里不展示了,有興趣的可以在線嘗試。

AST樹,顧名思義數(shù)據(jù)結(jié)構(gòu)中典型的一種數(shù)據(jù)類型-樹,那么我們也知道,樹都有一個(gè)根節(jié)點(diǎn),也會(huì)有許多子節(jié)點(diǎn)。AST語法樹是會(huì)有一個(gè)type值是Program的根節(jié)點(diǎn),如下

{
  "type": "Program",
  "start": 0,
  "end": 29,
  "body": [],
  "sourceType": "module"
}

經(jīng)過觀察子節(jié)點(diǎn),其實(shí)子節(jié)點(diǎn)(包括根節(jié)點(diǎn))都有相同的數(shù)據(jù)結(jié)構(gòu),如下

{
    "type": "VariableDeclaration",
    "start": 0,
    "end": 13,
    "declarations": [...],
    "kind": "let"
}
{
  type: "Identifier",
  name: ...
}
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

以上只是列舉了幾個(gè)不同類型的節(jié)點(diǎn)(注意:出于簡(jiǎn)化的目的移除了某些屬性),其實(shí)AST語法樹就是由這些節(jié)點(diǎn)組成的,它們組合在一起可以描述用于靜態(tài)分析的程序語法。

從上邊可以得出結(jié)論:每一個(gè)節(jié)點(diǎn)都有一個(gè)type字段代表節(jié)點(diǎn)的類型,還定義了一些附加屬性用來進(jìn)一步描述該節(jié)點(diǎn)類型。

babel編譯

babel編譯流程代碼演示

上邊我們也給出了babel編譯代碼的流程圖,下邊我們具體實(shí)踐一下babel編譯流程

這里先簡(jiǎn)單創(chuàng)建一個(gè)空項(xiàng)目,步驟如下:

創(chuàng)建一個(gè)文件夾,使用npm init -y創(chuàng)建package.json

然后在項(xiàng)目下創(chuàng)建src/index.js文件

let name = "hello babel";
console.log(name);

package.json中添加執(zhí)行scripts

"scripts": {
  "build": "node src/index.js"
}

為方便我們后邊打斷點(diǎn)debug,這里我們利用vscode工具給我們生成一個(gè)launch.js文件,添加自己的launch配置

1629953490(1).jpg

我的launch.js內(nèi)容如下

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Debug",
            "runtimeExecutable": "npm",
            "restart": true,
            "console": "integratedTerminal",
            "runtimeArgs": ["run-script", "build"],
        }
    ]
}

具體配置請(qǐng)小伙伴們搜一下...

然后我們點(diǎn)想要斷點(diǎn)的地方打上斷點(diǎn),擊上圖debug按鈕運(yùn)行即可,如下

image.png

更多關(guān)于vscode調(diào)試工具請(qǐng)自行學(xué)習(xí),這里不做過多講述

接下來正式回到babel編譯正題,我們需要安裝3個(gè)babel官方提供的插件

npm install -D @babel/parser @babel/generator @babel/traverse

接下來了解一下這3個(gè)包的簡(jiǎn)單用法,修改src/index.js代碼如下

const Parser = require("@babel/parser")
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

// 源代碼
const compilerCode = `
let age = 10;
age = age + 20;
`
// 源代碼經(jīng)過parse過程(詞法分析/語法分析)轉(zhuǎn)換成AST語法樹
const ast = Parser.parse(compilerCode, {});

// 對(duì)AST語法樹上的節(jié)點(diǎn)進(jìn)行操作
traverse(ast);

// ast語法樹生成最終代碼
const codeObj = generator(ast, {}, compilerCode);

console.log(codeObj.code);

以上只是簡(jiǎn)單的用代碼形式演示了babel是如何編譯代碼的。

編譯生成的代碼如下

let age = 10;
age = age + 20;

這里和源代碼比較一下發(fā)現(xiàn)沒有什么差別,因?yàn)槲覀儧]有使用插件對(duì)代碼進(jìn)行操作(壓縮,混淆,優(yōu)化等等)

@babel/parser 包的parse方法傳入源代碼,進(jìn)行詞法分析合語法分析,最終生成AST抽象語法樹
@babel/traversetraverse方法接收AST抽象語法樹并對(duì)其進(jìn)行遍歷(深度遍歷),在此過程中對(duì)節(jié)點(diǎn)進(jìn)行添加、更新及移除等操作。 這是Babel或是其他編譯器中最復(fù)雜的過程,同時(shí)也是插件將要介入工作的地方,插件部分我們后邊在講
@babel/generatorgenerator方法接收的AST抽象語法樹轉(zhuǎn)換成字符串形式的代碼,同時(shí)還會(huì)創(chuàng)建源碼映射(sourceMap,根據(jù)傳入的參數(shù)控制是否生成sourceMap

上邊也提到了,@babel/traversetraverse轉(zhuǎn)換過程是深度遍歷整顆樹對(duì)節(jié)點(diǎn)進(jìn)行操作,它會(huì)訪問樹中的所有節(jié)點(diǎn)。這時(shí)候該方法第二個(gè)參數(shù)就起到作用了。這個(gè)參數(shù)是一個(gè)對(duì)象,對(duì)象每個(gè)屬性是一個(gè)鉤子函數(shù)。這個(gè)對(duì)象的屬性值除了支持AST語法樹節(jié)點(diǎn)的type值外,還有enter,exit;也就是在遍歷每個(gè)節(jié)點(diǎn)的時(shí)候會(huì)先進(jìn)入enter鉤子函數(shù),如果存在該節(jié)點(diǎn)對(duì)應(yīng)的鉤子函數(shù),還會(huì)執(zhí)行該鉤子函數(shù),最后在訪問該節(jié)點(diǎn)結(jié)束的時(shí)候執(zhí)行exit鉤子函數(shù)...

修改轉(zhuǎn)換代碼如下:

traverse(ast, {
    enter(path){
        console.log(path.type, "-進(jìn)入")
    },
    exit(path){
        console.log(path.type,"-離開")
    }
});

再次debug運(yùn)行代碼

Program -進(jìn)入
VariableDeclaration -進(jìn)入
VariableDeclarator -進(jìn)入 
Identifier -進(jìn)入
Identifier -離開
NumericLiteral -進(jìn)入     
NumericLiteral -離開
VariableDeclarator -離開
VariableDeclaration -離開
ExpressionStatement -進(jìn)入
AssignmentExpression -進(jìn)入
Identifier -進(jìn)入
Identifier -離開
BinaryExpression -進(jìn)入
Identifier -進(jìn)入
Identifier -離開
NumericLiteral -進(jìn)入
NumericLiteral -離開
BinaryExpression -離開
AssignmentExpression -離開
ExpressionStatement -離開
Program -離開

從上邊打印結(jié)果可以看出,遍歷到每個(gè)節(jié)點(diǎn)時(shí)都有執(zhí)行enter,exit函數(shù)。合AST抽象語法樹對(duì)比,也能看出確實(shí)屬于深度優(yōu)先遞歸遍歷

接下來我們?cè)谔砑?code>VariableDeclaration鉤子函數(shù)代碼如下,

traverse(ast, {
    enter(path){
        
    },
    VariableDeclaration(path){
        console.log(path.type)
    },
    exit(path){
        
    }
});

再次debug運(yùn)行代碼,VariableDeclaration函數(shù)會(huì)執(zhí)行一次,因?yàn)槲覀冞@個(gè)AST語法樹只有一個(gè)VariableDeclaration類型的節(jié)點(diǎn)。

到這里,相信很多小伙伴注意到了,鉤子函數(shù)path參數(shù)是做什么的?

path代表著在遍歷AST的過程中連接兩個(gè)節(jié)點(diǎn)的路徑,你可以通過path.node獲取當(dāng)前的節(jié)點(diǎn)path.parent.node獲得父節(jié)點(diǎn),它也提供了path.replaceWith, path.removeAPI,這樣就能通過一定條件來獲取特點(diǎn)的節(jié)點(diǎn)進(jìn)行修改了。

到這里可能有的小伙伴還有一個(gè)問題,babel可能定義了很多節(jié)點(diǎn)類型,我們?cè)趺粗啦煌愋偷墓?jié)點(diǎn)是什么呢?

官方給出了所有類型點(diǎn)我查看類型,這里類型太多了,現(xiàn)用現(xiàn)查文檔吧!??!

@babel/types

這里小編也推薦一個(gè)插件@babel/types,該插件包含非常多api,官方文檔。它的作用是創(chuàng)建、修改、刪除、查找ast節(jié)點(diǎn)。另外從上邊知道AST的節(jié)點(diǎn)也是分為多種類型,比如ExpressionStatement是表達(dá)式、ClassDeclaration是類聲明、VariableDeclaration是變量聲明等等,同樣的這些類型都對(duì)應(yīng)了其創(chuàng)建方法:t.expressionStatement、t.classDeclarationt.variableDeclaration,也對(duì)應(yīng)了判斷方法:t.isExpressionStatementt.isClassDeclaration、t.isVariableDeclaration。這個(gè)插件往往和traverse遍歷插件一起使用,因?yàn)?code>types只能對(duì)單一節(jié)點(diǎn)進(jìn)行操作,一般是在對(duì)節(jié)點(diǎn)的深度遍歷中使用。

相信到這里,小伙伴們對(duì)babel編譯原理已經(jīng)有了基本了解,并且對(duì)AST抽象語法樹也有了了解。下一邊文章我們來實(shí)踐一下怎么編寫一個(gè)babel插件。

最后編輯于
?著作權(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)容