概要
目前在使用的三大框架(vue.js, react.js, angular.js)都有相應的腳手架工具已經貼心的幫我們集成了babel的各種配置,因此我們少了很多配置的工作量,一條命令就可以開始開發業務代碼了,覺得自己又牛逼閃閃了。顯示那是我們的錯覺,與大佬之間的距離也許就差一個對babel深度的學習,接下來我們來詳細了解一下babel的配置,讓你見到這些配置時不再一臉懵逼。
餐前小菜
babel將es6+(指es6及以上版本)分為
- 語法層:
let
、const
、class
、箭頭函數
等,這些需要在構建時進行轉譯,是指在語法層面上的轉譯,(比如class...將來會被轉譯成var function...) - api層:
Promise
、includes
、map
等,這些是在全局或者Object、Array等的原型上新增的方法,它們可以由相應es5的方式重新定義babel對這兩個分類的轉譯的做法肯定是不一樣的,我們也需要給出相應的配置
准備工作
以windows系統為例:
在你的項目目錄下新建文件夾babel-demo, 執行cd babel-demo進入文件夾,執行npm init -y 初始化項目。
執行 npm install --save-dev @babel/core @babel/cli
安裝babel必要的依賴。
babel-core
:Babel 的核心,包含各個核心的 API,供 Babel 插件和打包工具使用babel-cli
:命令行對 js 文件進行換碼的工具
新建目錄src, 在目錄下新建index.js, 並寫下如下測試代碼:
在package.json中配置一條命令--compiler,用於執行babel
"scripts": {
"compiler": "babel src --out-dir lib --watch"
},
然后執行 npm run compiler
理論上babel會將let
都轉化成var
,但是你會發現這時候轉譯出來的代碼跟原來一樣,這是為啥呢?
Babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個 plugin 里面。因此當我們不配置任何插件時,經過 babel 處理的代碼和輸入是相同的。所以我們需要安裝插件。
插件(用於處理語法層)
兩種方式使用插件,一種是一個個的安裝(比較麻煩),另一種是以preset的方式安裝一組插件,我們當然要選省事的preset了。
- 首先安裝preset
npm i @babel/preset-env -D
,@babel/preset-env
包含的插件支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 階段) - 配置
建立 .babelrc文件或者babelconfig.js文件,添加以下代碼,babel會自動尋找這個文件
{
"presets": [
[
"@babel/preset-env"
]
]
}
補充說明:如果你用 Vue ,presets 一般是
@vue/app
,這個是把 在@babel/preset-env
包含的 plugins 上又加了很多自己定義的 plugins。
所有的 Vue CLI 應用都使用@vue/babel-preset-app
,它包含了babel-preset-env
、JSX 支持
以及為最小化包體積優化過的配置。@vue/babel-preset-app
已經默認配置了@babel/plugin-transform-runtime
。通過它的文檔可以查閱到更多細節和 preset 選項。
執行npm run compiler
命令,這時候就轉譯成功了,轉譯結果如下:
"use strict";
var a = 1;
var b = 2;
var c = a + b;
babel還可配置taget
或者提供.browserlist
文件,用於指定目標環境,這樣能使你的代碼體積保持更小。
//.browserslistrc
> 0.25%
not dead
polyfill(用於處理Api層)
上邊只驗證了語法層面的轉譯,接下來我們試試api層面的轉譯是怎樣的
我們修改index.js如下:
// Promise是api層面的,是一個es6中的全局對象
const p = new Promise((resolve, reject) => {
resolve(100);
});
執行npm run compiler
命令,編譯結果如下:
"use strict";
var p = new Promise(function (resolve, reject) {
resolve(100);
});
const-->var沒有問題,但是Promise並沒有任何變化,難道是我被詛咒了嗎,其實並不是,這時候就需要Polyfill
這個東西了。
polyfill
的中文意思是墊片,顧名思義就是墊平不同瀏覽器或者不同環境下的差異,讓新的內置函數、實例方法等在低版本瀏覽器中也可以使用。
使用方法如下:
安裝 @babel/polyfill
依賴。注意這是一個運行時依賴。所以不要加-dev,npm install --save @babel/polyfill@babel/polyfill
模塊包括 core-js
和一個自定義的 regenerator runtime
模塊,可以模擬完整的 ES2015+ 環境。然后在index.js中引入。
import '@babel/polyfill';
const p = new Promise((resolve, reject) => {
resolve(100);
});
轉譯結果如下:
"use strict";
require("@babel/polyfill");
var p = new Promise(function (resolve, reject) {
resolve(100);
});
雖然看起來Promise還是沒有轉譯,但是我們引入的 polyfill
中已經包含了對Promise
的定義,所以這時候代碼便可以在低版本瀏覽器中運行了。
useBuiltIns屬性
不知道大家又沒有想到這樣一個問題,我代碼里邊只用到了幾個es6,卻需要引入所有的墊片,這不合情理啊。要優化這一點,就需要用到useBuiltIns這個屬性了。useBuiltIns這一配置項,它的值有三種:
false
: 不對polyfills做任何操作entry
: 根據target中瀏覽器版本的支持,將polyfills拆分引入,僅引入有瀏覽器不支持的polyfillusage
(新):檢測代碼中ES6/7/8等的使用情況,僅僅加載代碼中用到的polyfills
然后我們修改配置如下:
執行npm run compiler
命令,這是命令行中會出現如下警告:
意思是需要我們指定corejs的版本。修改.babelrc如下:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
目前推薦使用的corejs版本是3版本,新的特性也只會添加在這個版本。此時,轉譯結果如下:
"use strict";
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var p = new Promise(function (resolve, reject) {
resolve(100);
});
@babel/plugin-transform-runtime解決代碼冗余
代碼冗余是出現在轉譯語法層時出現的問題。修改下我們的index.js,這次我們再寫一個語法層面的es6-->class(let,const這些對比不出問題,所以用class),看看babel會把class轉化成什么
//index.js
class Student {
constructor(name,age){
this.name = name;
this.age = age
}
}
轉換結果如下:
"use strict";
require("core-js/modules/es.function.name");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Student = function Student(name, age) {
_classCallCheck(this, Student);
this.name = name;
this.age = age;
};
從結果看似乎沒什么問題,但事實上,如果我們在其他文件中也使用了class,你會發現_classCallCheck在每個文件中都會出現,這就造成了代碼冗余(我們示例中只使用了class,可能冗余的不明顯,實際項目中這些函數可能是很長的)。
這時候需要用到另外一個插件了@babel/plugin-transform-runtime
。該插件會開啟對 Babel 注入的輔助函數(比如上邊的_classCallCheck)的復用,以節省代碼體積。
這些輔助函數在@babel/runtime
中,所以需要安裝@babel/runtime
,當然@babel/runtime
也是運行時依賴。(在對一些語法進行編譯的時候,babel需要借助一些輔助函數)
安裝@babel/plugin-transform-runtime和@babel/runtime npm i --save-dev @babel/plugin-transform-runtime @babel/runtime
然后修改配置:
{
"presets": [
["@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":3
}]
],
"plugins": [
["@babel/plugin-transform-runtime"]
]
}
轉換結果如下:
"use strict";
require("core-js/modules/es.function.name");
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var Student = function Student(name, age) {
(0, _classCallCheck2["default"])(this, Student);
this.name = name;
this.age = age;
};
可以發現,相關的輔助函數是以require的方式引入而不是被直接插入進來的,這樣就不會冗余了。
除了解決代碼冗余,@babel/plugin-transform-runtime還有另外一個重要的能力——解決全局污染。
解決全局污染
全局污染是出現在轉譯api層出現的問題
我們這次依然用Promise來做實驗
修改index.js
new Promise(function (resolve, reject) {
resolve(100);
});
轉譯結果如下:
"use strict";
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
new Promise(function (resolve, reject) {
resolve(100);
});
可以看出preset-env在處理例如Promise這種的api時,只是引入了core-js中的相關的js庫,這些庫重新定義了Promise,然后將其掛載到了全局。
這里的問題就是:必然會造成全局變量污染,同理其他的例如Array.from等會修改這些全局對象的原型prototype,這也會造成全局對象的污染。
解決方式就是:將core-js
交給transform-runtime
處理。添加一個配置即可,非常簡單。
{
"presets": [
["@babel/preset-env"]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
可以看出,我們是將core-js
這個屬性添加到@babel/plugin-transform-runtime
這個插件的配置下,讓這個插件處理,同時也不需要配置useBuiltIns
了,因為在babel7
中已經將其設置為默認值
(transform-runtime是利用plugin自動識別並替換代碼中的新特性,檢測到需要哪個就用哪個)
這里注意下:有的博客上說無法轉譯includes等實例方法,其實說錯了,官方文檔是這樣說的:corejs: 2僅支持全局變量(例如Promise)和靜態屬性(例如Array.from),corejs: 3還支持實例屬性(例如[].includes)。轉譯結果:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
new _promise["default"](function (resolve, reject) {
resolve(100);
});
可以看出,這時的代碼並沒有在全局直接添加一個Promise
,而是定義了一個_promise["default"]
方法,這樣便不會出現全局變量污染的情況所以綜上可得出@babel/plugin-transform-runtime
這個插件的強大之處有以下幾點:
實現對輔助函數的復用,解決轉譯語法層時出現的代碼冗余
解決轉譯api層出現的全局變量污染
但是transform-runtime也有缺點:
每個特性都會經歷檢測和替換,隨着應用增大,可能會造成轉譯效率不高
webpack中使用babel
安裝babel-loader, npm install --save-dev babel-loader
webpack.config.js配置如下:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
"plugins": [
["@babel/plugin-transform-runtime",{
"corejs":3
}]
]
}
}
}
]
}
使用vue-cli的同學需要注意, 因為babel-loader
很慢,所以webpack官方推薦轉譯盡可能少的文件(參考),所以vue-cli配置了該loader的exclude
選項,將node_moduls
中的文件排除了,但是這樣可能會造成某個依賴出現兼容性問題。所以,如果你的項目中某個依賴出現了兼容性問題,這可能就是原因。解決辦法就是在vue.config.js
中配置transpileDependencies
這個選項,Babel就會 顯式轉譯這個依賴。