目錄
- Babel簡介
- Babel執行原理
- AST解析
- AST轉換
- 寫一個Babel外掛
Babel簡介
Babel 是一個 JavaScript 編譯器,它能將es2015,react等低端瀏覽器無法識別的語言,進行編譯。
上圖的左邊程式碼中有箭頭函式,Babel將進行了原始碼轉換,下面我們來看Babel的執行原理。
Babel執行原理
Babel 的三個主要處理步驟分別是:
解析(parse),轉換(transform),生成(generate)。
其過程分解用語言描述的話,就是下面這樣:
解析
使用 babylon 解析器對輸入的原始碼字串進行解析並生成初始 AST(File.prototype.parse)
利用 babel-traverse 這個獨立的包對 AST 進行遍歷,並解析出整個樹的 path,通過掛載的 metadataVisitor 讀取對應的元資訊,這一步叫 set AST 過程
轉換
transform 過程:遍歷 AST 樹並應用各 transformers(plugin) 生成變換後的 AST 樹
babel 中最核心的是 babel-core,它向外暴露出 babel.transform 介面。
let result = babel.transform(code, {
plugins: [
arrayPlugin
]
})
複製程式碼
生成
利用 babel-generator 將 AST 樹輸出為轉碼後的程式碼字串
AST解析
AST解析會把拿到的語法,進行樹形遍歷,對語法的每個節點進行響應的變化和改造再生產新的程式碼字串
節點(node)
AST將開頭提到的箭頭函式轉根據節點換為節點樹
ES2015箭頭函式
codes.map(code=>{
return code.toUpperCase()
})
複製程式碼
map+箭頭函式+返回其大寫字母 看上去是很簡單的函式,對應的抽象語法樹(AST)通常情況下也比較複雜,尤其是一些複雜的程式。我們不要試圖自己去分析抽象語法樹(AST),可以通過astexplorer.net可以線上看到不同的parser解析js程式碼後得到的AST,網站幫助我們來完成轉換,它允許我們在左邊輸入 JavaScript程式碼,右側會出可瀏覽的抽象語法樹(AST),我們可以通過這個工具輔助理解和試驗一些程式碼。
JavaScript AST visualizer 可以線上視覺化的看到AST。
AST樹形遍歷轉換後的結構
{
type:"ExpressionStatement",
expression:{
type:"CallExpression"
callee:{
type:"MemberExpression",
computed:false
object:{
type:"Identifier",
name:"codes"
}
property:{
type:"Identifier",
name:"map"
}
range:[]
}
arguments:{
{
type:"ArrowFunctionExpression",
id:null,
params:{
type:"Identifier",
name:"code",
range:[]
}
body:{
type:"BlockStatement"
body:{
type:"ReturnStatement",
argument:{
type:"CallExpression",
callee:{
type:"MemberExpression"
computed:false
object:{
type:"Identifier"
name:"code"
range:[]
}
property:{
type:"Identifier"
name:"toUpperCase"
}
range:[]
}
range:[]
}
}
range:[]
}
generator:false
expression:false
async:false
range:[]
}
}
}
}
複製程式碼
我們從 ExpressionStatement開始往樹形結構裡面走,看到它的內部屬性有callee,type,arguments,所以我們再依次訪問每一個屬性及它們的子節點。
於是就有了如下的順序
進入 ExpressionStatement
進入 CallExpression
進入 MemberExpression
進入 Identifier
離開 Identifier
進入 Identifier
離開 Identifier
離開 MemberExpression
進入 ArrowFunctionExpression
進入 Identifier
離開 Identifier
進入 BlockStatement
進入 ReturnStatement
進入 CallExpression
進入 MemberExpression
進入 Identifier
離開 Identifier
進入 Identifier
離開 Identifier
離開 MemberExpression
離開 CallExpression
離開 ReturnStatement
離開 BlockStatement
離開 ArrowFunctionExpression
離開 CallExpression
離開 ExpressionStatement
離開 Program
複製程式碼
Babel 的轉換步驟全都是這樣的遍歷過程。(有點像koa的洋蔥模型??)
AST轉換
解析好樹結構後,我們手動對箭頭函式進行轉換。
對比兩張圖,發現不一樣的地方就是兩個函式的arguments.type
解析程式碼
//babel核心庫,用來實現核心的轉換引擎
let babel = require('babel-core');
//可以實現型別判斷,生成AST節點
let types = require('babel-types');
let code = `codes.map(code=>{return code.toUpperCase()})`;//轉換語句
//visitor可以對特定節點進行處理
let visitor = {
ArrowFunctionExpression(path) {//定義需要轉換的節點,這裡攔截箭頭函式
let params = path.node.params
let blockStatement = path.node.body
//使用babel-types的functionExpression方法生成新節點
let func = types.functionExpression(null, params, blockStatement, false, false)
//替換節點
path.replaceWith(func) //
}
}
//將code轉成ast
let result = babel.transform(code, {
plugins: [
{ visitor }
]
})
console.log(result.code)
複製程式碼
注意: ArrowFunctionExpression() { ... } 是 ArrowFunctionExpression: { enter() { ... } } 的簡寫形式。
Path 是一個物件,它表示兩個節點之間的連線。
解析步驟
- 定義需要轉換的節點
ArrowFunctionExpression(path) {
......
}
複製程式碼
- 建立用來替換的節點
types.functionExpression(null, params, blockStatement, false, false)
複製程式碼
- 在node節點上找到需要的引數
- replaceWith(替換)
寫一個Babel外掛
從一個接收了 babel 物件作為引數的 function 開始。
export default function(babel) {
// plugin contents
}
複製程式碼
接著返回一個物件,其 visitor 屬性是這個外掛的主要節點訪問者。
export default function({ types: t }) {
return {
visitor: {
// visitor contents
}
};
};
複製程式碼
我們日常引入依賴的時候,會將整個包引入,導致打包後的程式碼太冗餘,加入了許多不需要的模組,比如index.js三行程式碼,打包後的檔案大小就達到了483 KiB,
index.js
import { flatten, join } from "lodash";
let arr = [1, [2, 3], [4, [5]]];
let result = _.flatten(arr);
複製程式碼
所以我們這次的目的是將
import { flatten, join } from "lodash";
複製程式碼
轉換為從而只引入兩個lodash模組,減少打包體積
import flatten from "lodash/flatten";
import join from "lodash/join";
複製程式碼
實現步驟如下:
- 在專案下的node_module中新建資料夾 babel-plugin-extract
注意:babel外掛資料夾的定義方式是 babel-plugin-外掛名 我們可以在.babelrc的plugin中引入自定義外掛 或者在webpack.config.js的loader options中加入自定義外掛
- 在babel-plugin-extract新建index.js
module.exports = function ({types:t}) {
return {
// 對import轉碼
visitor:{
ImportDeclaration(path, _ref = { opts: {} }) {
const specifiers = path.node.specifiers;
const source = path.node.source;
// 只有libraryName滿足才會轉碼
if (_ref.opts.library == source.value && (!t.isImportDefaultSpecifier(specifiers[0]))) { //_ref.opts是傳進來的引數
var declarations = specifiers.map((specifier) => { //遍歷 uniq extend flatten cloneDeep
return t.ImportDeclaration( //建立importImportDeclaration節點
[t.importDefaultSpecifier(specifier.local)],
t.StringLiteral(`${source.value}/${specifier.local.name}`)
)
})
path.replaceWithMultiple(declarations)
}
}
}
};
}
複製程式碼
- 修改webpack.prod.config.js中babel-loader的配置項,在plugins中新增自定義的外掛名
rules: [{
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ["env",'stage-0'],
plugins: [
["extract", { "library":"lodash"}],
["transform-runtime", {}]
]
}
}]
複製程式碼
注意:plugins 的外掛使用順序是順序的,而 preset 則是逆序的。所以上面的執行方式是extract>transform-runtime>stage-0>env
- 執行引入了自定義外掛的webpack.config.js
打包檔案現在為21.4KiB,明顯減小,自定義外掛成功!~
外掛檔案目錄
YUAN-PLUGINS
|
| - node_modules
| |
| | - babel-plugins-extract
| |
| index.js
|
| - src
| | - index.js
|
| - webpack.config.js
複製程式碼