Babel 配置用法解析
剛復工的時候我司業務太多了,我已不記得我們連續作戰了多少天,最近算是有時間可以學習學習我的babel大寶貝了,上周末看了下babel的一些核心模塊以及babel的一些配置,今天繼續以博客的形式記錄總結下來。
寫前面:babel默認是只會去轉義js語法的,不會去轉換新的API,比如像Promise、Generator、Symbol這種全局API對象,babel是不會去編譯的。在我學會了babe配置l大法之后,看我一會兒怎么把這些新的API給它編譯出來就完事兒了。
本文基於babel7.8.0。我主要記錄下babel配置需要的一些重要的模塊兒包,來一步步進行babel的一個配置解析(以babel.config.js方式配置為例)。
本文主要涉及到的一些babel包:@babel/core
,@babel/cli
,@babel/plugin*
,@babel/preset-env
,@babel/polyfill
,@babel/runtime
,@babel/plugin-transform-runtime
。
那,話不多說,發車?
@babel/core
@babel/core 這個包里主要都是一些去對代碼進行轉換的核心方法,具體里面的一些api方法我就不做介紹了;引用官網的一句話:“所有轉換將使用本地配置文件”,所以待會兒我們的babel.config.js配置文件就很重要了;再一個core就是核心的意思嘛,所以我們話不多說先把它裝起來,gogogo
npm install --save-dev @babel/core
@babel/cli
這個 @babel/cli就是babel帶有的內置cli,可以用來讓我們從命令行來編譯我們的文件,有了他我們就很方便的對babel進行學習了,那話不多說,先裝起來?
npm install --save-dev @babel/cli
裝完之后你就可以這樣來編譯你的文件:
npx babel study.js --watch --out-file study-compiled.js
簡單介紹下上面命令用到的幾個參數:--out-file
用來把編譯后的目標文件輸出到對應的文件;如果希望在每次更改目標文件時都進行編譯,可以加上 --watch
選項;當然還有一些別的選項,不過在我學習babel以及配置的話,這兩個選項已經夠我用了。
在操作的過程中如果改了babel配置發現編譯出來的文件並沒有實時編譯的情況,需要注意下,如果改了配置文件那就需要重新執行這段命令,要不然babel讀不到新的配置。
如果你已經創建了study.js文件並且執行了這段命令,你會發現,對應的study-compiled.js還沒發生變化,因為我們還沒開始寫babel的配置文件,莫慌,馬上開始。
@babel/plugin*和@babel/preset-env
babel插件和babel預設是babel配置的兩個主要模塊,所以我就放在一起說了。
@babel/plugin*
首先我們先來說下babel的Plugins,babel官網也說了,人babel是基於插件化的,大概就是說全是插件,所以說我們配置文件里如果什么插件也不配的話,那輸入和輸出就是一樣的,插件插件,你得插上我才讓你用。我來編譯一個最簡單的箭頭函數來看下這個babel的插件怎么用,來了,這波我們就需要配置文件了,以下所有的配置都是說的在babel.config.js文件里,相應的插件記得install
/* babel.config.js */
module.exports = {
presets: [
],
plugins: [
"@babel/plugin-transform-arrow-functions"
]
}
然后執行我們上面那段cli的命令,就會得到這種效果:
/* study.js */
const study = () => {}
/* study-compiled.js */
const study = function () {};
當然,如果我們想要使用es6給數值新增的指數運算符怎么辦,只需要添加相應的 @babel/plugin-transform-exponentiation-operator 插件即可:
/* babel.config.js */
module.exports = {
presets: [
],
plugins: [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-exponentiation-operator"
]
}
對應的es6語法就會變成:
/* study.js */
const exponentiation = 2 ** 2
/* study-compiled.js */
const exponentiation = Math.pow(2, 2);
@babel/preset-env
從上面的插件化我們就知道需要哪個插件就去引入就完事兒,那么問題來了,es6新增的語法那么多,我總不能一個插件一個插件去添加吧,這樣也太麻煩了,這個時候就要用到babel提供的presets了。
presets也就是預設的意思,大概意思就是可以預先設定好一些東西,就省得我們一個個的去引入插件了。官方提供了很多presets,比如preset-env(處理es6+規范語法的插件集合)、preset-stage(一些處理尚在提案階段的語法的插件集合,當然這種預設的方式在 babel 7+ 版本已經被廢棄了)、preset-react(處理react語法的插件集合)等等。
我們主要來介紹下preset-env:preset-env是一個智能預設,配置了它就可以讓你用es6+去書寫你的代碼,而且他會按需去加載所需要的插件,讓你的生活更加美好。。。接下來我們記得先install這個 @babel/preset-env一波,不配任何插件,然后我們再來看看效果如何:
/* babel.config.js */
module.exports = {
presets: [
"@babel/preset-env"
],
plugins: [
]
}
對應的es6語法就會變成:
/* study.js */
const study = () => {}
const arr1 = [1, 2, 33]
const arr2 = [...arr1]
const exponentiation = 2 ** 2
// 新增API
new Promise(() => {})
new Map()
/* study-compiled.js */
var study = function study() {};
var arr1 = [1, 2, 33];
var arr2 = [].concat(arr1);
var exponentiation = Math.pow(2, 2);
// 新增API
new Promise(function () {});
new Map();
你會發現es6+的語法都被編譯了,我們並沒有設置任何插件哦,應該也看到了新增的API方法並沒有被編譯,在這里我們埋下伏筆,等下文講到polyfill的時候再治他。
Browserslist集成
關於preset-env,我們還可以提供一個targets
配置項指定運行環境,就是我們可以配置對應目標瀏覽器環境,那么babel就會編譯出對應目標瀏覽器環境可以運行的代碼。相信有同學遇到過在低版本系統ios手機里自己的項目會白屏,其實是某些語法在ios低版本系統里不支持,這個時候我們可以直接配置ios 7瀏覽器環境都可以支持的代碼:
/* babel.config.js */
module.exports = {
presets: [
[
"@babel/preset-env", {
'targets': {
'browsers': ['ie >= 8', 'iOS 7'] // 支持ie8,直接使用iOS瀏覽器版本7
}
}
]
],
plugins: [
]
}
當然babel的Browserslist集成還支持在package.json文件里或者新建一個 .browserslistrc 文件來指定對應目標環境。browserslist配置源
@babel/polyfill(由core-js2和regenerator-runtime組成的一個集成包)
上文也提到了像Promise這種API咱們的babel並沒有給轉義,那是因為babel默認是只會去轉義js語法的,不會去轉換新的API,比如像Promise、Generator、Symbol這種全局API對象,babel是不會去編譯的,這個時候就要掏出 @babel/polyfill 了。用法很簡單,先安裝一波,然后我們只需要在入口文件頂部引入 @babel/polyfill 就可以使用新增的API了。
/* study.js */
import '@babel/polyfill'
// 新增API
new Promise(function () {});
/* study-compiled.js */
require("@babel/polyfill");
// 新增API
new Promise(function () {});
小細節:import被編譯成了require,如果想要編譯出來的模塊引入規范還是import,則可以在preset-env的配置項中添加"modules": false即可。
modules的options:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默認為"auto"
但是問題又來了,有時候我們項目里並沒有用到那么多的新增API,但是 @babel/polyfill 會把所有瀏覽器環境的的polyfill都引入,整個包的體積就會很大,我們想要對目標環境按需引入相應的polyfill應該怎么辦呢,這個時候我們就可以使用 preset-env 的配置項中的useBuiltIns
屬性來按需引入polyfill。
/* babel.config.js */
module.exports = {
presets: [
[
"@babel/preset-env", {
"modules": false,
"useBuiltIns": "entry",
'targets': {
'browsers': ['ie >= 8', 'iOS 7'] // 支持ie8,直接使用iOS瀏覽器版本7
}
}
]
],
plugins: [
]
}
這個時候就會在入口處只把所有ie8以上以及iOS 7瀏覽器不支持api的polyfill引入進來。
最終效果:
/* study.js */
import '@babel/polyfill'
// 新增API
new Promise(function () {});
/* study-compiled.js */
import "core-js/modules/es6.array.copy-within";
import "core-js/modules/es6.array.every";
...//省略若干
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";
import "regenerator-runtime/runtime";
// 新增API
new Promise(function () {});
此時你會發現,import '@babel/polyfill'沒有了,引入的是我們目標環境相應的polyfill。但是有沒有發現引入的都是import 'core-js/...'的內容,標題已經說啦,@babel/polyfil是由core-js2和regenerator-runtime組成的一個集成包。
這個時候你又會想,假如我的項目里面只用到了Promise這個API,能不能只給我引入Promise相應的API呢?答案是必可以!,讓我們先來好好了解下preset-env的配置項中的useBuiltIns
屬性。
useBuiltIns
選項:"usage"| "entry"| false,默認為false。
entry
我們已經用過了,意義就是在入口處將根據我們配置的瀏覽器兼容,將目標瀏覽器環境所有不支持的API都引入。
usage
就很nb了,當配置成usage的時候,babel會掃描你的每個文件,然后檢查你都用到了哪些新的API,跟進我們配置的瀏覽器兼容,只引入相應API的polyfill,我們把useBuiltIns
屬性設置為usage
再來看下編譯效果:
/* study.js */
import '@babel/polyfill'
// 新增API
new Promise(function () {});
/* study-compiled.js */
import "core-js/modules/es.object.to-string";
import "core-js/modules/es.promise";
// 新增API
new Promise(function () {});
我就問你帥不帥!完全的按需引入,牛逼了!
相信你也看到了一個東西,當我們使用useBuiltIns
選項的時候,你的命令行里面是不是顯示了一坨這樣的警告,大概是在配置文件中未指定core-js版本時,默認會使用core-js2:
WARNING: We noticed you're using the
useBuiltIns
option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via thecorejs
option.
前面也說到了 @babel/polyfil 是由core-js2和regenerator-runtime組成的一個集成包,現在core-js3已經發布了,而且很穩定。但是core-js2在18年的時候已經不再維護了;@babel/polyfil引入的是2不是3,並且 @babel/polyfill 在babel7.4.0已經不再推薦使用了,要廢掉(好像是因為@babel/polyfill不支持core-js2平滑的過渡到core-js3)。所以core-js官方現在推薦我們使用polyfill的時候直接引入core-js和regenerator-runtime/runtime這兩個包完全取代 @babel/polyfil 來為了防止重大更改。
當然,我們需要在preset-env配置項中指定core-js版本,這樣就不會再有警告⚠️了:
/* babel.config.js */
module.exports = {
presets: [
[
"@babel/preset-env", {
"modules": false,
"useBuiltIns": "entry",
"corejs": "3",
'targets': {
'browsers': ['not ie >= 8', 'iOS 7'] // 支持ie8,直接使用iOS瀏覽器版本7
}
}
]
],
plugins: [
]
}
@babel/runtime(依賴@babel/helpers和regenerator-runtime)
有的時候一些語法的轉換會比較復雜,babel會引入一些helper函數,比如說對es6的class進行轉換:
/* study.js */
class Test {}
/* study-compiled.js */
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Test = function Test() {
_classCallCheck(this, Test);
};
可以看到上面引入了helper函數來處理class的轉換。但是問題又來了,如果好多文件都使用到了復雜語法的轉換,這個還是簡單點的,有些helper函數是很復雜代碼量很多的,那豈不是每個文件都會定義一遍這些個函數,每個文件的代碼會很多?如果說可以把這些helper函數都抽離到一個公共的包里,用到的地方只需要引入對應的函數即可,我們的編譯出來的代碼量會大大滴減少,這個時候就需要用到 @babel/plugin-transform-runtime 插件來配合@babel/runtime進行使用。記得先安裝一波,然后在插件選項中加入 @babel/plugin-transform-runtime 這個插件,然后我們來看看編譯后的效果:
/* study.js */
class Test {}
/* study-compiled.js */
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
var Test = function Test() {
_classCallCheck(this, Test);
};
當然如果我們只是為了減少編譯出來的文件中代碼量而使用這個插件的話就太小看他了,而且也沒有必要。
@babel/plugin-transform-runtime還有一個最重要的作用:比如說像上面我們說的Promise就需要提供相應的polyfill去解決,這樣做會有一個副作用,就是會污染全局變量。如果我們只是在一個業務項目這樣搞還好,也沒別人要用到。但是如果我們是在維護一個公共的東西,比如公共組件庫,我們這樣搞,你的一些polyfill可能會把一些全局的api給改掉,副作用就會很明顯,別人用你的組件庫的時候就可能會出問題。@babel/plugin-transform-runtime插件為我們提供了一個配置項corejs,他可以給這些polyfill提供一個沙箱環境,這樣就不會污染到全局變量,無副作用你說美不美。
記得安裝 @babel/runtime-corejs2 這個包(穩定版用2就可以),注意如果不配置的話,是不會提供沙箱環境的。然后在 @babel/plugin-transform-runtime 插件配置corejs:
/* babel.config.js */
module.exports = {
presets: [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "usage",
"corejs": "3",
'targets': {
'browsers': ["ie >= 8", "iOS 7"] // 支持ie8,直接使用iOS瀏覽器版本7
}
}
]
],
plugins: [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2
}
]
]
}
我們來看下編譯后的效果:
/* study.js */
new Promise(() => {})
class Test {}
/* study-compiled.js */
import _classCallCheck from "@babel/runtime-corejs2/helpers/classCallCheck";
import _Promise from "@babel/runtime-corejs2/core-js/promise";
new _Promise(function () {});
var Test = function Test() {
_classCallCheck(this, Test);
};
小節
- 在你修改了babel配置項之后一定要記得重啟編譯命令,否則不會生效
- 維護公共組件庫或者一些別的公共庫推薦要使用@babel/runtime配合@babel/plugin-transform-runtime來建立沙箱環境
接下來本人會去繼續研究babel是如何解析編譯的,target:理解babel如何解析編譯,能夠手寫一個babel插件出來。最近需求比較多,下一篇估計得等到Q2了。。。
寫在最后:
最近也是疫情期間,大家一定要記得盡量少出門,出門必帶口罩。像白衣天使們致敬!