# Babel 簡介

ES2015 是 ECMAScript 2015 的簡稱。是一套規範怎麼實作 JavaScript 這個語言的細節,並且跟以往的版本比起來多了許多簡潔的語法跟功能。

不過顯然寫 Javascript 的開發者老早就厭倦了老舊的 JavaScript 寫法,而 ES6 提出的語法簡潔討喜,所以很快地成為主流並且深受開發者喜愛。

Day2 [JavaScript 基礎] 淺談 ECMAScript 與 JavaScript (opens new window)有談過,一套規範從草案到定案,到瀏覽器實作,往往需要一段不短的時間,如果直接使用這些語法,難免會有不相容的問題,尤其是各種不同的瀏覽器支援,更是令人不寒而慄。

你的使用者並不會在乎你的 Javascript 是用 ES5 還是 ES6 寫的,也不會在意你用 ajax 還是 fetch,但如果這些語法讓功能壞掉,使用者跟你的老闆還是會氣得跺腳。

為了解決這些問題(恐怕不只這樣),babel 問世了。簡單來說,babel 是一個解析器,能夠將 Javascript 轉為 AST(抽象語法樹),再透過 plugin 將 AST 轉換成瀏覽器看得懂的程式碼。

你可以到這裡 (opens new window)查看更多歷史。

蠻有趣的是,2015 年剛好也是 React 逐漸竄紅的一年,因此 babel 除了支援 ES6 外,也因為有 JSX 加持,讓 React 得以迅速走紅。不過到底是先有 babel 還是先有 jsx 呢?

# Babel 原理

Babel 的原理主要是將 Javascript 先轉換成抽象語法樹,再用各種 plugin 來轉換對應的程式碼。

寫一個完整的 Parser 不是件容易的事,首先你必須要實作整個 JavaScript 的語法以及結構,還有各種有關編譯器、語法解析的知識。

不過對於開發者來說,知道 AST 抽象語法樹的概念後,就可以拿來應用在很多地方,甚至自己動手寫個 babel plugin 也沒有問題。

# 什麼是 AST (抽象語法樹)

我並不打算詳細談論抽象語法樹的構成,但所有的程式語言可以大致區分為 keywords, expression, declration, identifier 等 token。舉例來說,一行 console.log('hello world);,轉換為語法樹是這樣子(可利用 AST explorer (opens new window)):

{
  "type": "Program",
  "start": 0,
  "end": 40,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 27,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 26,
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 11,
          "object": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 12,
            "end": 25,
            "value": "hello world",
            "raw": "'hello world'"
          }
        ]
      }
    }
}

console.log('') 是一個 Expression,當中的 console.log 是個 Call Expression,代表呼叫某個函數,而 . 的操作則是 Member Expression。consolelog 都是 Identifier,而 hello world 是 Literal。

有了這棵語法樹,我們就可以對程式碼做各種複雜的操作。

const { name, age } = person; // es6

var name = person.name; // es5
var age = person.age; // es5

為了讓 es6 的語法也能夠在較為老舊的瀏覽器上正常運作,我們透過 babel 先將 Javascript 編譯過一遍,轉換為等效的程式碼。

這件事聽起來很奇怪,用 Javascript 寫了一個 Parser 並且將 Javascript 轉換成 Javascript?這其實不能怪罪到 JavaScript 上,畢竟使用者是在瀏覽器上看網頁,當然不能強求他們更新或是要求他們只能用 chrome,不像後端伺服器一樣想升級就升級、想換語言就換語言。

如果你喜歡 old-school 的寫法,倒也不用大費周章安裝一堆 babel 套件,但如果能把程式碼寫得順眼漂亮一點,誰不想呢?這並不是說你用 ES2015 寫程式就會馬上功力大增,而是我們可以用更簡潔有力的語法來建構我們的程式。

一旦有了抽象語法樹,就可以很容易對程式碼做操作,例如為了支援瀏覽器,要將全部 VariableDeclaration 的類型全部改成 var 好了,只要尋訪樹裡頭所有 VariableDeclaration 並且將 kind 改成 var 就行了。

現在你知道 babel 是什麼了,一個很經典的使用案例是 React 剛推出時所採用的 JSX讓 React 大放異彩。

Babel 和 React 幾乎是同時竄紅,或者說是兩者相輔相成。

為什麼呢?我們可以用 markup 方式而不是一連串的 function call 來描述 UI,是一件相當棒的事。當然你要全部用 React.createElement 來寫也是沒有問題的。

function MyComponent() {
  return React.createElement("div", null, React.createElement("span", null, "hello world"));
}
// 等價於
function MyComponent() {
    return <div>
      <span>hello world</span>
    </div>
}

當然並不是所有的人都是 jsx 的擁護者,也有部分人反對這樣子的寫法,認為 <> 妨礙了他們的閱讀,不如 function call 的方式直白。

# JSX 如何轉換為 React.createElement

babel-plugin-transform-jsx (opens new window) 可以協助我們將 jsx 語法轉為 AST。而 React 的 jsx 則是利用了 plugin-transform-react-jsx 將 jsx 轉為 React.createElement 的。

當然,你也不必限定於 React.createElement,舉例來說好了,你自己也寫了一個超猛的 VirtualDOM 系統跟 render 機制,你也想使用 jsx,就可以利用這個套件來寫要怎麼處理 jsx

一旦你會使用 Babel,你會發現你不但可以自行轉換一些比較 legacy 的程式碼,甚至可以分析程式碼。

# DIY babel plugin

要寫自己的 babel plugin,可以參考 babel-handbook (opens new window)

舉例一個蠻無聊的場景,我想要把所有有 console.log 的程式碼抽換成最近剛寫好的 Logger.log(args),這雖然可以很簡單用編輯器尋找與取代就能達成,但這次我們用 babel 來試試看。

console.log('There is logging');

這是我的原始碼,我希望所有用到 console.log 地方全部替換成我剛寫好的 Logger

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

const visitor = {
   Expression(path) {
    if (t.isCallExpression(path.node)) {
      if (t.isMemberExpression(path.node.callee)) {
        const { callee } = path.node;
        const args = path.node.arguments;

        if (
          t.isIdentifier(callee.object) &&
          t.isIdentifier(callee.property) &&
          callee.object.name === 'console' &&
          callee.property.name === 'log'
        ) {
          const callExpr = t.callExpression(
            t.memberExpression(t.identifier('Logger'), t.identifier('log')),
            args
          );
          path.replaceWith(t.inherits(callExpr, path.node));
        }
      }
    }
  },
}

module.exports = (api, state) => ({
  name: 'transform-console-log',
  visitor
});

console.log() 是個 member call expression,所以在這邊我們判斷如果目前的 node 是 console.log 的呼叫的話,就替換成 Logger.log 的 expression。

輸出後就變成:

Logger.log('There is error!');

# 小結

  • Babel 是個 Javascript 的 Parser,核心代碼實現在 babylon (opens new window) (現在為 https://github.com/babel/babel/tree/master/packages/babel-parser)主要原理是將 Javascript 的程式碼轉為 AST 後,可利用 plugin 的方式轉換程式碼。
  • 我們可以利用 babel 做到很多事情,甚至解決一些重構上可能不好做的事