說實話,我從工作開始就一直在接觸babel
,然而對於babel
並沒有一個清晰的認識,只知道babel
是用於編譯javascript
,讓開發者能使用超前的ES6+
語法進行開發。自己配置babel
的時候,總是遇到很多困惑,下面我就以babel@7
爲例,重新簡單認識下babel
。
什麼是babel
Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。
babel
的配置文件一般是根目錄下的.babelrc
,babel@7
目前已經支持babel.config.js
,不妨用babel.config.js
試試。
泰拳警告
babel
提供的基礎能力是語法轉換,或者叫語法糖轉換。比如把箭頭函數轉爲普通的function
,而對於ES6
新引入的全局對象是默認不做處理的,如Promise
, Map
, Set
, Reflect
, Proxy
等。對於這些全局對象和新的API
,需要用墊片polyfill
處理,core-js
有提供這些內容。
所以babel
做的事情主要是:
- 根據你的配置做語法糖解析,轉換
- 根據你的配置塞入墊片
polyfill
如果不搞清楚這點,babel
的文檔看起來會很喫力!
必須掌握的概念
plugins
babel
默認不做任何處理,需要藉助插件來完成語法的解析,轉換,輸出。
插件分爲語法插件Syntax Plugins
和轉換插件Transform Plugins
。
語法插件
語法插件僅允許babel
解析語法,不做轉換操作。我們主要關注的是轉換插件。
轉換插件
轉換插件,顧名思義,負責的是語法轉換。
轉換插件將啓用相應的語法插件,如果啓用了某個語法的轉換插件,則不必再另行指定相應的語法插件了。
語法轉換插件有很多,從ES3
到ES2018
,甚至是一些實驗性的語法和相關框架生態下的語法,都有相關的插件支持。
語法轉換插件主要做的事情有:
利用@babel/parser
進行詞法分析和語法分析,轉換爲AST
–> 利用babel-traverse
進行AST
轉換(涉及添加,更新及移除節點等操作) –> 利用babel-generator
生成目標環境js
代碼
插件簡寫
babel@7
之前的縮寫形式是這樣的:
// 完整寫法
plugins: [
"babel-plugin-transform-runtime"
]
// 簡寫形式
plugins: [
"transform-runtime"
]
而在babel@7
之後,由於plugins
都歸到了@babel
目錄下,所以簡寫形式也有所改變:
// babel@7插件完整寫法
plugins: [
"@babel/plugin-transform-runtime"
]
// 簡寫形式,需要保留目錄
plugins: [
"@babel/transform-runtime"
]
插件開發
我們自己也可以開發插件,官網上的一個非常簡單的小例子:
export default function() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
presets
preset
,意爲“預設”,其實是一組plugin
的集合。我的理解是,根據這項配置,babel
會爲你預設(或稱爲“內置”)好一些ECMA
標準,草案,或提案下的語法或API
,甚至是你自己寫的一些語法規則。當然,這都是基於plugin
實現的。
官方presets
@babel/preset-env
@babel/preset-env
提供了一種智能的預設,根據配置的options
來決定支持哪些能力。
我們看看關鍵的options
有哪些。
- targets
描述你的項目要支持的目標環境。寫法源於開源項目browserslist。這項配置應該根據你需要兼容的瀏覽器而設置,不必與其他人一模一樣。示例如下:
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 9"]
}
- loose
可以直譯爲“鬆散模式”,默認爲false
,即爲normal
模式。簡單地說,就是normal
模式轉換出來的代碼更貼合ES6
風格,更嚴謹;而loose
模式更像我們平時的寫法。以class
寫法舉例:
我們先寫個簡單的class
:
class TestBabelLoose {
constractor(name) {
this.name = name
}
getName() {
return this.name
}
}
new TestBabelLoose('Tusi')
使用normal
模式編譯得到結果如下:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var TestBabelLoose =
/*#__PURE__*/
function () {
function TestBabelLoose() {
_classCallCheck(this, TestBabelLoose);
}
_createClass(TestBabelLoose, [{
key: "constractor",
value: function constractor(name) {
this.name = name;
}
}, {
key: "getName",
value: function getName() {
return this.name;
}
}]);
return TestBabelLoose;
}();
new TestBabelLoose('Tusi');
而使用loose
模式編譯得到結果是這樣的,是不是更符合我們用prototype
實現類的寫法?
"use strict";
var TestBabelLoose =
/*#__PURE__*/
function () {
function TestBabelLoose() {}
var _proto = TestBabelLoose.prototype;
_proto.constractor = function constractor(name) {
this.name = name;
};
_proto.getName = function getName() {
return this.name;
};
return TestBabelLoose;
}();
new TestBabelLoose('Tusi');
個人推薦配置loose: false
,當然也要結合項目實際去考量哪種模式更合適。
- modules
可選值有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false
,默認是auto
該配置將決定是否把ES6
模塊語法轉換爲其他模塊類型。注意,cjs
是commonjs
的別名。
其實我一直有個疑惑,爲什麼我看到的開源組件中,基本都是設置的modules: false
?後面終於明白了,原來這樣做的目的是把轉換模塊類型的處理權交給了webpack
,由webpack
去處理這項任務。所以,如果你也使用webpack
,那麼設置modules: false
就沒錯啦。
- useBuiltIns
可選值有:"entry" | "usage" | false
,默認是false
該配置將決定@babel/preset-env
如何去處理polyfill
"entry"
如果useBuiltIns
設置爲"entry"
,我們需要安裝@babel/polyfill
,並且在入口文件引入@babel/polyfill
,最終會被轉換爲core-js
模塊和regenerator-runtime/runtime
。對了,@babel/polyfill
也不會處理stage <=3
的提案。
我們用一段包含了Promise
的代碼來做下測試:
import "@babel/polyfill";
class TestBabelLoose {
constractor(name) {
this.name = name
}
getName() {
return this.name
}
testPromise() {
return new Promise(resolve => {
resolve()
})
}
}
new TestBabelLoose('Tusi')
但是編譯後,貌似引入了很多polyfill
啊,一共149個,怎麼不是按需引入呢?嗯…你需要往下看了。
import "core-js/modules/es6.array.map";
import "core-js/modules/es6.map";
import "core-js/modules/es6.promise";
import "core-js/modules/es7.promise.finally";
import "regenerator-runtime/runtime";
// 此處省略了144個包。。。
"usage"
如果useBuiltIns
設置爲"usage"
,我們無需安裝@babel/polyfill
,babel
會根據你實際用到的語法特性導入相應的polyfill
,有點按需加載的意思。
// 上個例子中,如果改用useBuiltIns: 'usage',最終轉換的結果,只有四個模塊
import "core-js/modules/es6.object.define-property";
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";
import "core-js/modules/es6.function.name";
配置"usage"
時,常搭配corejs
選項來指定core-js
主版本號
useBuiltIns: "usage",
corejs: 3
false
如果useBuiltIns
設置爲false
,babel
不會自動爲每個文件加上polyfill
,也不會把import "@babel/polyfill"
轉爲一個個獨立的core-js
模塊。
@babel/preset-env
還有一些配置,自己慢慢去折騰吧…
stage-x
stage-x
描述的是ECMA
標準相關的內容。根據TC39
(ECMA
39號技術專家委員會)的提案劃分界限,stage-x
大致分爲以下幾個階段:
- stage-0:
strawman
,還只是一種設想,只能由TC39
成員或者TC39
貢獻者提出。 - stage-1:
proposal
,提案階段,比較正式的提議,只能由TC39
成員發起,這個提案要解決的問題須有正式的書面描述,一般會提出一些案例,以及API
,語法,算法的雛形。 - stage-2:
draft
,草案,有了初始規範,必須對功能的語法和語義進行正式描述,包括一些實驗性的實現,也可以提出一些待辦事項。 - stage-3:
condidate
,候選,該提議基本已經實現,需要等待實踐驗證,用戶反饋及驗收測試通過。 - stage-4:
finished
,已完成,必須通過Test262
驗收測試,下一步就是納入到ECMA
標準中。比如一些ES2016
,ES2017
的語法就是通過這個階段被合入ECMA
標準中了。
有興趣瞭解的可以關注ecma262。
需要注意的是,babel@7已經移除了stage-x的preset,stage-4部分的功能已經被@babel/preset-env集成了,而如果你需要stage <= 3部分的功能,則需要自行通過plugins組裝。
As of v7.0.0-beta.55, we've removed Babel's Stage presets.
Please consider reading our blog post on this decision at
https://babeljs.io/blog/2018/07/27/removing-babels-stage-presets
for more details. TL;DR is that it's more beneficial in the long run to explicitly add which proposals to use.
If you want the same configuration as before:
{
"plugins": [
// Stage 2
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
// Stage 3
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
["@babel/plugin-proposal-class-properties", { "loose": false }],
"@babel/plugin-proposal-json-strings"
]
}
自己寫preset
如需創建一個自己的preset
,只需導出一份配置即可,主要是通過寫plugins
來實現preset
。此外,我們也可以在自己的preset
中包含第三方的preset
。
module.exports = function() {
return {
// 增加presets項去包含別人的preset
presets: [
require("@babel/preset-env")
],
// 用插件來包裝成自己的preset
plugins: [
"pluginA",
"pluginB",
"pluginC"
]
};
}
@babel/runtime
babel
運行時,很重要的一個東西,它一定程度上決定了你產出的包的大小!一般適合於組件庫開發,而不是應用級的產品開發。
說明
這裏有兩個東西要注意,一個是@babel/runtime
,它包含了大量的語法轉換包,會根據情況被按需引入。另一個是@babel/plugin-transform-runtime
,它是插件,負責在babel
轉換代碼時分析詞法語法,分析出你真正用到的ES6+
語法,然後在transformed code
中引入對應的@babel/runtime
中的包,實現按需引入。
舉個例子,我用到了展開運算符...
,那麼經過@babel/plugin-transform-runtime
處理後的結果是這樣的:
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
var arrayWithoutHoles = __webpack_require__(2);
var iterableToArray = __webpack_require__(3);
var nonIterableSpread = __webpack_require__(4);
function _toConsumableArray(arr) {
return arrayWithoutHoles(arr) || iterableToArray(arr) || nonIterableSpread();
}
module.exports = _toConsumableArray;
// EXTERNAL MODULE: ../node_modules/@babel/runtime/helpers/toConsumableArray.js
var toConsumableArray = __webpack_require__(0);
var toConsumableArray_default = /*#__PURE__*/__webpack_require__.n(toConsumableArray);
安裝和簡單配置
@babel/runtime
是需要按需引入到生產環境中的,而@babel/plugin-transform-runtime
是babel
輔助插件。因此安裝方式如下:
npm i --save @babel/runtime
npm i --save-dev @babel/plugin-transform-runtime
配置時也挺簡單:
const buildConfig = {
presets: [
// ......
],
plugins: [
"@babel/plugin-transform-runtime"
],
// ......
}
@babel/runtime和useBuiltIns: 'usage’有什麼區別?
兩者看起來都實現了按需加載的能力,但是實際上作用是不一樣的。@babel/runtime
處理的是語法支持,把新的語法糖轉爲目標環境支持的語法;而useBuiltIns: 'usage'
處理的是墊片polyfill
,爲舊的環境提供新的全局對象,如Promise
等,提供新的原型方法支持,如Array.prototype.includes
等。如果你開發的是組件庫,一般不建議處理polyfill
的,應該由調用者去做這些支持,防止重複的polyfill
。
- 開發組件時,如果僅使用
@babel/plugin-transform-runtime
- 加上
useBuiltIns: 'usage'
,多了很多不必要的包。
babel@7要注意的地方
最後簡單地提一下使用babel@7
要注意的地方,當然更詳細的內容還是要看babel官方。
babel@7
相關的包命名都改了,基本是@babel/plugin-xxx
,@babel/preset-xxx
這種形式。這是開發插件體系時一個比較標準的命名和目錄組織規範。- 建議用
babel.config.js
代替.babelrc
,這在你要支持不同環境時特別有用。 babel@7
已經移除了stage-x
的presets
,也不鼓勵再使用@babel/polyfill
。- 不要再使用
babel-preset-es2015
,babel-preset-es2016
等preset
了,應該用@babel/preset-env
代替。 - …
結語
本人只是對babel
有個粗略的認識,所以這是一篇babel
入門的簡單介紹,並沒有提到深入的內容,可能也存在錯誤之處。自己翻來覆去也看過好幾遍babel
的文檔了,一直覺得收穫不大,也沒理解到什麼東西,在與webpack
配合使用的過程中,還是有很多疑惑沒搞懂的。其實錯在自己不該在複雜的項目中直接去實踐。在最近重新學習webpack
和babel
的過程中,我覺得,對於不是很懂的東西,我們不妨從寫一個hello world
開始,因爲不是每個人都是理解能力超羣的天才…