咱們知道,各個瀏覽器對 JavaScript 版本的支持度各不相同,有不少優秀的新語法都不能直接在瀏覽器中運行。爲了解決這個「溝通不順暢」的問題,因此就有了 Babel。Babel是一個工具集,主要用於將ES6版本的JavaScript代碼轉爲ES5等向後兼容的JS代碼,從而能夠運行在低版本瀏覽器或其它環境中。所以,在編碼過程當中,可使用ES6/7/8編碼,而後使用Babel將代碼編譯爲向後兼容的Javascript代碼,這樣就不用擔憂所在環境是否支持了。javascript
簡單來講,Babel的工做就是:css
Babel的原理很簡單,首先是將源碼轉成抽象語法樹(AST)
,而後對語法樹進行處理生成新的預發樹,最後將新語法樹生成新的Javascript代碼,整個編譯過程分爲parsing(解析)、transforming(轉換)、generating(生成)。Babel只負責編譯新標準引入的語法,好比 Arrow function、Class、ES Module 等,它不會編譯原生對象新引入的方法和 API,好比 Array.includes,Map,Set 等,這些須要經過 Polyfill 來解決,文章後面會提到。html
Babel7的npm包都是放到Babel域下, 例如@babel/cli、@babel/core等,在Babel6中,安裝的包名是babel-core、babel-cli。本文將以Babel7爲例進行講解。前端
一、@babel/clijava
@babel/cli 是 Babel 提供的內建命令行工具。node
安裝:npm install i -S @babel/cli
react
二、@babel/corewebpack
@babel/core是咱們使用Bable進行轉碼的核心npm包,咱們使用的babel-cli、babel-node都依賴這個包。在命令行和webpack進行轉碼的時候都是經過Node來調用@babel/core相關功能API來進行的。git
安裝: npm install --save-dev @babel/core
es6
一般,咱們須要指定Babel的編譯規則來編譯代碼。Babel的配置文件默認會在當前目錄尋找文件,有:.babelrc
、.babelrc.js
、babel.config.js
、package.json
,它們的配置項都是同樣的,做用也同樣,只須要選擇一種便可。
一、.babelrc
配置:
{
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
複製代碼
二、babel.config.js
和.babelrc.js
配置:
module.exports = {
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
複製代碼
三、package.json
配置:
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"babel": {
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
}
複製代碼
對比分析不一樣格式的Babel配置文件,總結起來的配置項都是plugins(插件)和presets(預設)兩個數組(固然還有其餘的,咱們先只關注這兩個)。
插件是用來定義如何轉換你的代碼的,通常是單獨的某個新特性。當在Babel的配置項中填寫了須要使用的插件,Babel編譯的時候就會去加載node_modules中對應的npm包,而後編譯插件的對應的語法。
Class
語法for...of
語法...Babel支持的插件很是多,若是每個新特性轉換都須要經過安裝插件來解決,那麼咱們的開發效率會變得很是低效,Babel配置文件就會變得很是臃腫。因而,Babel推出了懶人包presets
。
預設就是一堆插件包的集合,例如babel-preset-es2015就是全部處理es2015的二十多個Babel插件的集合。這樣咱們就不須要寫一大堆插件的配置項了,使用一個預設便可。
經常使用的preset包有:
解釋下,stage-x,這裏麪包含的都是當年最新規範的草案,每一年更新。這裏面還細分爲:
stage 0
- 設想: 只是一個想法,可能有 Babel 插件,stage-0 的功能範圍最廣大,包含 stage-1 , stage-2 以及 stage-3 的全部功能。stage 1
- 提案: 初步嘗試,值得跟進。stage 2
- 初稿: 完成初步規範。stage 3
- 候選: 完成規範和瀏覽器初步實現。stage 4
- 完成: 將被添加到下一年度發佈。全部的預設也都須要安裝npm包到node_modules中才可使用。
plugins插件數組和presets預設數組是有順序要求的。若是兩個插件或預設都要處理同一個代碼片斷,那麼會根據插件和預設的順序來執行。規則以下:
一、插件plugins的執行順序是從左到右執行的。
{
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
複製代碼
在上面的示例中,Babel 在進行 AST 遍歷的時候會先調用 transform-decorators-legacy 插件中定義的轉換方法,而後再調用 transform-class-properties 中的方法。
二、預設presets的執行順序是從右到左執行的。
{
"presets": [
"a",
"b",
"c"
]
}
複製代碼
它的執行順序是 c、b、a,是否是有點奇怪,這主要是爲了確保向後兼容,由於大多數用戶將 "es2015" 放在 "stage-0" 以前。
三、插件plugins在預設presets以前執行
在Babel6的時代,常見的preset有babel-preset-es2015
、babel-preset-es2016
、babel-preset-es2017
、babel-preset-latest
、babel-preset-stage-0
、babel-preset-stage-1
、babel-preset-stage-2
等。
目前,Babel官方再也不推出babel-preset-es2017
之後的年代preset了。
@babel/preset-env
包含了babel-preset-latest
的功能,並對其進行加強,如今@babel/preset-env
徹底能夠替代babel-preset-latest
。
在默認狀況下,@babel/preset-env
只會編譯Javascript的語法,不會對新方法和新的原生對象進行轉譯。好比:
var fn = (num) => num + 2;
const arr = [1,2,3]
console.log(arr.includes(1))
複製代碼
轉譯後:
"use strict";
var fn = function fn(num) {
return num + 2;
};
var arr = [1, 2, 3];
console.log(arr.includes(1));
複製代碼
能夠發現,箭頭函數被轉換了,可是Array.includes方法沒有被處理,若是這個時候程序運行在低版本的瀏覽器上,就會出現includes is not function
的錯誤。這個時候就須要polyfill了。
polyfill廣義上講是爲環境提供不支持的特性的一類文件或庫,既有Babel官方的庫,也有第三方的。
@babel/polyfill
本質上是由兩個npm包core-js與regenerator-runtime組合而成的,因此在使用層面上還能夠再細分爲是引入@babel/polyfill
自己仍是其組合子包。使用方式有如下幾種:
<script src="https://cdn.bootcss.com/babel-polyfill/7.6.0/polyfill.js"></script>
複製代碼
import './polyfill.js';
var promise = Promise.resolve('ok');
console.log(promise);
複製代碼
1)安裝@babel/polyfill
2)修改a.js
內容
import '@babel/polyfill';
var promise = Promise.resolve('ok');
console.log(promise);
複製代碼
1)安裝core-js和regenerator-runtime
npm install --save core-js regenerator-runtime
複製代碼
2)修改a.js
內容
import "core-js/stable";
import "regenerator-runtime/runtime";
var promise = Promise.resolve('ok');
console.log(promise);
複製代碼
const path = require('path');
module.exports = {
entry: ['./polyfill.js', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
複製代碼
const path = require('path');
module.exports = {
entry: ['@babel/polyfill', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
複製代碼
const path = require('path');
module.exports = {
entry: ['core-js/stable', 'regenerator-runtime/runtime', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
複製代碼
轉譯結果:
"use strict";
eval("\nvar toObject = __webpack_require__(/*! ../internals/to-object */ \"./node_modules/core-js/internals/to-object.js\");\nvar toAbsoluteIndex = __webpack_require__(/*! ../internals/to-absolute-index */ \"./node_modules/core-js/internals/to-absolute-index.js\");\nvar toLength = __webpack_require__(/*! ../internals/to-length */ \"./node_modules/core-js/internals/to-length.js\");\n\n// `Array.prototype.fill` method implementation\n// https://tc39.es/ecma262/#sec-array.prototype.fill\nmodule.exports = function fill(value /* , start = 0, end = @length */) {\n var O = toObject(this);\n var length = toLength(O.length);\n var argumentsLength = arguments.length;\n var index = toAbsoluteIndex(argumentsLength > 1 ? arguments[1] : undefined, length);\n var end = argumentsLength > 2 ? arguments[2] : undefined;\n var endPos = end === undefined ? length : toAbsoluteIndex(end, length);\n while (endPos > index) O[index++] = value;\n return O;\n};\n\n\n//# sourceURL=webpack:///./node_modules/core-js/internals/array-fill.js?");
/***/ }),
...
複製代碼
這麼多的方法,在實際開發中該選擇哪種呢?從babel7.4版本開始,Babel官方已經不推薦再使用@babel/polyfill
,包括官方的polyfill.js庫文件。所以從2019年中開始,咱們的新項目都應該使用core-js
和regenerator-runtime
這兩個包。也就是說咱們應選擇方法4與方法7。
可是,@babel/polyfill
主要有兩個缺點:
@babel/polyfill
把兩個npm包所有都引入到了咱們的前端打包後的文件裏了,致使打包後的體積過大。@babel-polyfill
可能會污染全局變量,給不少類的原型鏈上都做了修改,這就有不可控的因素存在。@babel/preset-env
的參數項,數量有10多個,但大部分咱們要麼用不到,要麼已經或將要棄用。這裏建議你們掌握重點的幾個參數項,有的放矢。重點要學習的參數項有targets、useBuiltIns、modules和corejs這四個,能掌握這幾個參數項的真正含義。
該參數項能夠取值爲字符串、字符串數組或對象,不設置的時候取默認值空對象{}。
module.exports = {
presets: [["@babel/env", {
targets: {
"chrome": "58",
"ie": "11"
}
}]],
plugins: []
}
複製代碼
若是咱們對@babel/preset-env的targets參數項進行了設置,那麼就不使用browserslist的配置,而是使用targets的配置。如不設置targets,那麼就使用browserslist的配置。若是targets不配置,browserslist也沒有配置,那麼@babel/preset-env就對全部ES6語法轉換成ES5的。
useBuiltIns項取值能夠是"usage" 、 "entry" 或 false。默認值爲false。
咱們來看一個例子:
1)安裝npm包
npm install --save-dev @babel/cli @babel/core @babel/preset-env
npm install --save @babel/polyfill
複製代碼
2)修改Babel配置文件
module.exports = {
presets: [["@babel/env", {
useBuiltIns: "entry"
}]],
plugins: []
}
複製代碼
3)修改package.json
的browserslist
"browserslist": [
"firefox 58"
]
複製代碼
4)修改入口文件a.js
import '@babel/polyfill';
var promise = Promise.resolve('ok');
console.log(promise);
複製代碼
5)執行命令:npx babel a.js -o b.js
。轉碼後的結果:
"use strict";
require("core-js/modules/es7.array.flat-map");
require("core-js/modules/es7.string.trim-left");
require("core-js/modules/es7.string.trim-right");
require("core-js/modules/web.timers");
require("core-js/modules/web.immediate");
require("core-js/modules/web.dom.iterable");
var promise = Promise.resolve('ok');
console.log(promise);
複製代碼
Babel轉碼後針對火狐58不支持的特性引入了6個core-js的API補齊模塊,因爲火狐58已經支持了Promise屬性,因此沒有引入Promise API相關的補齊特性。
6)修改Babel配置文件: useBuiltIns: "usage"
,去掉a.js
中的import '@babel/polyfill';
,運行npx babel a.js -o b.js
,查看轉碼後的結果:
"use strict";
require("core-js/modules/es6.object.to-string.js");
require("core-js/modules/es6.promise.js");
var promise = Promise.resolve('ok');
console.log(promise);
複製代碼
總結:
useBuiltIns: "usage"
: 不須要額外配置 @babel/polyfill
,也不須要事先引入,@babel/polyfill
會自動安裝 @babel/polyfill
;useBuiltIns: "entry"
: 不須要額外配置 @babel/polyfill
,但須要在文件入口引入 @babel/polyfill
,使用 require
或者 import
;useBuiltIns: false
: 不在每個文件自動添加語法填充,須要額外在配置文件加入 @babel/polyfill
配置。該參數項的取值能夠是2或3,沒有設置的時候。默認值爲2。這個參數項只有useBuiltIns設置爲'usage'或'entry'時,纔會生效。
須要注意的是,corejs取值爲2的時候,須要安裝並引入core-js@2版本,或者直接安裝並引入polyfill也能夠。若是corejs取值爲3,必須安裝並引入core-js@3版本才能夠。
這個參數項的取值能夠是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不設置的時候,取默認值"auto"。
該項用來設置是否把ES6的模塊化語法改爲其它模塊化語法。
自動移除語法轉換後內聯的輔助函數(inline Babel helpers),使用@babel/runtime/helpers裏的輔助函數來替代。
@babel/preset-env作語法轉換作語法轉換:
{
"presets": [
"@babel/env"
],
"plugins": [
]
}
複製代碼
須要轉換的代碼爲:
class Person {
sayname() {
return 'name'
}
}
var john = new Person()
console.log(john)
複製代碼
Babel轉碼後的內容爲:
"use strict";
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 Person = /*#__PURE__*/function () {
function Person() {
_classCallCheck(this, Person);
}
_createClass(Person, [{
key: "sayname",
value: function sayname() {
return 'name';
}
}]);
return Person;
}();
var john = new Person();
console.log(john);
複製代碼
能夠看到轉換後的代碼上面增長了好幾個函數聲明,這就是注入的函數,咱們稱之爲輔助函數。@babel/preset-env在作語法轉換的時候,注入了這些函數聲明,以便語法轉換後使用。
但樣這作存在一個問題。在咱們正常的前端工程開發的時候,少則幾十個js文件,多則上千個。若是每一個文件裏都使用了class類語法,那會致使每一個轉換後的文件上部都會注入這些相同的函數聲明。這會致使咱們用構建工具打包出來的包很是大。
那麼怎麼辦?一個思路就是,咱們把這些函數聲明都放在一個npm包裏,須要使用的時候直接從這個包裏引入到咱們的文件裏。這樣即便上千個文件,也會從相同的包裏引用這些函數。經過webpack這一類的構建工具打包的時候,咱們只會把使用到的npm包裏的函數引入一次,這樣就作到了複用,減小了體積。
藉助@babel/plugin-transform-runtime
插件來幫助咱們解決這個問題。
module.exports = {
presets: ["@babel/env"],
plugins: ["@babel/plugin-transform-runtime"]
}
複製代碼
1)安裝npm包:
npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/plugin-transform-runtime
複製代碼
2)修改Babel配置文件
{
"presets": [
"@babel/env"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
複製代碼
3)執行npx babel a.js -o b.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 Person = /*#__PURE__*/function () {
function Person() {
(0, _classCallCheck2["default"])(this, Person);
}
(0, _createClass2["default"])(Person, [{
key: "sayname",
value: function sayname() {
return 'name';
}
}]);
return Person;
}();
var john = new Person();
console.log(john);
複製代碼
當代碼裏使用了core-js的API,自動引入@babel/runtime-corejs3/core-js-stable/,以此來替代全局引入的core-js/stable。
1)安裝npm包:
npm install --save @babel/runtime-corejs3
npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/plugin-transform-runtime
複製代碼
2)修改babel配置:
{
"presets": [
"@babel/env"
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
複製代碼
3)修改a.js
內容
var obj = Promise.resolve();
複製代碼
4)執行npx babel a.js -o b.js
命令
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var obj = _promise["default"].resolve();
複製代碼
當代碼裏使用了Generator/async函數,對Generator/async進行API轉換功能,默認是開啓的,不須要咱們設置。
歡迎你們關注個人公衆號 -- 《前端Talkking》 😄
若是還有什麼疑問或者建議,能夠多多交流,原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。