Babel
是一個 JavaScript
編譯器。
Babel
是一個工具鏈,主要用于將ECMAScript 2015+
版本的代碼轉(zhuǎn)換為向后兼容的JavaScript
語法,以便能夠運行在當前和舊版本的瀏覽器或其他環(huán)境中。
通俗地講,Babel
只是轉(zhuǎn)義新標準引入的語法,如ES6
中的箭頭函數(shù)、解構(gòu)等。而新標準中新增的方法、函數(shù)等就需要通過Polyfill
在目標環(huán)境中添加這些缺失的特性來解決。
Babel
的功能:
- 語法轉(zhuǎn)換
- 通過
Polyfill
方式在目標環(huán)境中添加缺失的特性,對應模塊@babel/polyfill
- 源碼轉(zhuǎn)換
Babel` 編譯過程:
- 解析:將代碼字符串解析成抽象語法樹。
- 轉(zhuǎn)換:對抽象語法樹進行轉(zhuǎn)換操作。(本文重點)
- 輸出:根據(jù)變換后的抽象語法樹再生成代碼字符串。
常見使用方式:
-
.babelrc/babel.config.json
(Babel
配置文件) -
babel-loader
(webpack/rollup
等)
初始化項目
- 創(chuàng)建一個空目錄,執(zhí)行
npm init -y
- 新建
src/index.js
const fn = () => { };
- 安裝必要依賴:
@babel/core、@babel/cli
-
@babel/core
核心庫,包含了所有的核心API
-
@babel/cli
提供的CLI
命令行工具,主要是babel
命令,適合安裝在項目中 -
@babel/node
提供了babel-node
命令,更適合全局安裝
npm i -D @babel/core @babel/cli
-
- 在
package.json
中配置scripts
,添加babel
的編譯命令
執(zhí)行編譯:"scripts": { "compiler": "babel src --out-dir lib --watch" }
npm run compiler
,生成lib/index.js
。當前沒有配置任何插件,所以編譯前后的代碼完全一樣。
插件
雖然Babel
是開箱即用的,但如果沒有為其添加任何插件,那么它什么也不會做。
Babel
構(gòu)建在插件之上,使用現(xiàn)有的或自己編寫的插件可以組成一個轉(zhuǎn)換通道,Babel
的插件分為兩種:語法插件、轉(zhuǎn)換插件
-
語法插件
只允許Babel
解析(不是轉(zhuǎn)換)特定類型的語法,可以在AST
轉(zhuǎn)換時使用,以支持解析新語法// for Example import * as babel from "@babel/core"; const code = babel.transformFromAstSync(ast, { //支持可選鏈 plugins: ["@babel/plugin-proposal-optional-chaining"], babelrc: false }).code;
-
轉(zhuǎn)換插件
會啟用相應的語法插件(因此不需要同時指定這兩種插件),先解析,后轉(zhuǎn)換!
在項目根目錄下創(chuàng)建配置文件.babelrc
,安裝并配置插件
// 一個編譯箭頭函數(shù)的babel插件
npm i @babel/plugin-transform-arrow-functions -D
//.babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
# 也可以指定插件的絕對/相對路徑
"plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"]
重新編譯的結(jié)果:
// lib/index.js
"use strict";
const fn = function () { };
OK! 現(xiàn)在可以轉(zhuǎn)換箭頭函數(shù)了,如果繼續(xù)轉(zhuǎn)化其他的JS新特性,還需要繼續(xù)添加插件,一個個配置的話必然很繁瑣!
為此,Babel
提供了預設。
預設
通過使用或創(chuàng)建一個 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
是一個靈活的預設,你不需要管理目標環(huán)境需要的語法轉(zhuǎn)換或瀏覽器polyfills
,就可以使用最新的JavaScript
,同時也會讓JavaScript
打包后的文件更小。
總之,主要作用是:轉(zhuǎn)換目標瀏覽器中缺失的JavaScript
新語法,加載Polyfills
在不做任何配置的情況下,@babel/preset-env
所包含的插件支持所有最新的JS特性(ES2015、ES2016
等,不包含 stage
階段),并將其轉(zhuǎn)換為ES5
代碼。
但如果代碼中包含了可選鏈(目前仍在stage
階段),還需要安裝相應的插件。
安裝與配置.babelrc
npm i @babel/preset-env -D
// .babelrc
{
"presets": ["@babel/preset-env"]
}
Browserslist 集成
@babel/preset-env
會根據(jù)配置的目標環(huán)境,生成插件列表來編譯。對基于瀏覽器或Electron
的項目,官方推薦使用.browserslistrc
文件來指定目標環(huán)境。默認情況下,如果沒有在.babelrc
中設置targets
或ignoreBrowserslistConfig
,@babel/preset-env
會使用browserslist
配置源。
比如,僅包括瀏覽器市場份額超過0.25%
的用戶所需的 polyfill
和代碼轉(zhuǎn)換(忽略沒有安全更新的瀏覽器,如IE10
和BlackBerry
)
三種配置方式:
-
.babelrc:@babel/preset-env
的targets
可以設置支持哪些平臺、哪些版本等等目標信息{ "presets": [ ["@babel/preset-env", { "targets": { "browsers": "> 0.25%, not dead", "node": "xxx", ... } }] ] }
- 在項目跟目下創(chuàng)建
browserslist
配置文件.browserslistrc
> 0.25% not dead
-
package.json
:官方建議這種方式"browserslist": [ "> 0.25%", "not dead" ]
如果不是要兼容所有的瀏覽器和環(huán)境,推薦指定目標環(huán)境,這樣在編譯代碼能保持最小!
再比如,.browserslistrc
配置為:
last 2 Chrome versions
編譯后發(fā)現(xiàn)代碼并沒有轉(zhuǎn)換,這是因為Chrome
瀏覽器最新的兩個版本都支持箭頭函數(shù)。
更多 browserslist
配置:browserslist 配置
Polyfill
修改一下src/index.js
const isHas = [1,2,3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});
編譯后發(fā)現(xiàn)代碼并沒有轉(zhuǎn)換!--
因為@babel/preset-env
轉(zhuǎn)換的是語法,但新的內(nèi)置函數(shù)、方法無法轉(zhuǎn)換。
這時,就需要 polyfill
登場了!
polyfill
的譯文是墊片,顧名思義,就是墊平不同瀏覽器或不同環(huán)境下的差異,讓新的內(nèi)置函數(shù)、方法在低版本瀏覽器中也可以使用。
core-js/stable
(Polyfill ECMAScript
特性)和 regenerator-runtime/runtime
(需要使用轉(zhuǎn)換后的generator
函數(shù))模塊,可以模擬完整的ES2015+
環(huán)境(不包含第4階段前的提議)。
這就意味著可以使用:新的內(nèi)置函數(shù)如
Promise、WeakMap
,新的靜態(tài)方法如Array.from、Object.assign
,新的實例方法如Array.prototype.includes
,以及generator
函數(shù)(前提是使用@babel/plugin-transform-regenerator
插件)。
但為了添加這些功能,polyfill
將添加到全局范圍和類似String
這樣的內(nèi)置原型中(如你所想,會污染全局環(huán)境,后面會講避免全局污染的方法)。
注意:Babel v7.4.0
之前,@babel/polyfill
包含core-js(默認v2)
和regenerator-runtime
兩個模塊;但從Babel v7.4.0
開始,@babel/polyfill
被廢棄了,支持直接安裝并導入core-js(默認v3)
和regenerator-runtime
core-js
是能夠使用新API
的最重要的包,與Babel
高度集成,core-js@3
廢棄了@babel/polyfill
,實現(xiàn)了完全無污染的API
轉(zhuǎn)譯。
// Babel v7.4.0 之前
npm i -S @babel/polyfill // 不使用 -D 是因為這是一個需要在源碼之前運行的墊片
// Babel v7.4.0
npm i core-js regenerator-runtime -S
# core-js: ^3.6.4:提供 es 新的特性
# regenerator-runtime: ^0.14.4:代碼中用到generator、async函數(shù)的話,提供對 generator 支持
兩種使用方式:
-
src/index.js
的首部// Babel v7.4.0 之前 require("@babel/polyfill"); or import "@babel/polyfill"; // Babel v7.4.0 import "core-js/stable"; import "regenerator-runtime/runtime";
- 在
webpack
里配置入口:entry: ["@babel/polyfill", "./src/index.js"],
現(xiàn)在的代碼不管在低版本還是高版本瀏覽器,甚至Node
環(huán)境中都能正常運行了!
按需引入
然而,很多時候都不需要完整的引入
Polyfill
,這樣會增大構(gòu)建包的體積。
Babel
在@babel/preset-env
中提供了參數(shù)useBuiltIns
,設置為usage
時就只會包含代碼中需要的polyfill
。另外還必須同時配置corejs
。@babel/polyfill
默認安裝core-js v2
,而core-js v2
分支中已經(jīng)不再添加新特性,為了可以使用更多新特性,請安裝core-js v3
useBuiltIns
的可選值
-
false
不對Polyfill
做操作,引入所有的Polyfill
; -
usage
根據(jù)配置的瀏覽器兼容性,以及代碼中使用到的API
來進行Polyfill
,實現(xiàn)按需加載; -
entry
根據(jù)browserslist
的配置,引入目標環(huán)境不兼容的polyfill
,還需要在入口文件中手動添加import "@babel/polyfill"
還有一個常用的參數(shù) modules
,可以取值 amd, umd, systemjs, commonjs, false
,這可以讓Babel
以特定的模塊化格式來輸出代碼。false
表示不進行模塊化處理。
// Babel v7.4.0 之前還需要額外安裝core-js v3
npm install -S core-js@3
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3 // 默認使用的時corejs@2,因此必須設置corejs@3
}]
]
}
編譯時,Babel
會檢查所有代碼,查找在目標環(huán)境中缺失的功能,然后僅僅把需要的polyfill
包含進來。
// 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
模式構(gòu)建,包體積要比引入整個@babel/polyfill
小很多了。
提取輔助函數(shù)
Polyfill
雖然解決了Babel
不轉(zhuǎn)換非語法的新API
問題,但會使用很小的輔助函數(shù)來實現(xiàn)類似_classCallCheck、_createClass
等公共方法。默認情況下,這些輔助函數(shù)將被inject(添加)
到需要它的每個文件中。
修改src/index.js
class Point { }
編譯之后:
// lib/index
"use strict";
function _classCallCheck ...
var Point = function Point() ...
試想一下,100個文件都是用了class
,那就意味著諸如_classCallCheck
之類的輔助函數(shù)被inject
了100次,導致編譯后的bundle(包)
體積變大!
為此,Babel
提供了單獨的模塊@babel/runtime
,用于提供編譯模塊的輔助函數(shù)。啟用@babel/plugin-transform-runtime
插件后,Babel
就會使用@babel/runtime
中的工具函數(shù),以 閉包 的形式注入。
npm i -D @babel/plugin-transform-runtime #僅編譯時使用
npm i -S @babel/runtime #編譯和運動時都需要
在 .babelrc
中配置 @babel/plugin-transform-runtime
插件
{
// "presets": [
// ["@babel/preset-env", {
// "useBuiltIns": "usage",
// "corejs": 3
// }]
// ],
"plugins": [
["@babel/plugin-transform-runtime", { corejs: 3 }]
]
}
編譯之后:
// lib/index
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var Point = function Point() {
(0, _classCallCheck2.default)(this, Point);
};
可以看出,幫助函數(shù)已經(jīng)不是直接被 inject
到代碼中,而是從@babel/runtime
中引入的。
全局污染問題
@babel/plugin-transform-runtime
:構(gòu)建過程的代碼轉(zhuǎn)換,是一個可以重復使用Babel
注入的幫助程序,以節(jié)省代碼大小的插件。
除此之外,該轉(zhuǎn)換器還有另外一個作用:為我們的代碼創(chuàng)建一個沙盒環(huán)境。
如果直接使用
core-js
或@babel/polyfill
,它們所提供的諸如內(nèi)置程序Promise、Set、Map
等,將會污染全局環(huán)境。雖然這對于應用程序或命令行工具可能是可以的,但如果代碼是要發(fā)布供他人使用的庫,或者無法完全控制代碼運行環(huán)境,那么這種污染將成為一個問題。
@babel/plugin-transform-runtime
會將這些內(nèi)置別名作為core-js
的別名,因此可以無縫使用它們,無需使用polyfill
(官網(wǎng))
- 安裝依賴:
npm i @babel/runtime-corejs3 -S
- 配置
.babelrc
{ "presets": ["@babel/preset-env"], "plugins": [ ["@babel/plugin-transform-runtime", { "corejs": 3 }] ] }
- 修改
src/index.js
,加入include()、Promise
let isHas = [1,2,3].includes(2); Array.from([1, 2, 3], x => x + x); // [2, 4, 6] let promsie = new Promise(); async function fn() { return 1 }
重新編譯可知,@babel/plugin-transform-runtime
通過導入模塊的方式引入所需功能,直接去修改Array.prototype
或 新增Promise
函數(shù),避免了全局環(huán)境的污染。
搭配
webpack
時,如果使用commonJs
編寫模塊,且使用了@babel/plugin-transform-runtime
,則應該安裝@babel/plugin-transform-modules-commonjs
,webpack
需要利用它處理ES6Module
。
更多插件/預設的補充知識
-
插件的排列順序很重要!!!
如果兩個轉(zhuǎn)換插件都將處理程序的某個代碼片段,則將根據(jù)轉(zhuǎn)換插件或preset
的排列順序依次執(zhí)行。-
Plugin
在Presets
前運行 - 插件順序從前往后排列
-
Presets
順序是從后往前的
先執(zhí)行{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
@babel/preset-react
, 后執(zhí)行@babel/preset-env
-
- 插件參數(shù)
插件和preset
都可以接受參數(shù),參數(shù)由插件名和參數(shù)對象組成一個數(shù)組"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
,該規(guī)則同樣適用于帶有scope
的插件"plugins": [ "newPlugin", // babel-plugin-newPlugin "@scp/myPlugin" // @scp/babel-plugin-myPlugin ]
- 如果插件名稱為
- 創(chuàng)建自己的
Preset
- 可以簡單的返回一個插件數(shù)組
module.exports = function() { return { plugins: ["A", "B", "C"] } }
-
preset
中也可以包含其他的preset
,以及帶有參數(shù)的插件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") ] } }
- 可以簡單的返回一個插件數(shù)組
配置文件
Babel
支持多種格式的配置文件,根據(jù)使用場景可以選擇不同的配置文件。
所有的Babel API
參數(shù)都可以配置,但如果該參數(shù)需要使用JS
代碼,那可能需要使用JS
代碼版的配置文件。
- 如果希望以編程的方式創(chuàng)建配置文件或編譯
node_modules
目錄下的模塊,那么babel.config.js
可以滿足需求; - 如果只需要一個簡單的且只用于單個軟件包的配置,那可以選擇
.babelrc
-
babel.config.js
在項目根目錄下創(chuàng)建babel.config.js
文件,配置文檔:babel.config.jsmodule.exports = function(api) { api.cache(true); const presets = [...]; const plugins = [...]; return { presets, plugins }; }
-
.babelrc
在項目根目錄下創(chuàng)建.babelrc
文件,配置文檔:.babelrc{ "presets": [], "plugins": [] }
-
package.json
可以將.babelrc
中的配置信息作為babel 鍵(key)
添加到package.json
文件中:{ "name": "my-package", "babel": { "presets": [], "plugins": [] } }
-
.babelrc.js
與.babelrc
配置相同,但是支持使用JS
編寫。//可以在其中調(diào)用 Node.js 的API const presets = []; const plugins = []; module.exports = { presets, plugins };
使用Webpack構(gòu)建
- 安裝依賴:
npm install -D webpack-cli webpack babel-loader clean-webpack-plugin
- 在根目錄下創(chuàng)建配置文件
webpack.config.js
const path = require('path') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: 'production', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[hash].js' }, module: { rules: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ //排除 node_modules 目錄 } ] }, plugins: [ new CleanWebpackPlugin() //清理 output 指定的目錄 ] }
-
package.json
"scripts": { "build": "webpack }
- 構(gòu)建:
npm run build