[轉] babel 教程
在前端開發領域,瀏覽器兼容性問題從來不曾消失。除了 CSS,我們還要面對 JavaScript 的兼容性問題。
不同的瀏覽器講著不同的 JavaScript 語言,不同的瀏覽器版本同樣講著不同的 JavaScript 語言。
你用了 JavaScript 的 A 特性,能夠在 B 瀏覽器上正常運行,卻在 C 瀏覽器的 D 版本上報錯。
這正是 Babel.js 要解決的問題。更進一步,它能夠讓所有瀏覽器上都還不能正常運行的特性正常運行在所有瀏覽器上。
也因此,Babel 項目非常龐大,而且在不斷地更新、調整,這意味著,一篇教程不可能囊括所有 - 當然,我也沒那種打算。
本文基於 babel 7.0.0-beta.39。
babel-cli
babel-cli
是 babel 提供的命令行工具,用於命令行下編譯我們的源代碼 - 至於腳本語言用“編譯”一詞是否合適,我們且留給他人去論證。
這裏假定我們已經初始化一個項目。
執行如下命令可以在項目下安裝 babel-cli
:
npm install --save-dev @babel/core @babel/cli
嗯?怎麽不是 npm install --save-dev babel-cli
?@
符號又是什麽意思?這正是 babel 7 的一大調整,從原來的 babel-xx
的包命名格式遷移到域內包(scoped package)。如果你有興趣,可以閱讀 babel 博客上重命名包名稱的說明。
假定我們的項目下有一個 babel.test.js
文件,內容是:
let fun = () => console.log(‘hello babel.js‘)
我們試試運行 npx babel babel.test.js
:
$ npx babel babel.test.js
let fun = () => console.log(‘hello babel.js‘);
還是原來的代碼,沒有任何變化。說好的編譯呢?
這個調整則是在 babel 6 裏發生的。Babel 6 做了大量模塊化的工作,將原來集成一體的各種編譯功能分離出去,獨立成插件。這意味著,默認情況下,當下版本的 babel 不會編譯代碼。
babel 插件
換句話說,我們要將上面的箭頭函數編譯成 ES5 函數,需要額外安裝 babel 插件。
首先,安裝 @babel/plugin-transform-arrow-functions:
npm install --save-dev @babel/plugin-transform-arrow-functions
然後,在命令行編譯時指定使用該插件:
$ npx babel babel.test.js --plugins @babel/plugin-transform-arrow-functions
let fun = function () {
return console.log(‘hello babel.js‘);
};
編譯成功。
配置文件 .babelrc
隨著各種新插件的加入,我們的命令行參數只會越來越長。
這時,我們可以新建一個 .babelrc
文件,把各種命令行參數統一到其中。
比如,要配置前面提到過的箭頭函數插件:
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
之後,在命令行只要運行 npx babel babel.test.js
即可,babel 會自動讀取 .babelrc
裏的配置並應用到編譯中:
$ npx babel babel.test.js
let fun = function () {
return console.log(‘hello babel.js‘);
};
babel 套餐
我們有一個項目,頁面要求支持 IE 10,但 IE 10 不支持箭頭函數、class
、const
,可是你喜歡用這些新增的 JavaScript 語法,你在項目裏寫了這麽一段代碼:
const alertMe = (msg) => {
window.alert(msg)
}
class Robot {
constructor (msg) {
this.message = msg
}
say () {
alertMe(this.message)
}
}
const marvin = new Robot(‘hello babel‘)
顯然,在 IE 10 下這段代碼報錯了。
好消息是,babel 有各類插件滿足你的上述需求。
我們來安裝相應的插件:
$ npm install --save-dev @babel/plugin-transform-arrow-functions @babel/plugin-transform-block-scoping @babel/plugin-transform-classes
接著,將它們加入 .babelrc
配置文件中:
{
"plugins": [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-classes"
]
}
然後運行 npx babel babel.test.js
,就有編譯結果了:
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var alertMe = function (msg) {
window.alert(msg);
};
var Robot = function () {
function Robot(msg) {
_classCallCheck(this, Robot);
this.message = msg;
}
_createClass(Robot, [{
key: ‘say‘,
value: function say() {
alertMe(this.message);
}
}]);
return Robot;
}();
var marvin = new Robot(‘hello babel‘);
只是,這樣安裝插件、配置 .babelrc
的過程非常乏味,而且容易出錯。通常,我們不會關心到具體的某個 ES2015 特性支持情況這個層面,我們更關心瀏覽器版本這個層面。
你說,我不想關心 babel 插件的配置,我只希望,給 babel 一個我想支持 IE 10 的提示,babel 就幫我編譯出能在 IE 10 上正常運行的 JavaScript 代碼。
歡迎 @babel/preset-env。
等等,Preset 是什麽?前面我們已經認識了插件,那麽不妨把 Preset 理解為套餐,每個套餐裏打包了不同的插件,這樣安裝套餐就等於一次性安裝各類 babel 插件。
我們來看看怎樣使用 @babel/preset-env
。
首先在項目下安裝:
$ npm install --save-dev @babel/preset-env
然後修改 .babelrc
:
{
"presets": ["@babel/preset-env"]
}
運行 npx babel babel.test.js
,輸出結果如下:
‘use strict‘;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var alertMe = function alertMe(msg) {
window.alert(msg);
};
var Robot = function () {
function Robot(msg) {
_classCallCheck(this, Robot);
this.message = msg;
}
_createClass(Robot, [{
key: ‘say‘,
value: function say() {
alertMe(this.message);
}
}]);
return Robot;
}();
var marvin = new Robot(‘hello babel‘);
Wow,與前面辛苦配置各種插件後的輸出結果幾乎一模一樣。
可是,我們還沒告訴 babel 我們要支持 IE 10 的,為什麽它卻好像預知一切?
我們來看 babel-preset-env
的一段文檔:
Without any configuration options, babel-preset-env behaves exactly the same as babel-preset-latest (or babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 together).
默認情況下,babel-preset-env
等效於三個套餐,而不巧我們前面安裝的幾個插件已經囊括在 babel-preset-es2015
中。
那麽,如果我只想支持最新版本的 Chrome 呢?
這時我們可以調整 .babelrc
的配置:
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["last 1 Chrome versions"]
}
}]
]
}
再次編譯,結果如下:
$ npx babel babel.test.js
‘use strict‘;
const alertMe = msg => {
window.alert(msg);
};
class Robot {
constructor(msg) {
this.message = msg;
}
say() {
alertMe(this.message);
}
}
const marvin = new Robot(‘hello babel‘);
最新版本的 Chrome 已經支持箭頭函數、class
、const
,所以 babel 在編譯過程中,不會編譯它們。這也是為什麽我們把 @babel/preset-env
稱為 JavaScript 的 Autoprefixer。
babel-polyfill
Babel includes a polyfill that includes a custom regenerator runtime and core-js.
基本上,babel-polyfill
就是 regenerator runtime
加 core-js
。
可是,為什麽需要 polyfill 這所謂的墊片?前面聊到 @babel/preset-env
時,不是說只要定義好我想支持的目標瀏覽器,babel 就能編譯出能運行在目標瀏覽器上的代碼嗎?
我們暫時去掉 babel-
,從 polyfill 說起。
拿 findIndex
來說,IE 11 仍不支持該方法,假如你的代碼裏寫了 findIndex
,IE 11 瀏覽器會報如下錯誤:
Object doesn‘t support property or method ‘findIndex‘
怎麽辦,這時我們就可以寫個 polyfill:
// https://tc39.github.io/ecma262/#sec-array.prototype.findIndex
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, ‘findIndex‘, {
value: function(predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError(‘"this" is null or not defined‘);
}
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== ‘function‘) {
throw new TypeError(‘predicate must be a function‘);
}
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
// 5. Let k be 0.
var k = 0;
// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return k.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return k;
}
// e. Increase k by 1.
k++;
}
// 7. Return -1.
return -1;
}
});
}
如果目標環境中已經存在 findIndex
,我們什麽都不做,如果沒有,我們就在 Array
的原型中定義一個。這便是 polyfill 的意義。babel-polyfill
同理。
雖說瀏覽器的特性支持狀況千差萬別,但其實可以提煉出兩類:
- 大家都有,只是 A 語法與 B 語法的區別;
- 不是大家都有:有的有,有的沒有。
babel 編譯過程處理第一種情況 - 統一語法的形態,通常是高版本語法編譯成低版本的,比如 ES6 語法編譯成 ES5 或 ES3。而 babel-polyfill
處理第二種情況 - 讓目標瀏覽器支持所有特性,不管它是全局的,還是原型的,或是其它。這樣,通過 babel-polyfill
,不同瀏覽器在特性支持上就站到同一起跑線。
下面我們看看 babel-polyfill
的用法。
安裝 babel-polyfill
$ npm install --save @babel/polyfill
使用 babel-polyfill
我們需要在程序入口文件的頂部引用 @babel-polyfill
:
require(‘@babel/polyfill‘)
[].findIndex(‘babel‘)
或者使用 ES6 的寫法:
import ‘@babel/polyfill‘
[].findIndex(‘babel‘)
需要註意的是,babel-polyfill
不能多次引用。如果我們的代碼中有多個 require(‘@babel/polyfill‘)
,則執行時會報告錯誤:
only one instance of @babel/polyfill is allowed
這是因為引入的 babel-polyfill
會在全局寫入一個 _babelPolyfill
變量。第二次引入時,會檢測該變量是否已存在,如果已存在,則拋出錯誤。
註意事項
如前面所說的,babel-polyfill
其實包含 regenerator runtime
、core-js
,如果你的代碼只需要其中一部分 polyfill,那麽你可以考慮直接引入 core-js
下的特定 polyfill,不必使用 babel-polyfill
這樣的龐然大物。
babel-runtime
babel-runtime
是 babel 生態裏最讓人困惑的一個包。
我們先來看看它的 package.json
裏的 description
怎麽寫:
babel selfContained runtime
有點不知所謂。
不過從 package.json
裏沒有 main
字段我們可以看出,它的用法肯定不是 require(‘babel-runtime‘)
這樣。
我們再看包依賴:
"dependencies": {
"core-js": "^2.5.3",
"regenerator-runtime": "^0.11.1"
}
跟 babel-polyfill
的包依賴 一模一樣。
這正是讓人疑惑不解的地方,為什麽要有兩個不同名稱卻相同依賴的包?它們的目的是否一樣,是否能夠共用?
我們拿 Object.assign
為例,剖析下 babel-polyfill
與 babel-runtime
的異同。
我們知道,IE 11 不支持 Object.assign
,此時,我們有倆種候選方案:
- 引入
babel-polyfill
,這樣Object.assign
就會被創造出來 - 配置
@babel/plugin-transform-object-assign
第二種方案中,babel 會將所有的 Object.assign
替換成 _extends
這樣一個輔助函數。如下所示:
Object.assign({}, {})
編譯成:
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
_extends({}, {});
問題是,如果你的項目裏有 100 個文件,其中有 50 個文件裏寫了 Object.assign
,那麽,壞消息來了,_extends
輔助函數會出現 50 次。
怎麽辦?我們自然而然會想到把 _extends
分離出去,然後在每個文件中引入 - 這正是 babel-runtime
的作用:
var _extends = require("@babel/runtime/helpers/extends");
_extends({}, {});
非常漂亮。可沒人想要手動轉換這些代碼。
babel 提供了 @babel/plugin-transform-runtime
來做這些轉換。
@babel/plugin-transform-runtime
我們首先安裝插件:
$ npm install --save-dev @babel/plugin-transform-runtime
然後再安裝 babel-runtime
:
$ npm install @babel/runtime
最後在 .babelrc
中配置:
{
"plugins": [
"@babel/plugin-transform-object-assign",
"@babel/plugin-transform-runtime"
]
}
這樣,我們不需要 babel-polyfill
也一樣可以在程序中使用 Object.assign
,編譯後的代碼最終能夠正常運行在 IE 11 下。
提問:在經過
@babel/plugin-transform-runtime
的處理後,IE 11 下現在有Object.assign
嗎?
答案是,仍然沒有。
這正是 babel-polyfill
與 babel-runtime
的一大區別,前者改造目標瀏覽器,讓你的瀏覽器擁有本來不支持的特性;後者改造你的代碼,讓你的代碼能在所有目標瀏覽器上運行,但不改造瀏覽器。
如果你還是困惑,我推薦一個非常簡單的區分方法 - 打開瀏覽器開發者工具,在 console 裏執行代碼:
- 引入
babel-polyfill
後的 IE 11,你可以在 console 下執行Object.assign({}, {})
- 而引入
babel-runtime
後的 IE 11,仍然提示你:Object doesn‘t support property or method ‘assign‘
再回到我們前面提出的一個問題:babel-polyfill
是否可以跟 babel-runtime
共用?
這個問題,且留給讀者們自己探索。
babel-register
經過 babel 的編譯後,我們的源代碼與運行在生產下的代碼是不一樣的。
babel-register
則提供了動態編譯。換句話說,我們的源代碼能夠真正運行在生產環境下,不需要 babel 編譯這一環節。
我們先在項目下安裝 babel-register
:
$ npm install --save-dev @babel/register
然後在入口文件中 require
:
require(‘@babel/register‘)
require(‘./app‘)
在入口文件頭部引入 @babel/register
後,我們的 app
文件中即可使用任意 es2015 的特性。
當然,壞處是動態編譯,導致程序在速度、性能上有所損耗。
babel-node
我們上面說,babel-register
提供動態編譯,能夠讓我們的源代碼真正運行在生產環境下 - 但其實不然,我們仍需要做部分調整,比如新增一個入口文件,並在該文件中 require(‘@babel/register‘)
。而 babel-node
能真正做到一行源代碼都不需要調整:
$ babel-node app.js
只是,請不要在生產環境中使用 babel-node
,因為它是動態編譯源代碼,應用啟動速度非常慢。
[轉] babel 教程