# 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。console
與 log
都是 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 做到很多事情,甚至解決一些重構上可能不好做的事