一步一步帶你認識Babel7

1. 什麼是Babel

咱們知道,各個瀏覽器對 JavaScript 版本的支持度各不相同,有不少優秀的新語法都不能直接在瀏覽器中運行。爲了解決這個「溝通不順暢」的問題,因此就有了 Babel。Babel是一個工具集,主要用於將ES6版本的JavaScript代碼轉爲ES5等向後兼容的JS代碼,從而能夠運行在低版本瀏覽器或其它環境中。所以,在編碼過程當中,可使用ES6/7/8編碼,而後使用Babel將代碼編譯爲向後兼容的Javascript代碼,這樣就不用擔憂所在環境是否支持了。javascript

簡單來講,Babel的工做就是:css

  • 語法轉換
  • 經過 Polyfill 的方式在目標環境中添加缺失的特性
  • Javascript源碼轉換

2. Babel的基本原理

Babel的原理很簡單,首先是將源碼轉成抽象語法樹(AST),而後對語法樹進行處理生成新的預發樹,最後將新語法樹生成新的Javascript代碼,整個編譯過程分爲parsing(解析)、transforming(轉換)、generating(生成)。Babel只負責編譯新標準引入的語法,好比 Arrow function、Class、ES Module 等,它不會編譯原生對象新引入的方法和 API,好比 Array.includes,Map,Set 等,這些須要經過 Polyfill 來解決,文章後面會提到。html

Xnip2021-02-24_11-29-09

Babel7的npm包都是放到Babel域下, 例如@babel/cli、@babel/core等,在Babel6中,安裝的包名是babel-core、babel-cli。本文將以Babel7爲例進行講解。前端

3. Babel的使用

3.1 Babel運行所需的基本環境

一、@babel/clijava

@babel/cli 是 Babel 提供的內建命令行工具。node

安裝:npm install i -S @babel/clireact

二、@babel/corewebpack

@babel/core是咱們使用Bable進行轉碼的核心npm包,咱們使用的babel-cli、babel-node都依賴這個包。在命令行和webpack進行轉碼的時候都是經過Node來調用@babel/core相關功能API來進行的。git

安裝: npm install --save-dev @babel/corees6

3.2 配置文件

一般,咱們須要指定Babel的編譯規則來編譯代碼。Babel的配置文件默認會在當前目錄尋找文件,有:.babelrc.babelrc.jsbabel.config.jspackage.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(預設)兩個數組(固然還有其餘的,咱們先只關注這兩個)。

4. 插件plugins和預設presets

4.1 插件plugins

插件是用來定義如何轉換你的代碼的,通常是單獨的某個新特性。當在Babel的配置項中填寫了須要使用的插件,Babel編譯的時候就會去加載node_modules中對應的npm包,而後編譯插件的對應的語法。

  • @babel/plugin-transform-arrow-functions: 轉換箭頭函數的語法
  • @babel/plugin-transform-classes: 使用 Class 語法
  • @babel/plugin-transform-for-of: 使用 for...of 語法...

Babel支持的插件很是多,若是每個新特性轉換都須要經過安裝插件來解決,那麼咱們的開發效率會變得很是低效,Babel配置文件就會變得很是臃腫。因而,Babel推出了懶人包presets

4.2 預設presets

預設就是一堆插件包的集合,例如babel-preset-es2015就是全部處理es2015的二十多個Babel插件的集合。這樣咱們就不須要寫一大堆插件的配置項了,使用一個預設便可。

經常使用的preset包有:

  • @babel/preset-env
  • @babel/preset-react
  • @babel/preset-typescript
  • @babel/preset-stage-0
  • @babel/preset-stage-1...

解釋下,stage-x,這裏麪包含的都是當年最新規範的草案,每一年更新。這裏面還細分爲:

  • stage 0 - 設想: 只是一個想法,可能有 Babel 插件,stage-0 的功能範圍最廣大,包含 stage-1 , stage-2 以及 stage-3 的全部功能。
  • stage 1 - 提案: 初步嘗試,值得跟進。
  • stage 2 - 初稿: 完成初步規範。
  • stage 3 - 候選: 完成規範和瀏覽器初步實現。
  • stage 4 - 完成: 將被添加到下一年度發佈。

全部的預設也都須要安裝npm包到node_modules中才可使用。

4.3 執行順序

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以前執行

4.4 @babel/preset-env一統江湖

在Babel6的時代,常見的preset有babel-preset-es2015babel-preset-es2016babel-preset-es2017babel-preset-latestbabel-preset-stage-0babel-preset-stage-1babel-preset-stage-2等。

目前,Babel官方再也不推出babel-preset-es2017之後的年代preset了。

@babel/preset-env包含了babel-preset-latest的功能,並對其進行加強,如今@babel/preset-env徹底能夠替代babel-preset-latest

5. @babel/polyfill

在默認狀況下,@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自己仍是其組合子包。使用方式有如下幾種:

1. 在html文件中引入polyfill文件

<script src="https://cdn.bootcss.com/babel-polyfill/7.6.0/polyfill.js"></script>
複製代碼

2. 在前端工程的入口文件裏引入polyfill.js

import './polyfill.js';

var promise = Promise.resolve('ok');
console.log(promise);
複製代碼

3. 在前端工程的入口文件裏引入@babel/polyfill

1)安裝@babel/polyfill

2)修改a.js內容

import '@babel/polyfill';

var promise = Promise.resolve('ok');
console.log(promise);
複製代碼

4. 在前端工程的入口文件裏引入core-js/stable與regenerator-runtime/runtime

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);
複製代碼

5. 在前端工程構建工具的配置文件入口項引入polyfill.js

const path = require('path');

module.exports = {
  entry: ['./polyfill.js', './a.js'],
  output: {
    filename: 'b.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};
複製代碼

6. 在前端工程構建工具的配置文件入口裏引入@babel/polyfill

const path = require('path');

module.exports = {
  entry: ['@babel/polyfill', './a.js'],
  output: {
    filename: 'b.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};
複製代碼

7. 在前端工程構建工具的配置文件入口裏引入core-js/stable與regenerator-runtime/runtime

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-jsregenerator-runtime這兩個包。也就是說咱們應選擇方法4方法7

可是,@babel/polyfill主要有兩個缺點:

  1. @babel/polyfill把兩個npm包所有都引入到了咱們的前端打包後的文件裏了,致使打包後的體積過大。
  2. @babel-polyfill 可能會污染全局變量,給不少類的原型鏈上都做了修改,這就有不可控的因素存在。

6. @babel/preset-env

@babel/preset-env的參數項,數量有10多個,但大部分咱們要麼用不到,要麼已經或將要棄用。這裏建議你們掌握重點的幾個參數項,有的放矢。重點要學習的參數項有targets、useBuiltIns、modules和corejs這四個,能掌握這幾個參數項的真正含義。

6.1 targets

該參數項能夠取值爲字符串、字符串數組或對象,不設置的時候取默認值空對象{}。

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的。

6.2 useBuiltIns

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 配置。

6.3 corejs

該參數項的取值能夠是2或3,沒有設置的時候。默認值爲2。這個參數項只有useBuiltIns設置爲'usage'或'entry'時,纔會生效。

須要注意的是,corejs取值爲2的時候,須要安裝並引入core-js@2版本,或者直接安裝並引入polyfill也能夠。若是corejs取值爲3,必須安裝並引入core-js@3版本才能夠。

6.4 modules

這個參數項的取值能夠是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不設置的時候,取默認值"auto"。

該項用來設置是否把ES6的模塊化語法改爲其它模塊化語法。

7. @babel/plugin-transform-runtime

7.1 做用1

自動移除語法轉換後內聯的輔助函數(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);

複製代碼

7.2 做用2

當代碼裏使用了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();
複製代碼

7.3 做用3

當代碼裏使用了Generator/async函數,對Generator/async進行API轉換功能,默認是開啓的,不須要咱們設置。

8. 最後

歡迎你們關注個人公衆號 -- 《前端Talkking》 😄

若是還有什麼疑問或者建議,能夠多多交流,原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。

參考資料

  1. Babel7知識梳理

  2. Babel教程

  3. Show me the code,babel 7 最佳實踐

  4. 2020, 再談 Polyfill 最佳實踐

  5. Babel 7 轉碼的正確姿式

  6. 史上最清晰易懂的babel配置解析

  7. Babel7 使用配置詳解

  8. babel工做原理淺析

相關文章
相關標籤/搜索