前言
[實(shí)踐系列] 主要是讓我們通過實(shí)踐去加深對(duì)一些原理的理解。
有興趣的同學(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)行原理

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。
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。

實(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)為

我們可以看到這句聲明語句位于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)為

我們可以看到箭頭函數(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)為

我們可以看到普通函數(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ì)有很多干貨哦!!!