說實話,我從工作開始就一直在接觸babel ,,然而對于babel 并沒有一個清晰的認(rèn)識,,只知道babel 是用于編譯javascript ,讓開發(fā)者能使用超前的ES6+ 語法進行開發(fā),。自己配置babel 的時候,,總是遇到很多困惑,下面我就以babel@7 為例,,重新簡單認(rèn)識下babel ,。 什么是babelBabel 是一個工具鏈,主要用于將 ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,,以便能夠運行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中,。 babel 的配置文件一般是根目錄下的.babelrc ,babel@7 目前已經(jīng)支持babel.config.js ,,不妨用babel.config.js 試試,。
泰拳警告babel 提供的基礎(chǔ)能力是語法轉(zhuǎn)換,,或者叫語法糖轉(zhuǎn)換。比如把箭頭函數(shù)轉(zhuǎn)為普通的function ,,而對于ES6 新引入的全局對象是默認(rèn)不做處理的,,如Promise , Map , Set , Reflect , Proxy 等。對于這些全局對象和新的API ,,需要用墊片polyfill 處理,,core-js 有提供這些內(nèi)容。
所以babel 做的事情主要是: 根據(jù)你的配置做語法糖解析,,轉(zhuǎn)換 根據(jù)你的配置塞入墊片polyfill
如果不搞清楚這點,,babel 的文檔看起來會很吃力! 必須掌握的概念pluginsbabel 默認(rèn)不做任何處理,,需要借助插件來完成語法的解析,,轉(zhuǎn)換,,輸出,。
插件分為語法插件Syntax Plugins 和轉(zhuǎn)換插件Transform Plugins 。 語法插件語法插件僅允許babel 解析語法,,不做轉(zhuǎn)換操作,。我們主要關(guān)注的是轉(zhuǎn)換插件。 轉(zhuǎn)換插件轉(zhuǎn)換插件,,顧名思義,,負責(zé)的是語法轉(zhuǎn)換。 轉(zhuǎn)換插件將啟用相應(yīng)的語法插件,,如果啟用了某個語法的轉(zhuǎn)換插件,,則不必再另行指定相應(yīng)的語法插件了。 語法轉(zhuǎn)換插件有很多,,從ES3 到ES2018 ,,甚至是一些實驗性的語法和相關(guān)框架生態(tài)下的語法,都有相關(guān)的插件支持,。 語法轉(zhuǎn)換插件主要做的事情有: 利用@babel/parser 進行詞法分析和語法分析,,轉(zhuǎn)換為AST --> 利用babel-traverse 進行AST 轉(zhuǎn)換(涉及添加,更新及移除節(jié)點等操作) --> 利用babel-generator 生成目標(biāo)環(huán)境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"
]
插件開發(fā)我們自己也可以開發(fā)插件,,官網(wǎng)上的一個非常簡單的小例子: export default function() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
presetspreset ,,意為“預(yù)設(shè)”,其實是一組plugin 的集合,。我的理解是,,根據(jù)這項配置,,babel 會為你預(yù)設(shè)(或稱為“內(nèi)置”)好一些ECMA 標(biāo)準(zhǔn),草案,,或提案下的語法或API ,,甚至是你自己寫的一些語法規(guī)則。當(dāng)然,,這都是基于plugin 實現(xiàn)的,。
官方presets@babel/preset-env @babel/preset-flow @babel/preset-react @babel/preset-typescript
@babel/preset-env@babel/preset-env 提供了一種智能的預(yù)設(shè),根據(jù)配置的options 來決定支持哪些能力,。
我們看看關(guān)鍵的options 有哪些,。 描述你的項目要支持的目標(biāo)環(huán)境。寫法源于開源項目browserslist,。這項配置應(yīng)該根據(jù)你需要兼容的瀏覽器而設(shè)置,,不必與其他人一模一樣。示例如下: "targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 9"]
}
可以直譯為“松散模式”,,默認(rèn)為false ,,即為normal 模式。簡單地說,,就是normal 模式轉(zhuǎn)換出來的代碼更貼合ES6 風(fēng)格,,更嚴(yán)謹(jǐn);而loose 模式更像我們平時的寫法,。以class 寫法舉例: 我們先寫個簡單的class : class TestBabelLoose {
constractor(name) {
this.name = name
}
getName() {
return this.name
}
}
new TestBabelLoose('Tusi')
使用normal 模式編譯得到結(jié)果如下: 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 模式編譯得到結(jié)果是這樣的,,是不是更符合我們用prototype 實現(xiàn)類的寫法? "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 ,,當(dāng)然也要結(jié)合項目實際去考量哪種模式更合適,。 可選值有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false ,默認(rèn)是auto 該配置將決定是否把ES6 模塊語法轉(zhuǎn)換為其他模塊類型,。注意,,cjs 是commonjs 的別名。 其實我一直有個疑惑,,為什么我看到的開源組件中,,基本都是設(shè)置的modules: false ?后面終于明白了,,原來這樣做的目的是把轉(zhuǎn)換模塊類型的處理權(quán)交給了webpack ,,由webpack 去處理這項任務(wù)。所以,,如果你也使用webpack ,,那么設(shè)置modules: false 就沒錯啦。 可選值有:"entry" | "usage" | false ,,默認(rèn)是false 該配置將決定@babel/preset-env 如何去處理polyfill "entry"
如果useBuiltIns 設(shè)置為"entry" ,,我們需要安裝@babel/polyfill ,,并且在入口文件引入@babel/polyfill ,最終會被轉(zhuǎn)換為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 設(shè)置為"usage" ,我們無需安裝@babel/polyfill ,,babel 會根據(jù)你實際用到的語法特性導(dǎo)入相應(yīng)的polyfill ,,有點按需加載的意思。 // 上個例子中,,如果改用useBuiltIns: 'usage',,最終轉(zhuǎn)換的結(jié)果,,只有四個模塊
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 設(shè)置為false ,babel 不會自動為每個文件加上polyfill ,,也不會把import "@babel/polyfill" 轉(zhuǎn)為一個個獨立的core-js 模塊,。 stage-xstage-x 描述的是ECMA 標(biāo)準(zhǔn)相關(guān)的內(nèi)容,。根據(jù)TC39 (ECMA 39號技術(shù)專家委員會)的提案劃分界限,,stage-x 大致分為以下幾個階段:
stage-0:strawman ,還只是一種設(shè)想,,只能由TC39 成員或者TC39 貢獻者提出,。 stage-1:proposal ,提案階段,,比較正式的提議,,只能由TC39 成員發(fā)起,這個提案要解決的問題須有正式的書面描述,,一般會提出一些案例,,以及API ,語法,,算法的雛形,。 stage-2:draft ,,草案,有了初始規(guī)范,,必須對功能的語法和語義進行正式描述,,包括一些實驗性的實現(xiàn),也可以提出一些待辦事項,。 stage-3:condidate ,,候選,該提議基本已經(jīng)實現(xiàn),,需要等待實踐驗證,,用戶反饋及驗收測試通過。 stage-4:finished ,,已完成,,必須通過Test262 驗收測試,下一步就是納入到ECMA 標(biāo)準(zhǔn)中,。比如一些ES2016 ,,ES2017 的語法就是通過這個階段被合入ECMA 標(biāo)準(zhǔn)中了。
有興趣了解的可以關(guān)注ecma262,。 需要注意的是,,babel@7已經(jīng)移除了stage-x的preset,stage-4部分的功能已經(jīng)被@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:///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如需創(chuàng)建一個自己的preset ,,只需導(dǎo)出一份配置即可,,主要是通過寫plugins 來實現(xiàn)preset 。此外,,我們也可以在自己的preset 中包含第三方的preset ,。 module.exports = function() {
return {
// 增加presets項去包含別人的preset
presets: [
require("@babel/preset-env")
],
// 用插件來包裝成自己的preset
plugins: [
"pluginA",
"pluginB",
"pluginC"
]
};
}
@babel/runtimebabel 運行時,很重要的一個東西,,它一定程度上決定了你產(chǎn)出的包的大?。∫话氵m合于組件庫開發(fā),,而不是應(yīng)用級的產(chǎn)品開發(fā),。
說明這里有兩個東西要注意,一個是@babel/runtime ,,它包含了大量的語法轉(zhuǎn)換包,,會根據(jù)情況被按需引入。另一個是@babel/plugin-transform-runtime ,,它是插件,,負責(zé)在babel 轉(zhuǎn)換代碼時分析詞法語法,,分析出你真正用到的ES6+ 語法,然后在transformed code 中引入對應(yīng)的@babel/runtime 中的包,,實現(xiàn)按需引入,。 舉個例子,我用到了展開運算符... ,,那么經(jīng)過@babel/plugin-transform-runtime 處理后的結(jié)果是這樣的: /* 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 是需要按需引入到生產(chǎn)環(huán)境中的,,而@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'有什么區(qū)別,?兩者看起來都實現(xiàn)了按需加載的能力,,但是實際上作用是不一樣的。@babel/runtime 處理的是語法支持,,把新的語法糖轉(zhuǎn)為目標(biāo)環(huán)境支持的語法,;而useBuiltIns: 'usage' 處理的是墊片polyfill ,為舊的環(huán)境提供新的全局對象,,如Promise 等,,提供新的原型方法支持,如Array.prototype.includes 等,。如果你開發(fā)的是組件庫,,一般不建議處理polyfill 的,應(yīng)該由調(diào)用者去做這些支持,,防止重復(fù)的polyfill ,。 babel@7要注意的地方最后簡單地提一下使用babel@7 要注意的地方,,當(dāng)然更詳細的內(nèi)容還是要看babel官方,。 babel@7 相關(guān)的包命名都改了,基本是@babel/plugin-xxx , @babel/preset-xxx 這種形式,。這是開發(fā)插件體系時一個比較標(biāo)準(zhǔn)的命名和目錄組織規(guī)范,。
建議用babel.config.js 代替.babelrc ,這在你要支持不同環(huán)境時特別有用,。 babel@7 已經(jīng)移除了stage-x 的presets ,,也不鼓勵再使用@babel/polyfill 。
不要再使用babel-preset-es2015 , babel-preset-es2016 等preset 了,,應(yīng)該用@babel/preset-env 代替,。 ......
結(jié)語本人只是對babel 有個粗略的認(rèn)識,所以這是一篇babel 入門的簡單介紹,,并沒有提到深入的內(nèi)容,,可能也存在錯誤之處,。自己翻來覆去也看過好幾遍babel 的文檔了,一直覺得收獲不大,,也沒理解到什么東西,,在與webpack 配合使用的過程中,還是有很多疑惑沒搞懂的,。其實錯在自己不該在復(fù)雜的項目中直接去實踐,。在最近重新學(xué)習(xí)webpack 和babel 的過程中,我覺得,,對于不是很懂的東西,,我們不妨從寫一個hello world 開始,因為不是每個人都是理解能力超群的天才......
|