利用 Babel 玩轉你的代碼


我在團隊幫助開發 Node 工具時,遇到了需要對多份相似的代碼進行一定的處理,但又不能改變原本倉庫的代碼,這個非常像我們的編譯工具做的事情。在一開始的時候,參考了類似 FIS 的功能,簡單參照使用代碼的 HOOK 配合正則來進行代碼的操作。后面覺得實在難以拓展且無法確保修正后的代碼可用性,還是覺得必須使用 Babel 來實現源碼的操作。

什么是 Babel

babel 可以說是前端工程化用的最多的工具之一。在 ES6 僅僅只有標准時,我們就開始利用 Babel 的 ES6 轉換到 ES5 的功能來使用新的語法特性。包括后續大熱的React 制定的 JSX 的語法,“憑空”創建出來的語法,為什么能這么快且自然的就融入到前端體系里,這都離不開 Babel 這個編譯器的功勞(compiler)。除了編譯器功能外,還可以對代碼進行各類的靜態分析。

原理

babel 利用 babylon 解析器進行語法解析。解析成 AST(抽象語法樹)后,經過一定的規則對這個樹進行處理后再生成新的語法樹。最終新的語法樹在生成對應的合法的 Javascript 語法。

AST

AST,就是編譯時常說的抽象語法樹。樹狀結構可以幫助我們更好的索引和操作數據結構。例如我們前端三板斧 HTML/CSS/Javascript,而我們平常認知最深的 HTML DOM 樹,通過一棵樹就可以表達整個頁面結構,而 CSS 和 Javascript 都可以通過編譯器將語法解析成 AST 抽象語法樹。

代碼生成 AST 需要進行詞法分析和語法分析。

我們可以看一個最簡單的程序語法來管中窺豹一下,我們利用 AST Expoler 看看下面這段語法的 AST 到底是長什么樣的。

if(3 > 5) {
  console.log('3 大於 5')
} else {
  console.log('5 大於等於 3')
}

簡單畫個示意圖,部分屬性也精簡了一下。

我們可以看到,所有的 AST 樹的根結點都是 Program 節點。但即使一個非常簡單的 if 判斷語法對應的都是一支比較復雜的樹結構。而 if 語句對應的最重要的三個屬性就是 consequent/test/alternate。判斷分支內是一個 BinaryExpression(二元表達式),而在 ture/false 分支內則是 CallExpression(函數調用表達式),而調用的函數來自 MemberExpression(成員表達式)。

下面這個表上面這段代碼用到的

屬性 描述
BlockStatement 塊級表達式
BinaryExpression 二元表達式
CallExpression 函數調用表達式
MemberExpression 成員表達式
Literal 文字

更多詳細的 AST 類型都可以查看 Babylon 提供的 Spec 文檔

babel 插件

在我們簡單了解 AST 樹后,就可以具體看看如何利用 Babel 插件來玩轉我們的代碼。Babel 提供了幾個核心的 Lib,我們可以關心以下幾個,babel-corebabel-typesbabel-template

  • babel-core。Babel 的核心庫,提供了將代碼編譯轉化的能力。
  • babel-types。提供 AST 樹節點的類型。
  • babel-template。可以將普通字符串轉化成 AST,提供更便捷的使用。

Visitors

Visitors 訪問者模式,是我們訪問 AST 樹上的節點時所用到的方法。

const visitors = {
  BinaryExpression(path) {
    console.log('訪問二元表達式的節點')
  },
  Literal(path) {
    console.log('訪問文本')
  }
}

例如上圖就可以定義不同節點類型的訪問者,對節點路徑進行訪問並對節點進行操作並轉化。

Paths

我們在 我們定義成樹狀結構,除了可以完整描述整個代碼的結構,也方便讓節點和節點之間進行索引和操作。我們通過 visitor 訪問節點時,其實訪問的是節點的 path,並不是實際上的節點。path 上有該節點信息 path.node,也有類似 DOM 樹上一樣的可以訪問到父節點的信息 path.parent,方便后續的操作。

// 我們通過 visitor 訪問到的 AST 節點后,回調參數 path 示例

{
  "parent": {
    "type": "BinaryExpression",
    "operator": ">"
    ....
  },
  "node": {
    "type": "Literal",
    "name": "3",
    ....
  }
}

// in vistors

const visitors = {
  Literal(path) {
    console.log(path.node.name)
    console.log(path.parent.operator)
  }
}

示例

我們在了解 visitors 和 paths 后,就可以正式看看如何編寫自己的 babel 插件了。

替換操作符

我們最開頭的例子的判斷條件是一個二元表達式,我們希望將大於號(>)都轉換成小於等於(<=)號。

const babel = require('babel-core')
const t = require('babel-types')

const code = `
  if(3>5) {}
`

const visitor = {
  BinaryExpression(path) {
    if(path.node.operator === '>') {
      path.replaceWith(
        t.binaryExpression(
          '<=',
          path.node.left,
          path.node.right
        )
      );
    }
  }
}

const result = babel.transform(code, {
  plugins: [
    { visitor }
  ]
})

console.log(result.code)

import 拆分

在網上很多有關 babel 插件的例子,都會提到這個 import 語句拆分的案例,我也覺得這個案例非常實用且有意義。

我們在使用 lodash 時,如果是用普通的方式引入時,會引入非常大的包,有的時候你只需要使用其中一兩個小函數,所以 lodash 提供了分包按需引入的方式。但兩者的寫法不一樣,所以我們需要使用 babel 插件來實現這個功能。

// 轉化前,bad
import { uniq, sort as _sort } from 'lodash'

// 轉化后,better
import uniq from 'lodash/uniq'
import _sort from 'lodash/sort'

我們可以先去 AST explore 先看看具體 AST 的樹結構。然后再來寫出我們的插件。

const babel = require('babel-core')
const t = require('babel-types')

const code = `import { unqi, sort as _sort } from 'lodash'`

const visitor = {
  ImportDeclaration(path) {
    const specifiers = path.node.specifiers
    const source = path.node.source

    if(!t.isImportDefaultSpecifier(specifiers[0])) {
      const myImportDeclaration = specifiers.map(specifier => {
        return t.importDeclaration(
          [t.importDefaultSpecifier(specifier.local)],
          t.stringLiteral(`${source.value}/${specifier.imported.name}`)
        )
      })

      path.replaceWithMultiple(myImportDeclaration)
    }
  }
}

const result = babel.transform(code, {
  plugins: [
    { visitor }
  ]
})

console.log(result.code)

為你的函數插入一行代碼

我們如何對一個函數內部新增 AST 節點,“憑空”插入一行正確無誤的代碼呢。

const babel = require('babel-core')
const t = require('babel-types')

const code = `
  class Page {
    onLoad() {
      console.log('Hello')
    }
  }
`

const visitor = {
  BlockStatement(path) {
    if(path.parent.key.name === 'onLoad' && path.node.body.length === 1) {
      const body = path.node.body
      const newBlockStatement = t.blockStatement(body.concat(
        t.expressionStatement(
          t.callExpression(
            t.memberExpression(
              t.identifier('console'),
              t.identifier('log')
            ),
            [t.stringLiteral('World!')]
          )
        )
      ))
      path.replaceWith(newBlockStatement)
    }
  }
}
const result = babel.transform(code, {
  plugins: [
    { visitor }
  ]
})

console.log(result.code)

其實思路是一樣的,只要我們構建出一個 AST 節點,再把這個節點再插到原本的 AST 樹上,那么這個 AST 樹就可以轉化成我們想要的代碼了。

最后

這里我們了解了 AST 樹,理解了我們學習工作中常用的 Babel 到底背后做了哪些事,我們就可以更好的融入到前端開發的社區,去做更多的事情。更多更復雜更詳細的用法,都可以參照 Babel 官網,或 Babel 插件手冊。當然有興趣的同學也可以把玩一下 CSS 的相關 AST 編譯,原理都是一樣的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM