對 Babel
的配置項的做用不那麼瞭解,是否會影響平常開發呢?老實說,大多狀況下沒有特別大的影響(畢竟有搜索引擎)。javascript
不過呢,仍是想更進一步瞭解下,因而最近認真閱讀了 Babel
的文檔,外加不斷編譯驗證,輸出了本篇文章,爲了更好的閱讀體驗,修修改改,最終算是以我我的比較喜歡的方式推動了每一個知識點(每個配置的引入都是有緣由的),但願可以幫助你對 Babel
的各類配置有一個更清晰的認識 (已經很懂的小夥伴,無視本文) 。html
Babel 是一個 JS 編譯器java
Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript 語法,以便可以運行在當前和舊版本的瀏覽器或其餘環境中。node
咱們先看看 Babel
可以作什麼:react
Polyfill
方式在目標環境中添加缺失的特性(@babel/polyfill模塊
)本篇文章的目的是搞明白 Babel
的使用和配置,搞清楚 @babel/runtime
,@babel/polyfill
,@babel/plugin-transform-runtime
這些做用是什麼,插件和預設都是用來幹什麼的,咱們爲何須要配置它們,而不是講如何進行 AST
轉換,若是你對 AST
轉換很是感興趣,歡迎閱讀咱們的 RN轉小程序引擎 Alita 的源碼,其中應用了大量的 AST
轉換。webpack
更多文章可戳(如Star,謝謝你):https://github.com/YvetteLau/Bloggit
爲了更清晰的瞭解每一步,首先建立一個新項目,例如 babelTemp
(你愛取啥名取啥名),使用 npm init -y
進行初始化,建立 src/index.js
,文件內容以下(你也能夠隨便寫點什麼):github
const fn = () => {
console.log('a');
};
複製代碼
OK,建立好的項目先放在一邊,先了解下理論知識:web
Babel 的核心功能包含在 @babel/core
模塊中。看到 core
這個詞了吧,意味着核心,沒有它,在 babel
的世界裏註定步履維艱。不安裝 @babel/core
,沒法使用 babel
進行編譯。chrome
babel
提供的命令行工具,主要是提供 babel
這個命令,適合安裝在項目裏。
@babel/node
提供了 babel-node
命令,可是 @babel/node
更適合全局安裝,不適合安裝在項目裏。
npm install --save-dev @babel/core @babel/cli
複製代碼
如今你就能夠在項目中使用 babel
進行編譯啦(若是不安裝 @babel/core
,會報錯噢)
將命令配置在 package.json
文件的 scripts
字段中:
//...
"scripts": {
"compiler": "babel src --out-dir lib --watch"
}
複製代碼
使用 npm run compiler
來執行編譯,如今咱們沒有配置任何插件,編譯先後的代碼是徹底同樣的。
由於 Babel
雖然開箱即用,可是什麼動做也不作,若是想要 Babel
作一些實際的工做,就須要爲其添加插件(plugin
)。
Babel
構建在插件之上,使用現有的或者本身編寫的插件能夠組成一個轉換通道,Babel
的插件分爲兩種: 語法插件和轉換插件。
語法插件
這些插件只容許 Babel
解析(parse) 特定類型的語法(不是轉換),能夠在 AST
轉換時使用,以支持解析新語法,例如:
import * as babel from "@babel/core";
const code = babel.transformFromAstSync(ast, {
//支持可選鏈
plugins: ["@babel/plugin-proposal-optional-chaining"],
babelrc: false
}).code;
複製代碼
轉換插件
轉換插件會啓用相應的語法插件(所以不須要同時指定這兩種插件),這點很容易理解,若是不啓用相應的語法插件,意味着沒法解析,連解析都不能解析,又何談轉換呢?
若是插件發佈在 npm
上,能夠直接填寫插件的名稱, Babel
會自動檢查它是否已經被安裝在 node_modules
目錄下,在項目目錄下新建 .babelrc
文件 (下文會具體介紹配置文件),配置以下:
//.babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
複製代碼
也能夠指定插件的相對/絕對路徑
{
"plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"]
}
複製代碼
執行 npm run compiler
,能夠看到箭頭函數已經被編譯OK, lib/index.js
內容以下:
const fn = function () {
console.log('a');
};
複製代碼
如今,咱們僅支持轉換箭頭函數,若是想將其它的新的JS特性轉換成低版本,須要使用其它對應的 plugin
。若是咱們一個個配置的話,會很是繁瑣,由於你可能須要配置幾十個插件,這顯然很是不便,那麼有沒有什麼辦法能夠簡化這個配置呢?
有!預設!(感謝強大的 Babel
)
經過使用或建立一個 preset
便可輕鬆使用一組插件。
官方 Preset
注: 從 Babel v7 開始,因此針對標準提案階段的功能所編寫的預設(stage preset)都已被棄用,官方已經移除了 @babel/preset-stage-x
。
@babel/preset-env
主要做用是對咱們所使用的而且目標瀏覽器中缺失的功能進行代碼轉換和加載 polyfill
,在不進行任何配置的狀況下,@babel/preset-env
所包含的插件將支持全部最新的JS特性(ES2015,ES2016等,不包含 stage 階段),將其轉換成ES5代碼。例如,若是你的代碼中使用了可選鏈(目前,仍在 stage 階段),那麼只配置 @babel/preset-env
,轉換時會拋出錯誤,須要另外安裝相應的插件。
//.babelrc
{
"presets": ["@babel/preset-env"]
}
複製代碼
須要說明的是,@babel/preset-env
會根據你配置的目標環境,生成插件列表來編譯。對於基於瀏覽器或 Electron
的項目,官方推薦使用 .browserslistrc
文件來指定目標環境。默認狀況下,若是你沒有在 Babel
配置文件中(如 .babelrc)設置 targets
或 ignoreBrowserslistConfig
,@babel/preset-env
會使用 browserslist
配置源。
若是你不是要兼容全部的瀏覽器和環境,推薦你指定目標環境,這樣你的編譯代碼可以保持最小。
例如,僅包括瀏覽器市場份額超過0.25%的用戶所需的 polyfill
和代碼轉換(忽略沒有安全更新的瀏覽器,如 IE10 和 BlackBerry):
//.browserslistrc
> 0.25%
not dead
複製代碼
例如,你將 .browserslistrc
的內容配置爲:
last 2 Chrome versions
複製代碼
而後再執行 npm run compiler
,你會發現箭頭函數不會被編譯成ES5,由於 chrome
的最新2個版本都可以支持箭頭函數。如今,咱們將 .browserslistrc
仍然換成以前的配置。
就我們目前的代碼來講,當前的配置彷佛已是OK的了。
咱們修改下 src/index.js
。
const isHas = [1,2,3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});
複製代碼
編譯出來的結果爲:
"use strict";
var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});
複製代碼
這個編譯出來的代碼在低版本瀏覽器中使用的話,顯然是有問題的,由於低版本瀏覽器中數組實例上沒有 includes
方法,也沒有 Promise
構造函數。
這是爲何呢?由於語法轉換隻是將高版本的語法轉換成低版本的,可是新的內置函數、實例方法沒法轉換。這時,就須要使用 polyfill
上場了,顧名思義,polyfill
的中文意思是墊片,所謂墊片就是墊平不一樣瀏覽器或者不一樣環境下的差別,讓新的內置函數、實例方法等在低版本瀏覽器中也可使用。
@babel/polyfill
模塊包括 core-js
和一個自定義的 regenerator runtime
模塊,能夠模擬完整的 ES2015+ 環境(不包含第4階段前的提議)。
這意味着可使用諸如 Promise
和 WeakMap
之類的新的內置組件、 Array.from
或 Object.assign
之類的靜態方法、Array.prototype.includes
之類的實例方法以及生成器函數(前提是使用了 @babel/plugin-transform-regenerator
插件)。爲了添加這些功能,polyfill
將添加到全局範圍和相似 String
這樣的內置原型中(會對全局環境形成污染,後面咱們會將不污染全局環境的方法)。
首先,安裝 @babel/polyfill
依賴:
npm install --save @babel/polyfill
複製代碼
注意:不使用 --save-dev
,由於這是一個須要在源碼以前運行的墊片。
咱們須要將完整的 polyfill
在代碼以前加載,修改咱們的 src/index.js
:
import '@babel/polyfill';
const isHas = [1,2,3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});
複製代碼
@babel/polyfill
須要在其它代碼以前引入,咱們也能夠在 webpack
中進行配置。
例如:
entry: [
require.resolve('./polyfills'),
path.resolve('./index')
]
複製代碼
polyfills.js
文件內容以下:
//固然,還可能有一些其它的 polyfill,例如 stage 4以前的一些 polyfill
import '@babel/polyfill';
複製代碼
如今,咱們的代碼無論在低版本仍是高版本瀏覽器(或node環境)中都能正常運行了。不過,不少時候,咱們未必須要完整的 @babel/polyfill
,這會致使咱們最終構建出的包的體積增大,@babel/polyfill
的包大小爲89K (當前 @babel/polyfill
版本爲 7.7.0)。
咱們更指望的是,若是我使用了某個新特性,再引入對應的 polyfill
,避免引入無用的代碼。
值得慶幸的是, Babel
已經考慮到了這一點。
@babel/preset-env
提供了一個 useBuiltIns
參數,設置值爲 usage
時,就只會包含代碼須要的 polyfill
。有一點須要注意:配置此參數的值爲 usage
,必需要同時設置 corejs
(若是不設置,會給出警告,默認使用的是"corejs": 2) ,注意: 這裏仍然須要安裝 @babel/polyfill
(當前 @babel/polyfill
版本默認會安裝 "corejs": 2):
首先說一下使用 core-js@3
的緣由,core-js@2
分支中已經不會再添加新特性,新特性都會添加到 core-js@3
。例如你使用了 Array.prototype.flat()
,若是你使用的是 core-js@2
,那麼其不包含此新特性。爲了可使用更多的新特性,建議你們使用 core-js@3
。
安裝依賴依賴:
npm install --save core-js@3
複製代碼
core-js (點擊瞭解更多) : JavaScript 的模塊化標準庫,包含
Promise
、Symbol
、Iterator
和許多其餘的特性,它可讓你僅加載必需的功能。
如今,修改 Babel
的配置文件以下:
//.babelrc
const presets = [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
複製代碼
Babel
會檢查全部代碼,以便查找在目標環境中缺失的功能,而後僅僅把須要的 polyfill
包含進來。
例如,src/index.js
代碼不變:
const isHas = [1,2,3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});
複製代碼
咱們看看編譯出來的文件(lib/index
):
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});
複製代碼
一樣的代碼,咱們用 webpack
構建一下(production
模式),能看到最終的代碼大小僅爲: 20KB。而若是咱們引入整個 @babel/polyfill
的話,構建出的包大小爲:89KB
前面曾提到,在 useBuiltIns
參數值爲 usage
時,仍然須要安裝 @babel/polyfill
,雖然咱們上面的代碼轉換中看起來並無使用到,可是,若是咱們源碼中使用到了 async/await
,那麼編譯出來的代碼須要 require("regenerator-runtime/runtime")
,在 @babel/polyfill
的依賴中,固然啦,你也能夠只安裝 regenerator-runtime/runtime
取代安裝 @babel/polyfill
。
到了這一步,已經很棒棒了,是否是想跳起來轉個圈圈?
下面我要說的內容,也許你已經知道,也許你還不知道,這都不重要,可是此刻起,你要知道了: Babel
會使用很小的輔助函數來實現相似 _createClass
等公共方法。默認狀況下,它將被添加(inject
)到須要它的每一個文件中。
假如,咱們的 src/index.js
是這樣的:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
};
getX() {
return this.x;
}
}
let cp = new ColorPoint(25, 8);
複製代碼
編譯出來的 lib/index.js
,以下所示:
"use strict";
require("core-js/modules/es.object.define-property");
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 Point =
/*#__PURE__*/
function () {
function Point(x, y) {
_classCallCheck(this, Point);
this.x = x;
this.y = y;
}
_createClass(Point, [{
key: "getX",
value: function getX() {
return this.x;
}
}]);
return Point;
}();
var cp = new ColorPoint(25, 8);
複製代碼
看起來,彷佛並無什麼問題,可是你想一下,若是你有10個文件中都使用了這個 class
,是否是意味着 _classCallCheck
、_defineProperties
、_createClass
這些方法被 inject
了10次。這顯然會致使包體積增大,最關鍵的是,咱們並不須要它 inject
屢次。
這個時候,就是 @babel/plugin-transform-runtime
插件大顯身手的時候了,使用 @babel/plugin-transform-runtime
插件,全部幫助程序都將引用模塊 @babel/runtime
,這樣就能夠避免編譯後的代碼中出現重複的幫助程序,有效減小包體積。
@babel/plugin-transform-runtime
是一個能夠重複使用 Babel
注入的幫助程序,以節省代碼大小的插件。
注意:諸如
Array.prototype.flat()
等實例方法將不起做用,由於這須要修改現有的內置函數(可使用@babel/polyfill
來解決這個問題) ——> 對此須要說明的是若是你配置的是corejs3
,core-js@3
如今已經支持原型方法,同時不污染原型。
另外,@babel/plugin-transform-runtime
須要和 @babel/runtime
配合使用。
首先安裝依賴,@babel/plugin-transform-runtime
一般僅在開發時使用,可是運行時最終代碼須要依賴 @babel/runtime
,因此 @babel/runtime
必需要做爲生產依賴被安裝,以下 :
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
複製代碼
除了前文所說的,@babel/plugin-transform-runtime
能夠減小編譯後代碼的體積外,咱們使用它還有一個好處,它能夠爲代碼建立一個沙盒環境,若是使用 @babel/polyfill
及其提供的內置程序(例如 Promise
,Set
和 Map
),則它們將污染全局範圍。雖然這對於應用程序或命令行工具多是能夠的,可是若是你的代碼是要發佈供他人使用的庫,或者沒法徹底控制代碼運行的環境,則將成爲一個問題。
@babel/plugin-transform-runtime
會將這些內置別名做爲 core-js
的別名,所以您能夠無縫使用它們,而無需 polyfill
。
修改 .babelrc
的配置,以下:
//.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime"
]
]
}
複製代碼
從新編譯 npm run compiler
, 如今,編譯出來的內容爲(lib/index.js
):
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var Point =
/*#__PURE__*/
function () {
function Point(x, y) {
(0, _classCallCheck2.default)(this, Point);
this.x = x;
this.y = y;
}
(0, _createClass2.default)(Point, [{
key: "getX",
value: function getX() {
return this.x;
}
}]);
return Point;
}();
var cp = new ColorPoint(25, 8);
複製代碼
能夠看出,幫助函數如今不是直接被 inject
到代碼中,而是從 @babel/runtime
中引入。前文說了使用 @babel/plugin-transform-runtime
能夠避免全局污染,咱們來看看是如何避免污染的。
修改 src/index.js
以下:
let isHas = [1,2,3].includes(2);
new Promise((resolve, reject) => {
resolve(100);
});
複製代碼
編譯出來的代碼以下(lib/index.js
):
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var isHas = [1, 2, 3].includes(2);
new Promise(function (resolve, reject) {
resolve(100);
});
複製代碼
Array.prototype
上新增了 includes
方法,而且新增了全局的 Promise
方法,污染了全局環境,這跟不使用 @babel/plugin-transform-runtime
沒有區別嘛。
若是咱們但願 @babel/plugin-transform-runtime
不只僅處理幫助函數,同時也能加載 polyfill
的話,咱們須要給 @babel/plugin-transform-runtime
增長配置信息。
首先新增依賴 @babel/runtime-corejs3
:
npm install @babel/runtime-corejs3 --save
複製代碼
修改配置文件以下(移除了 @babel/preset-env
的 useBuiltIns
的配置,否則不就重複了嘛嘛嘛,不信的話,你用 async/await
編譯下試試咯):
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",{
"corejs": 3
}
]
]
}
複製代碼
而後從新編譯,看一下,編譯出來的結果(lib/index.js
):
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _context;
var isHas = (0, _includes.default)(_context = [1, 2, 3]).call(_context, 2);
new _promise.default(function (resolve, reject) {
resolve(100);
});
複製代碼
能夠看出,沒有直接去修改 Array.prototype
,或者是新增 Promise
方法,避免了全局污染。若是上面 @babel/plugin-transform-runtime
配置的 core-js
是 "2",其中不包含實例的 polyfill
須要單獨引入。
劃重點:若是咱們配置的
corejs
是3
版本,那麼無論是實例方法仍是全局方法,都不會再污染全局環境。
看到這裏,不知道你們有沒有這樣一個疑問?給 @babel/plugin-transform-runtime
配置 corejs
是如此的完美,既能夠將幫助函數變成引用的形式,又能夠動態引入 polyfill
,而且不會污染全局環境。何須要給 @babel/preset-env
提供 useBuiltIns
功能呢,看起來彷佛不須要呀。
帶着這樣的疑問,我新建了幾個文件(內容簡單且基本一致,使用了些新特性),而後使用 webpack
構建,如下是我對比的數據:
序號 | .babelrc 配置 | webpack mode production |
---|---|---|
0 | 不使用 @babel/plugin-transform-runtime |
36KB |
1 | 使用@babel/plugin-transform-runtime ,並配置參數 corejs : 3。不會污染全局環境 |
37KB |
2 | 使用@babel/plugin-transform-runtime ,不配置 corejs |
22KB |
我猜想是 @babel/runtime-corejs3/XXX
的包自己比 core-js/modules/XXX
要大一些~
插件的排列順序很重要!!!
若是兩個轉換插件都將處理「程序(Program)」的某個代碼片斷,則將根據轉換插件或 preset
的排列順序依次執行。
例如:
{
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"]
}
複製代碼
先執行 @babel/plugin-proposal-class-properties
,後執行 @babel/plugin-syntax-dynamic-import
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
複製代碼
preset
的執行順序是顛倒的,先執行 @babel/preset-react
, 後執行 @babel/preset-env
。
插件和 preset
均可以接受參數,參數由插件名和參數對象組成一個數組。preset
設置參數也是這種格式。
如:
{
"plugins": [
[
"@babel/plugin-proposal-class-properties",
{ "loose": true }
]
]
}
複製代碼
若是插件名稱爲 @babel/plugin-XXX
,可使用短名稱@babel/XXX
:
{
"plugins": [
"@babel/transform-arrow-functions" //同 "@babel/plugin-transform-arrow-functions"
]
}
複製代碼
若是插件名稱爲 babel-plugin-XXX
,可使用端名稱 XXX
,該規則一樣適用於帶有 scope
的插件:
{
"plugins": [
"newPlugin", //同 "babel-plugin-newPlugin"
"@scp/myPlugin" //同 "@scp/babel-plugin-myPlugin"
]
}
複製代碼
能夠簡單的返回一個插件數組
module.exports = function() {
return {
plugins: [
"A",
"B",
"C"
]
}
}
複製代碼
preset
中也能夠包含其餘的preset
,以及帶有參數的插件。
module.exports = function() {
return {
presets: [
require("@babel/preset-env")
],
plugins: [
[require("@babel/plugin-proposal-class-properties"), { loose: true }],
require("@babel/plugin-proposal-object-rest-spread")
]
}
}
複製代碼
Babel 支持多種格式的配置文件。這部份內容補充瞭解下便可,誰管你用哪一種配置文件,只要你的配置是OK的就能夠了(敷衍)~
全部的 Babel
API 參數均可以被配置,可是若是該參數須要使用的 JS 代碼,那麼可能須要使用 JS 代碼版的配置文件。
根據使用場景能夠選擇不一樣的配置文件:
若是但願以編程的方式建立配置文件或者但願編譯 node_modules
目錄下的模塊:那麼 babel.config.js
能夠知足你的需求。
若是隻是須要一個簡單的而且中用於單個軟件包的配置:那麼 .babelrc
便可知足你的需求。
在項目根目錄下建立一個名爲 babel.config.js
的文件。
module.exports = function(api) {
api.cache(true);
const presets = [...];
const plugins = [...];
return {
presets,
plugins
};
}
複製代碼
具體的配置能夠查看:babel.config.js 文檔
在項目根目錄下建立一個名爲 .babelrc
的文件:
{
"presets": [],
"plugins": []
}
複製代碼
具體的配置能夠參考 .babelrc 文檔
能夠將 .babelrc
中的配置信息做爲 babel
鍵(key) 添加到 package.json
文件中:
{
"name": "my-package",
"babel": {
"presets": [],
"plugins": []
}
}
複製代碼
與 .babelrc
配置相同,可是可使用JS編寫。
//能夠在其中調用 Node.js 的API
const presets = [];
const plugins = [];
module.exports = { presets, plugins };
複製代碼
不知道是否全面,不過真的寫不動了(若有不全,後續再補充)~就醬~若是有錯誤,歡迎指正。
參考連接