【第六期】babel深刻教程(babel7版本)

最近在一些項目編譯系統的工做中涉及到了不少關於babel插件的開發,關於babel大多數人的感覺多是既陌生又熟悉,可能大多數人對於babel的應用場景的認識就是在webpack中使用一個babel-loader,但當你真正瞭解他掌握它的時候,會發現他其實還有些更強的用法。。。前端

基本概念

babel是什麼?

Babel 是一個編譯器(輸入源碼 => 輸出編譯後的代碼)。就像其餘編譯器同樣,編譯過程分爲三個階段:解析、轉換和打印輸出。(官網的解釋)。node

babel plugin和babel preset是什麼?

babel中有不少概念,好比:插件(plugin),預設(preset)和一些比較基礎的工具(例如@babel/parser,@babel/traverse等等)。關於他們的關係,能夠理解爲babel的plugin構建在基礎工具之上,而babel的preset是多個babel plugin的打包集合,例如咱們所熟悉的@babel/preset-env,@babel/preset-react。react

babel深刻

本篇文章不對babel官方的plugin,preset庫作過多闡述,畢竟這是一篇深刻教程。咱們要提的是一個更本質的問題:babel是如何轉譯代碼的?webpack

咱們大致上把這個轉譯代碼的過程分爲三步:git

  • 第一步(parse):code=>ast
  • 第二步(transform):ast=>修改過的ast
  • 第三步(generate):修改過的ast=>編譯後的code

這三步分別對應babel的三個基本工具,第一步對應@babel/parser,第二步對應@babel/traverse,第三步對應@babel/generator。下面就來詳述一下這三個過程。github

parse(@babel/parser)

這一步是babel將code轉化爲ast。ast是Abstract syntax tree的縮寫,即抽象語法樹,單說抽象語法樹可能不太好理解,咱們能夠先來看一下一個具體的例子,你可使用astexplorer.net/來幫你運行@babel/parser:web

function mirror(something) {
  return something
}
複製代碼

被轉譯成ast:shell

{
  "type": "File",
  "start": 0,
  "end": 49,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 3,
      "column": 1
    }
  },
  "program": {
    "type": "Program",
    "start": 0,
    "end": 49,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 3,
        "column": 1
      }
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "FunctionDeclaration",
        "start": 0,
        "end": 49,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 3,
            "column": 1
          }
        },
        "id": {
          "type": "Identifier",
          "start": 9,
          "end": 15,
          "loc": {
            "start": {
              "line": 1,
              "column": 9
            },
            "end": {
              "line": 1,
              "column": 15
            },
            "identifierName": "mirror"
          },
          "name": "mirror"
        },
        "generator": false,
        "async": false,
        "params": [
          {
            "type": "Identifier",
            "start": 16,
            "end": 25,
            "loc": {
              "start": {
                "line": 1,
                "column": 16
              },
              "end": {
                "line": 1,
                "column": 25
              },
              "identifierName": "something"
            },
            "name": "something"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "start": 27,
          "end": 49,
          "loc": {
            "start": {
              "line": 1,
              "column": 27
            },
            "end": {
              "line": 3,
              "column": 1
            }
          },
          "body": [
            {
              "type": "ReturnStatement",
              "start": 31,
              "end": 47,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 2
                },
                "end": {
                  "line": 2,
                  "column": 18
                }
              },
              "argument": {
                "type": "Identifier",
                "start": 38,
                "end": 47,
                "loc": {
                  "start": {
                    "line": 2,
                    "column": 9
                  },
                  "end": {
                    "line": 2,
                    "column": 18
                  },
                  "identifierName": "something"
                },
                "name": "something"
              }
            }
          ],
          "directives": []
        }
      }
    ],
    "directives": []
  },
  "comments": []
}
複製代碼

乍一看彷佛很複雜,可是你要作的是從中找到關鍵信息,咱們將當中影響閱讀的字段去除(去除loc,start,end,以及函數體外層的嵌套):express

{
  "type": "FunctionDeclaration",
  "id": {
    "type": "Identifier",
    "name": "mirror"
  },
  "generator": false,
  "async": false,
  "params": [
    {
      "type": "Identifier",
      "name": "something"
    }
  ],
  "body": {
    "type": "BlockStatement",
    "body": [
      {
        "type": "ReturnStatement",
        "argument": {
          "type": "Identifier",
          "name": "something"
        }
      }
    ],
    "directives": []
  }
}
複製代碼

這樣是否是簡單不少!咱們看一下這個json描述了什麼:外層是一個叫mirror的函數聲明,他的傳參有一個,叫something,函數體內部return了一個叫something的變量。咱們把這個描述與上邊的js代碼對照着看,居然不謀而合(其實從這一點也能看出code<=>ast這個過程是可逆的)。對於初學者而言,上邊的抽象語法樹難以理解的多是這些名字冗長的節點type,下邊簡單列舉一下js中的常見的節點名稱(慎看,能夠選擇性跳過,可是瞭解這些節點名稱能夠加深你對babel甚至js語言自己的理解)。詳見babeljs.io/docs/en/bab…編程

type 字面含義 描述
FunctionDeclaration 函數聲明 function a() {}
FunctionExpression 函數表達式 var a = function() {}
ArrowFunctionExpression 箭頭函數表達式 ()=>{}(此處能夠思考:爲何沒有箭頭函數聲明,以及Declaration和Expression的區別)
AwaitExpression await表達式 async function a () { await b() }
CallExpression 調用表達式 a()
MemberExpression 成員表達式 a.b
VariableDeclarator 變量聲明 var,const,let(var,const,let用Node中的kind區分)
Identifier 變量標識符 var a(這裏a是一個Identifier)
NumericLiteral 數字字面量 var a = 1
StringLiteral 字符串字面量 var a = 'a'
BooleanLiteral 布爾值字面量 var a = true
NullLiteral null字面量 var a = null(此處能夠思考:爲何沒有undefined字面量)
BlockStatement {}
ArrayExpression 數組表達式 []
ObjectExpression 對象表達式 var a = {}
SpreadElement 擴展運算符 {...a},[...a]
ObjectProperty 對象屬性 {a:1}(這裏的a:1是一個ObjectProperty)
ObjectMethod 函數屬性 {a(){}}
ExpressionStatement 表達式語句 a()
IfStatement if if () {}
ForStatement for for (;;){}
ForInStatement for in for (a in b) {}
ForOfStatement for of for (a of b) {}
ImportDeclaration import聲明 import 'a'
ImportDefaultSpecifier import default說明符 import a from 'a'
ImportSpecifier import說明符 import {a} from 'a'
NewExpression new表達式 new A()
ClassDeclaration class聲明 class A {}
ClassBody class body class A {}(類的內部)

常見的列舉的差很少了。。。就先寫到這吧。

generate(@babel/generator)

generate原本應該是第三步,爲何將第三步放到這裏呢?由於他比較簡單,並且當咱們使用traverse時,須要用到它。在這裏咱們簡單的把一段code轉換爲ast,再轉換爲code:

先安裝好依賴。這一點之後再也不贅述

yarn add @babel/parser @babel/generator
複製代碼
const parser = require('@babel/parser')
const generate = require('@babel/generator').default

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',
})
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
  • 結果:
function mirror(something) {
  return something;
}
複製代碼

這就是generator的基本用法,詳細參照babeljs.io/docs/en/bab…

transform(@babel/traverse,@babel/types,@babel/template)

到了最爲關鍵的transform步驟了,這裏的主角是@babel/traverse,@babel/types和@babel/template是輔助工具。咱們首先來談一下visitor這個概念。

visitor

  1. visitor是什麼

訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法。

假如你這樣寫了一個visitor傳遞給babel:

const visitor = {
  Identifier () {
    enter () {
      console.log('Hello Identifier!')
    },
    exit () {
      console.log('Bye Identifier!')
    }
  }
}
複製代碼

那麼babel會使用他的遞歸遍歷器去遍歷整棵ast,在進入和退出Identifier節點時,會執行咱們定義的函數。

2.在通常狀況下exit較少使用,因此能夠簡寫成:

const visitor = {
  Identifier () {
    console.log('Hello Identifier!')
  }
}
複製代碼

3.若有必要,你還能夠把方法名用|分割成a節點類型|b節點類型形式的字符串,把同一個函數應用到多種訪問節點。

const visitor = {
  'FunctionExpression|ArrowFunctionExpression' () {
    console.log('A function expression or a arrow function expression!')
  }
}
複製代碼

好了,如今以上邊的mirror函數爲例,來動手寫一個traverse的簡單示例吧:

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',
})
const visitor = {
  Identifier (path) {
    console.log(path.node.name)
  }
}
traverse(ast, visitor)
複製代碼
  • 結果:mirror,something,something

與你的預估是否一致呢?若是一致,那咱們能夠繼續往下。此處你可能提出疑問:這個path是什麼?

path

能夠簡單地認爲path是對當前訪問的node的一層包裝。例如使用path.node能夠訪問到當前的節點,使用path.parent能夠訪問到父節點,這裏列出了path所包含的內容(還沒有列出path中所包含的一些方法)。

{
  "parent": {...},
  "node": {...},
  "hub": {...},
  "contexts": [],
  "data": {},
  "shouldSkip": false,
  "shouldStop": false,
  "removed": false,
  "state": null,
  "opts": null,
  "skipKeys": null,
  "parentPath": null,
  "context": null,
  "container": null,
  "listKey": null,
  "inList": false,
  "parentKey": null,
  "key": null,
  "scope": null,
  "type": null,
  "typeAnnotation": null
}
複製代碼

當你有一個 Identifier() 成員方法的訪問者時,你其實是在訪問路徑而非節點。 經過這種方式,你操做的就是節點的響應式表示(譯註:即路徑)而非節點自己。babel handbook

path中還提供了一系列的工具函數,例如traverse(在當前path下執行遞歸),remove(刪除當前節點),replaceWith(替換當前節點)等等。

解釋完了path以後,咱們試着真正的來轉換一下代碼吧,在這裏使用了@babel/generator來將ast轉換爲code

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',
})
const visitor = {
  Identifier (path) {
    path.node.name = path.node.name.split('').reverse().join('')
  }
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
  • 結果:
function rorrim(gnihtemos) {
  return gnihtemos;
}
複製代碼

這段代碼應該不難理解,就是將全部的變量作了個字符串翻轉。是否是事情已經變得有趣起來了?

@babel/types

Babel Types模塊是一個用於 AST 節點的 Lodash 式工具庫(譯註:Lodash 是一個 JavaScript 函數工具庫,提供了基於函數式編程風格的衆多工具函數), 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯很是有用。(依然是handbook原話)

展現一下最經常使用的使用方式,用來判斷節點的類型

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const t = require('@babel/types')

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',
})
const visitor = {
  enter(path) {
    if (t.isIdentifier(path.node)) {
      console.log('Identifier!')
    }
  }
}
traverse(ast, visitor)
複製代碼
  • 結果:Identifier! Identifier! Identifier!

@babel/types還能夠用來生成節點,結合上邊的知識,咱們試着改動mirror函數的返回值

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',
})
const strNode = t.stringLiteral('mirror')
const visitor = {
  ReturnStatement (path) {
    path.traverse({
      Identifier(cpath){
        cpath.replaceWith(strNode)
      }
    })
  }
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
  • 結果:
function mirror(something) {
  return "mirror";
}
複製代碼

在這裏咱們用到了t.stringLiteral('mirror')去建立一個字符串字面量節點,而後遞歸遍歷ReturnStatement下的Identifier,並將其替換成咱們所建立的字符串字面量節點(注意此處咱們已經開始使用了一些path下的公共方法)。

@babel/template

使用@babel/type建立一些簡單節點會很容易,可是若是是大段代碼的話就會變得困難了,這個時候咱們可使用@babel/template。下面寫了一個簡單示例,爲mirror函數內部寫了一些邏輯判斷。

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const template = require('@babel/template').default
const t = require('@babel/types')

const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
  sourceType: 'module',
})
const visitor = {
  FunctionDeclaration(path) {
    // 在這裏聲明瞭一個模板,比用@babel/types去生成方便不少
    const temp = template(` if(something) { NORMAL_RETURN } else { return 'nothing' } `)
    const returnNode = path.node.body.body[0]
    const tempAst = temp({
      NORMAL_RETURN: returnNode
    })
    path.node.body.body[0] = tempAst
  }
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
  • 結果:
function mirror(something) {
  if (something) {
    return something;
  } else {
    return 'nothing';
  }
}
複製代碼

完美!以上,babel基本的工具使用方式就介紹的差很少了,下邊步入正題:嘗試寫一個babel插件。

寫一個babel插件

其實到這裏,編寫一個babel插件已經很是簡單了,咱們嘗試直接將上邊的代碼移植成爲一個babel插件

module.exports = function (babel) {
  const {
    types: t,
    template
  } = babel
  const visitor = {
    FunctionDeclaration(path) {
      const temp = template(` if(something) { NORMAL_RETURN } else { return 'nothing' } `)
      const returnNode = path.node.body.body[0]
      const tempAst = temp({
        NORMAL_RETURN: returnNode
      })
      path.node.body.body[0] = tempAst
    }
  }
  return {
    name: 'my-plugin',
    visitor
  }
}
複製代碼

babel插件暴露了一個函數,函數的傳參是babel,你可使用解構賦值獲取到types,template這些工具。函數返回值中包含一個name和一個visitor,name是插件的名稱,visitor就是咱們上邊屢次編寫的visitor。

你可能注意到了一些babel插件是能夠傳參的,那咱們如何在babel插件中接收參數呢

module.exports = function (babel) {
  const {
    types: t,
    template
  } = babel
  const visitor = {
    FunctionDeclaration(path, state) {
      const temp = template(` if(something) { NORMAL_RETURN } else { return '${state.opts.whenFalsy}' } `)
      const returnNode = path.node.body.body[0]
      const tempAst = temp({
        NORMAL_RETURN: returnNode
      })
      path.node.body.body[0] = tempAst
    }
  }
  return {
    name: 'my-plugin',
    visitor
  }
}
複製代碼

在上邊的例子中咱們看到在visitor中能夠傳入第二個參數state,在這個state中,使用state.opts[配置名]就可訪問到用戶所傳遞的對應配置名的值

如何測試你所編寫的babel插件是可使用的呢?引用你所編寫的插件並測試一下:

const babel = require("@babel/core")

const code = `function mirror(something) { return something }`
const res = babel.transformSync(code, {
  plugins: [
    [require('你編寫的插件地址'), {
      whenFalsy: 'Nothing really.'
    }]
  ]
})

console.log(res.code)
複製代碼
  • 結果:
function mirror(something) {
  if (something) {
    return something;
  } else {
    return 'Nothing really.';
  }
}
複製代碼

以上,咱們基本對babel的原理有了一個基本的認識,而且能夠本身寫出一個babel插件了。至於如何將babel的威力發揮在平常的工做中呢?就須要各位去自行探索了。


水滴前端團隊招募夥伴,歡迎投遞簡歷到郵箱:fed@shuidihuzhu.com

相關文章
相關標籤/搜索