babel外掛入門-AST(抽象語法樹)

圓兒圈圈發表於2018-03-27

目錄

  • Babel簡介
  • Babel執行原理
  • AST解析
  • AST轉換
  • 寫一個Babel外掛

Babel簡介

Babel 是一個 JavaScript 編譯器,它能將es2015,react等低端瀏覽器無法識別的語言,進行編譯。

babel外掛入門-AST(抽象語法樹)

上圖的左邊程式碼中有箭頭函式,Babel將進行了原始碼轉換,下面我們來看Babel的執行原理。

Babel執行原理

Babel 的三個主要處理步驟分別是:

解析(parse),轉換(transform),生成(generate)。

Babel三個步驟

其過程分解用語言描述的話,就是下面這樣:

解析

使用 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-generatorAST 樹輸出為轉碼後的程式碼字串

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外掛入門-AST(抽象語法樹)

babel外掛入門-AST(抽象語法樹)

解析程式碼
//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)
複製程式碼

babel-types文件連結

babel外掛入門-AST(抽象語法樹)

  • 在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);
複製程式碼

babel外掛入門-AST(抽象語法樹)

所以我們這次的目的是將

import { flatten, join } from "lodash";
複製程式碼

轉換為從而只引入兩個lodash模組,減少打包體積

import flatten from "lodash/flatten";
import join from "lodash/join";
複製程式碼

實現步驟如下:

  1. 在專案下的node_module中新建資料夾 babel-plugin-extract

注意:babel外掛資料夾的定義方式是 babel-plugin-外掛名 我們可以在.babelrc的plugin中引入自定義外掛 或者在webpack.config.js的loader options中加入自定義外掛

  1. 在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)
                }
            }
        }
    };
}
複製程式碼
  1. 修改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

  1. 執行引入了自定義外掛的webpack.config.js

babel外掛入門-AST(抽象語法樹)

打包檔案現在為21.4KiB,明顯減小,自定義外掛成功!~

外掛檔案目錄

YUAN-PLUGINS
|
| - node_modules
|   |
|   | - babel-plugins-extract
|           |
|           index.js
|   
| - src
|   | - index.js
|
| - webpack.config.js

複製程式碼

覺得好玩就關注一下~歡迎大家收藏寫評論~~~

相關文章