babel - 從入門到上手
本文將引導你一步一步的學會 babel,在學習的過程將着重介紹以下幾點:
-
babel 轉譯的過程
-
AST 介紹
-
babel 常用的 api
-
@babel/preset-env
-
@babel/plugin-transform-runtime
1.babel 的作用
官方定義:Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。
2.babel 轉譯的三個階段
-
源碼 parse 生成 AST(parse)
-
遍歷 AST 並進行各種增刪改 (核心)(transform)
-
轉換完 AST 之後再打印成目標代碼字符串(generate)
3. AST 如何生成
在學習 AST 和 babel 轉換時,可以藉助下面兩個網站輔助查看代碼轉換成 AST 之後的結果。
https://esprima.org/demo/parse.html
https://astexplorer.net/
整個解析過程主要分爲以下兩個步驟:詞法分析和語法分析
3.1 詞法分析
詞法分析,這一步主要是將字符流 (char stream) 轉換爲令牌流(token stream),又稱爲分詞。其中拆分出來的各個部分又被稱爲 詞法單元 (Token)。
可以這麼理解,詞法分析就是把你的代碼從 string 類型轉換成了數組,數組的元素就是代碼裏的單詞 (詞法單元), 並且標記了每個單詞的類型。
比如:
const a = 1
生成的 tokenList
[
{ type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator',value: '=' },
{ type: 'Numeric', value: '1' },
{ type: 'Punctuator', value: ';' },
];
詞法分析結果,缺少一些比較關鍵的信息:需要進一步進行 語法分析 。
3.2 語法分析
語法分析會將詞法分析出來的 詞法單元 轉化成有語法含義的 **抽象語法樹結構 (AST),**同時,驗證語法,語法如果有錯,會拋出語法錯誤。
這裏截取 AST 樹上的 program 的 body 部分 (採用 @babel/parser 進行轉化)
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"loc": {},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"loc": {},
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"loc": {},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 10,
"end": 11,
"loc": {},
"extra": {},
"value": 1
}
}
],
"kind": "const"
}
]
可以看到,經過語法分析階段轉換後生成的 AST,通過樹形的層屬關係建立了語法單元之間的聯繫。
4. AST 節點
轉換後的 AST 是由很多 AST 節點組成,主要有以下幾種類型:字面量(Literal)、標誌符(Identifer)、語句(statement)、聲明(Declaration)、表達式(Expression)、註釋(Comment)、程序(Program)、文件(File)。
每種 AST 節點都有自己的屬性,但是它們也有一些公共屬性:
-
結點類型(type):AST 節點的類型。
-
位置信息(loc):包括三個屬性 start、 end、 loc。其中 start 和 end 代表該節點對應的源碼字符串的開始和結束下標,不區分行列。loc 屬性是一個對象,有 line 和 column 屬性分別記錄開始和結束行列號。
-
註釋(comments):主要分爲三種 leadingComments、innerComments、trailingComments ,分別表示開始的註釋、中間的註釋、結尾的註釋。
5. babel 常用的 api
babel 中有五個常用的 api:
-
針對 parse 階段有 @babel/parser,功能是把源碼轉成 AST
-
針對 transform 階段有 @babel/traverse,用於增刪改查 AST
-
針對 generate 階段有 @babel/generate,會把 AST 打印爲目標代碼字符串,同時生成 sourcemap
-
在 transform 階段,當需要判斷和生成結點時,需要 @babel/types,
-
當需要批量創建 AST 的時候可以使用 @babel/template 來簡化 AST 創建邏輯。
我們可以通過這些常用的 api 來自己實現一個 plugin,對代碼進行轉換。接下來就介紹一下幾個常見的 api。
5.1 @babel/parser
babelParser.parse(code, [options])--- 返回的 AST 根節點是 File 節點 babelParser.parseExpression(code, [options])--- 返回的 AST 根結點是 Expression
第一個參數是源代碼,第二個參數是 options,其中最常用的就是 plugins、sourceType 這兩個:
-
sourceType: 指示分析代碼的模式,主要有三個值:script、module、unambiguous。
-
plugins:指定要使用插件數組。
5.2 @babel/traverse(核心)
function traverse(ast, opts)
ast:經過 parse 之後的生成的 ast
opts :指定 visitor 函數 -- 用於遍歷節點時調用(核心)
方法的第二參數中的 visitor 是我們自定義插件時經常用到的地方,你可以通過兩種方式來定義這個參數
第一種是以方法的形式聲明 visitor
traverse(ast, {
BlockStatement(path, state) {
console.log('BlockStatement>>>>>>')
}
});
第二種是以對象的形式聲明 visitor
traverse(ast, {
BlockStatement: {
enter(path, state) {
console.log('enter>>>', path, state)
},
exit(path, state) {
console.log('exit>>>', path, state)
}
}
});
每一個 visitor 函數會接收兩個參數 path 和 state,path 用來操作節點、遍歷節點和判斷節點,而 state 則是遍歷過程中在不同節點之間傳遞數據的機制, 我們也可以通過 state 存儲一些遍歷過程中的共享數據。
5.3. @babel/generator
轉換完 AST 之後,就要打印目標代碼字符串,這裏通過 @babel/generator 來實現,其方法常用的參數有兩個:
-
要打印的 AST
-
options-- 指定打印的一些細節,比如 comments 指定是否包含註釋
6. babel 的內置功能
上面我們介紹了幾個用於實現插件的 api,而 babel 本身爲了實現對語法特性的轉換以及對 api 的支持 (polyfill),也內置了很多的插件 (plugin) 和預設 (preset)。
其插件主要分爲三類:
-
syntax plugin:只是在 parse 階段使用,可以讓 parser 能夠正確的解析對應的語法成 AST
-
transform plugin:是對 AST 的轉換,針對 es20xx 中的語言特性、typescript、jsx 等的轉換都是在這部分實現的
-
proposal plugin:未加入語言標準的特性的 AST 轉換插件
那麼預設是**什麼呢?**預設其實就是對於插件的一層封裝,通過配置預設,使用者可以不用關心具體引用了什麼插件,從而減輕使用者的負擔。
而根據上面不同類型的插件又產生了如下幾種預設:
-
專門根據 es 標準處理語言特性的預設 -- babel-preset-es20xx
-
對其 react、ts 兼容的預設 -- preset-react preset-typescript
我們目前最常使用的便是 @babel/preset-env 這個預設,下文將會通過一個例子來介紹它的使用。
7. 案例 1-- 自定義插件
需求
如果有一行代碼
const a = 1
我需要通過 babel 自定義插件來給標識符增加類型定義,讓它成爲符合 ts 規範的語句,結果:const a: number = 1。
實現
通過 babel 處理代碼,其實就是在對 AST 節點進行處理。
我們先搭起一個架子
// 源代碼
const sourceCode = `
const a = 1
`;
// 調用parse,生成ast
const ast = parser.parse(sourceCode, {})
// 調用traverse執行自定義的邏輯,處理ast節點
traverse(ast, {})
// 生成目標代碼
const { code } = generate(ast, {});
console.log('result after deal with》〉》〉》', code)
在引入對應的包後,我們的架子主要分爲三部分,我們首先需要知道這句話轉換完之後的 AST 節點類型
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"loc": {...},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"loc": {...},
"id": {...},
"init": {...}
}
],
"kind": "const"
}
]
上圖可以看出這句話的類型是 VariableDeclaration,所以我們要寫一個可以遍歷 VariableDeclaration 節點的 visitor。
// 調用traverse執行自定義的邏輯,處理ast節點
traverse(ast, {
VariableDeclaration(path, state) {
console.log('VariableDeclaration>>>>>>', path.node.type)
}
})
繼續觀察結構,該節點下面有 declarations 屬性,其包括所有的聲明,declarations[0] 就是我們想要的節點。
traverse(ast, {
VariableDeclaration(path, state) {
console.log('VariableDeclaration>>>>>>', path.node.type)
const tarNode = path.node.declarations[0]
console.log('tarNode>>>>>>', tarNode)
}
})
每一個聲明節點類型爲 VariableDeclarator,該節點下有兩個重要的節點,id(變量名的標識符)和 init(變量的值)。這裏我們需要找到變量名爲 a 的標識符,且他的值類型爲 number(對應的節點類型爲 NumericLiteral)。
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"loc": {...},
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"loc": {...},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 10,
"end": 11,
"loc": {...},
"extra": {...},
"value": 1
}
}
]
這時候就需要我們使用一個新的包 **@babel/types **來判斷類型。判斷類型時只需調用該包中對應的判斷方法即可,方法名都是以 isXxx 或者 assertXxx 來命名的 (Xxx 代表節點類型),需要傳入對應的節點才能判斷該節點的類型。
traverse(ast, {
VariableDeclaration(path, state) {
const tarNode = path.node.declarations[0]
if(types.isIdentifier(tarNode.id) && types.isNumericLiteral(tarNode.init))
{
console.log('inside>>>>>>')
}
}
})
鎖定了節點後,我們需要更改 id 節點的 name 內容, 就可以實現需求了。
traverse(ast, {
VariableDeclaration(path, state) {
const tarNode = path.node.declarations[0]
if(types.isIdentifier(tarNode.id)&&types.isNumericLiteral(tarNode.init))
{
console.log('inside>>>>>>')
tarNode.id.name = `${tarNode.id.name}: number`
}
}
})
8. 案例 2-- 工程化使用
8.1 準備工作
@babel/core 是 babel 的核心庫,@babel/cli 是 babel 的命令行工具。如果要使用 babel,首先要安裝 @babel/core 和 @babel/cli。
源代碼爲:
const fn = () => 1 ;
位置放在 src 下的 test.js 文件。
8.2 通過配置文件使用
根據官方文檔的說法,目前有兩類配置文件:項目範圍配置 和 文件相對配置。
1. 項目範圍配置(全局配置) -- babel.config.json
2. 文件相對配置(局部配置) -- .babelrc.json、package.json
區別:第一種配置作用整個項目,如果 babel 決定應用這個配置文件,則一定會應用到所有文件的轉換。而第二種配置文件只能應用到 “當前目錄” 下的文件中。
babel 在決定一個 js 文件應用哪些配置文件時, 會執行如下策略: 如果這個 js 文件在當前項目內,則會遞歸向上搜索最近的一個 .babelrc 文件 (直到遇到 package.json),將其與全局配置合併。
這裏我們只需使用 babel.config.json 的形式進行配置
配置文件:
{
"presets": [
[
"@babel/preset-env"
]
]
}
再在 package.json 裏配置一下執行的腳本
"dev": "./node_modules/.bin/babel src --out-dir lib"
8.4 常用的包
我們在工程裏常用包主要有兩個:
-
@babel/preset-env
-
@babel/plugin-transform-runtime
8.4.1 @babel/preset-env
@babel/preset-env 是一個智能的預設,它允許你使用最新的 JavaScript,而不需要微管理你的目標環境需要哪些語法轉換,根據 babel 官網上的描述,它是通過 browsersList、compat-table 相結合來實現智能的引入語法轉換工具。
compat-data 形如如下,其註明了什麼特性,在什麼環境下支持,再結合通過 browsersList 查詢出的環境版本號,就可以確定需要引入哪些 plugin 或者 preset。
"es6.array.fill": {
"chrome": "45",
"opera": "32",
"edge": "12",
"firefox": "31",
"safari": "7.1",
"node": "4",
"ios": "8",
"samsung": "5",
"rhino": "1.7.13",
"electron": "0.31"
},
@babel/preset-env 有三個常用的關鍵可選項:
-
targets
-
useBuiltIns
-
corejs
targets
描述項目支持的環境 / 目標環境,支持 browserslist 查詢寫法
{ "targets": "> 0.25%, not dead" } // 全球使用人數大於0.25%且還沒有廢棄的版本
支持最小環境版本構成的對象
{ "targets": { "chrome": "58", "ie": "11" } }
如果沒配置 targets, Babel 會假設你的目標是最老的瀏覽器 @babel/preset-env 將轉換所有 ES2015-ES2020 代碼爲 ES5 兼容
useBuiltIns
可以使用三個值:"usage" 、"entry" 、 false,默認使用 false
false
當使用 false 時:在不主動 import 的情況下不使用 preset-env 來處理 polyfills
entry
babel 將會根據瀏覽器目標環境 (targets) 的配置,引入全部瀏覽器暫未支持的 polyfill 模塊,只要我們在打包配置入口 或者 文件入口寫入 import "core-js" 這樣的引入, babel 就會根據當前所配置的目標瀏覽器 (browserslist) 來引入所需要的 polyfill 。
usage
設置 useBuiltIns 的值爲 usage 時,babel 將會根據我們的代碼使用情況自動注入 polyfill。
8.4.2 entry 與 usage 的區別:
在上文所示例子的基礎上,我們修改一下源代碼
function test() {
new Promise()
}
test()
const arr = [ 1 , 2 , 3 , 4 ].map(item => item * item)
console.log(arr)
我們沒有配置 useBuiltIns 時,preset-env 只對代碼的語法進行了處理,對於新增的 api 並沒有引入對應的 polyfill。下圖是轉換結果:
"use strict";
function test() {
new Promise();
}
test();
var arr = [1, 2, 3, 4].map(function (item) {
return item * item;
});
console.log(arr);
當我們使用 useBuiltIns:“entry” 時 (入口文件需要引入 core-js),由於我們沒有指定 targets,結果當然是引入了一堆包。
加入 "targets": "> 0.25%, not dead" 後,很明顯少了很多的引入(如下圖所示),這也印證了上面所說的, 當 useBuiltIns 的值爲 entry 時,@babel/preset-env 會按照你所設置的 targets 來引入所需的 polyfill。
當我們使用 useBuiltIns:“usage” 時,這時就無須在入口文件引入 core-js 了。
可以看出引入的包非常精準,需要哪些就引入哪些 polyfill。當然你也可以配置 targets,這樣的話 targets 會輔助 preset-env 引入,從而進一步控制引入包數量。
corejs
corejs 是 JavaScript 的模塊化標準庫,其中包括各種 ECMAScript 特性的 polyfill。上面我們轉換後的代碼中引入的 polyfill 都是來源於 corejs。它現有 2 和 3 兩個版本,目前 2 版本已經進入功能凍結階段了,新的功能會添加到 3 版本中。
具體的變化可以查看 corejs 的 github 的說明文檔:core-js@3, babel and a look into the future
這個選項只有在和 useBuiltIns: "usage" 或 useBuiltIns:"entry" 一起使用時纔有效果,該屬性默認爲 "2.0"。其作用是進一步約束引入的 polyfill 的數量。
8.4.3 @babel/plugin-transform-runtime
雖然經過了 preset-env 的轉換,代碼已經可以實現不同版本的特性兼容了。但是會產生兩個問題:
1.preset-env 轉換後引入的 polyfill,是通過 require 進行引入的,這就意味着,對於 Array.from 等靜態方法,以及 includes 等實例方法,會直接在 global 上添加。這就導致引入的 polyfill 方法可能和其他庫發生衝突。
2.babel 轉換代碼的時候,可能會使用一些幫助函數來協助轉換,比如 class
class a {}
轉換之後:
這裏就使用了_classCallCheck 這樣的輔助函數,如果有多個文件聲明 class 的話,就會重複創建這樣的方法。
@babel/plugin-transform-runtime 這個插件的作用就是爲了處理這樣的問題。該插件也有一個 corejs 的配置,這裏配置的是 runtime-corejs 的版本,目前有 2、3 兩個版本。
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": "3.0"
}
]
]
}
轉換結果:
這裏由於 babel 是先執行 plugins 後執行 presets 的內容,@babel/plugin-transform-runtime 插件先於 preset-env 將 polyfill 引入了,並且做了一層包裝,所以就無須再通過 @babel/preset-env 來引入 polyfill 了。
可以看到,轉換之後的_classCallCheck 的方法定義全部改爲了從 runtime-corejs 中引入,對於新特性的 polyfill 也不再掛載在全局了。這樣的方法適合定義類庫時使用,可以防止變量污染全局。
結束語
相信通過這麼一篇文章,大家基本都瞭解了 babel 的基礎原理,以及它是如何實現對代碼的轉換的,並可以自己實現簡單的一個 babel 的插件。當然,本文中所述之內容只是 babel 全部內容的十之一二,只是作一個學習 babel 的引路石,如果有較強烈的需求,還是要常翻閱官方文檔。最後希望大家可以將 bebel 學的更透徹。
參考文章:
-
https://juejin.cn/post/6844903797571977223
-
https://juejin.cn/post/6844904013033373704
-
https://www.cnblogs.com/zhishaofei/p/13896056.html
-
https://zhuanlan.zhihu.com/p/367724302
-
https://www.babeljs.cn/
-
https://juejin.cn/book/6946117847848321055
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/LlQRx5SPmFgnTDO8VunGnw