iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 9
0
Modern Web

深入現代前端開發系列 第 9

Day9 為什麼前端需要工程化? — Babel

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

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

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

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

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

你可以到這裡查看更多歷史。

蠻有趣的是,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):

{
  "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 可以協助我們將 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

舉例一個蠻無聊的場景,我想要把所有有 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。你可以讓整個 plugin 更彈性一點而不是寫死在裡頭。

輸出後就變成:

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

小結


上一篇
Day8 為什麼前端需要工程化? — webpack
下一篇
Day10 有了 jQuery 為什麼要有xxx?
系列文
深入現代前端開發32

尚未有邦友留言

立即登入留言