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