babel插件入門-AST(抽象語法樹)

目錄

  • Babel簡介
  • Babel運行原理
  • AST解析
  • AST轉換
  • 寫一個Babel插件

Babel簡介

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

上圖的左邊代碼中有箭頭函數,Babel將進行了源碼轉換,下面咱們來看Babel的運行原理。html

Babel運行原理

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

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

Babel三個步驟

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

解析webpack

使用 babylon 解析器對輸入的源代碼字符串進行解析並生成初始 AST(File.prototype.parse)web

利用 babel-traverse 這個獨立的包對 AST 進行遍歷,並解析出整個樹的 path,經過掛載的 metadataVisitor 讀取對應的元信息,這一步叫 set AST 過程express

轉換瀏覽器

transform 過程:遍歷 AST 樹並應用各 transformers(plugin) 生成變換後的 AST 樹bash

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核心庫,用來實現核心的轉換引擎
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文檔連接

  • 在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";
複製代碼

實現步驟以下:

  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

打包文件如今爲21.4KiB,明顯減少,自定義插件成功!~

插件文件目錄

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

複製代碼

以爲好玩就關注一下~歡迎你們收藏寫評論~~~

相關文章
相關標籤/搜索