Babel7 知識梳理
對 Babel 的配置項的作用不那么了解,是否會影響日常開發呢?老實說,大多情況下沒有特別大的影響(畢竟有搜索引擎)。
不過呢,還是想更進一步了解下,於是最近認真閱讀了 Babel 的文檔,外加不斷編譯驗證,輸出了本篇文章,為了更好的閱讀體驗,修修改改,最終算是以我個人比較喜歡的方式推進了每個知識點(每一個配置的引入都是有原因的),希望能夠幫助你對 Babel 的各種配置有一個更清晰的認識 (已經很懂的小伙伴,無視本文) 。
Babel 是一個 JS 編譯器
Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。
我們先看看 Babel 能夠做什么:
- 語法轉換
- 通過
Polyfill方式在目標環境中添加缺失的特性(@babel/polyfill模塊) - 源碼轉換(codemods)
本篇文章的目的是搞明白 Babel 的使用和配置,搞清楚 @babel/runtime,@babel/polyfill,@babel/plugin-transform-runtime 這些作用是什么,插件和預設都是用來干什么的,我們為什么需要配置它們,而不是講如何進行 AST 轉換,如果你對 AST 轉換非常感興趣,歡迎閱讀我們的 RN轉小程序引擎 Alita 的源碼,其中應用了大量的 AST 轉換。
更多文章可戳(如Star,謝謝你):https://github.com/YvetteLau/Blog
為了更清晰的了解每一步,首先創建一個新項目,例如 babelTemp(你愛取啥名取啥名),使用 npm init -y 進行初始化,創建 src/index.js,文件內容如下(你也可以隨便寫點什么):
const fn = () => { console.log('a'); };
OK,創建好的項目先放在一邊,先了解下理論知識:
核心庫 @babel/core
Babel 的核心功能包含在 @babel/core 模塊中。看到 core 這個詞了吧,意味着核心,沒有它,在 babel 的世界里注定寸步難行。不安裝 @babel/core,無法使用 babel 進行編譯。
CLI命令行工具 @babel/cli
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/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
注: 從 Babel v7 開始,所以針對標准提案階段的功能所編寫的預設(stage preset)都已被棄用,官方已經移除了 @babel/preset-stage-x。
@babel/preset-env
@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的中文意思是墊片,所謂墊片就是墊平不同瀏覽器或者不同環境下的差異,讓新的內置函數、實例方法等在低版本瀏覽器中也可以使用。
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/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 的排列順序依次執行。
- 插件在 Presets 前運行。
- 插件順序從前往后排列。
- 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" ] }
創建 Preset
可以簡單的返回一個插件數組
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
在項目根目錄下創建一個名為 babel.config.js 的文件。
module.exports = function(api) { api.cache(true); const presets = [...]; const plugins = [...]; return { presets, plugins }; }
具體的配置可以查看:babel.config.js 文檔
.babelrc
在項目根目錄下創建一個名為 .babelrc 的文件:
{ "presets": [], "plugins": [] }
具體的配置可以參考 .babelrc 文檔
package.json
可以將 .babelrc 中的配置信息作為 babel 鍵(key) 添加到 package.json 文件中:
{ "name": "my-package", "babel": { "presets": [], "plugins": [] } }
.babelrc.js
與 .babelrc 配置相同,但是可以使用JS編寫。
//可以在其中調用 Node.js 的API const presets = []; const plugins = []; module.exports = { presets, plugins };
不知道是否全面,不過真的寫不動了(如有不全,后續再補充)~就醬~如果有錯誤,歡迎指正。
參考鏈接
歡迎關注公眾號,進一步技術交流:

