前端科普系列(4):Babel —— 把 ES6 送上天的通天塔

本文首發於 vivo互聯網技術 微信公衆號 
連接: https://mp.weixin.qq.com/s/plJewhUd0xDXh3Ce4CGpHg
做者:Morrain

1、前言

在上一節 《CommonJS:不是前端卻革命了前端》中,咱們聊到了 ES6 Module,它是 ES6 中對模塊的規範,ES6 是 ECMAScript 6.0 的簡稱,泛指 JavaScript 語言的下一代標準,它的第一個版本 ES2015 已經在 2015 年 6 月正式發佈,本文中提到的 ES6 包括 ES201五、ES201六、ES2017等等。在第一節的《Web:一路前行一路忘川》中也提到過,ES2015 從制定到發佈歷經了十幾年,引入了不少的新特性以及新的機制,瀏覽器對 ES6 的支持進度遠遠趕不上前端開發小哥哥們使用 ES6 的熱情,因而矛盾就日益顯著……html

2、Babel 是什麼

先來看下它在官網上的定義:前端

Babel is a JavaScript compilerwebpack

沒錯就一句話,Babel 是 JavaScript 的編譯器。至於什麼是編譯器,能夠參考the-super-tiny-compiler這個項目,能夠找到很好的答案。git

本文是以 Babel 7.9.0 版本進行演示和講解的,另外建議學習者閱讀英文官網,中文官網會比原版網站慢一個版本,而且不少依然是英文的。

Babel 就是一套解決方案,用來把 ES6 的代碼轉化爲瀏覽器或者其它環境支持的代碼。注意個人用詞哈,我說的不是轉化爲 ES5 ,由於不一樣類型以及不一樣版本的瀏覽器對 ES6 新特性的支持程度都不同,對於瀏覽器已經支持的部分,Babel 能夠不轉化,因此 Babel 會依賴瀏覽器的版本,後面會講到。這裏能夠先參考browerslist項目。es6

Babel 的歷史

在學習任何一門知識前,我都習慣先了解它的歷史,這樣才能深入理解它存在乎義。github

Babel 的做者是 FaceBook 的工程師 Sebastian McKenzie。他在 2014 年發佈了一款 JavaScript 的編譯器 6to5。從名字就能看出來,它主要的做用就是將 ES6 轉化爲 ES5。web

這裏的 ES6 指 ES2015,由於當時尚未正式發佈, ES2015 的名字還未被正式肯定。

因而不少人評價,6to5 只是 ES6 獲得支持前的一個過渡方案,它的做者很是不一樣意這個觀點,認爲 6to5 不光會按照標準逐步完善,依然具有很是大的潛力反過來影響並推動標準的制定。正由於如此 6to5 的團隊以爲 '6to5' 這個名字並無準確的傳達這個項目的目標。加上 ES6 正式發佈後,被命名爲 ES2015,對於 6to5 來講更偏離了它的初衷。因而 2015 年 2 月 15 號,6to5 正式改名爲 Babel。chrome

(圖片來源於網絡)express

Babel 是巴比倫文化裏的通天塔,用來給 6to5 這個項目命名真得太貼切了!羨慕這些牛逼的人,不光代碼寫得好,還這麼有文化,不像咱們,起個變量名都得憋上半天,吃了沒有文化的虧。這也是爲何我把這篇文章起名爲 《Babel:把 ES6 送上天的通天塔》的緣由。npm

3、Babel 怎麼用

瞭解了 Babel 是什麼後,很明顯咱們就要開始考慮怎麼使用 Babel 來轉化 ES6 的代碼了,除了 Babel 自己提供的 cli 等工具外,它還支持和其它打包工具配合使用,譬如 webpack、rollup 等等,能夠參考官網對不一樣平臺提供的配置說明

本文爲了感覺 Babel 最原始的用法,不結合其它任何工具,直接使用 Babel 的 cli 來演示。

一、構建 Babel 演示的工程

使用以下命令構建一個 npm 包,並新建 src 目錄 和 一個 index.js 文件。

npm init -y

package.json 內容以下:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

二、安裝依賴包

npm install --save-dev @babel/core @babel/cli @babel/preset-env
後面會介紹這些包的做用,先看用法

增長 babel 命令來編譯 src 目錄下的文件到 dist 目錄:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "babel": "babel src --out-dir dist",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.9.0",
    "@babel/preset-env": "^7.9.0"
  }
}

三、增長 Babel 配置文件

在工程的根目錄添加 babel.config.js 文件,增長 Babel 編譯的配置,沒有配置是不進行編譯的。

const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = []
 
module.exports = { presets, plugins }
上例中 debug 配置是爲了打印出 Babel 工做時的日誌,能夠方便的看來,Babel 轉化了哪些語法。
  1. presets 主要是配置用來編譯的預置,plugins 主要是配置完成編譯的插件,具體的含義後面會講
  2. 推薦用 Javascript 文件來寫配置文件,而不是 JSON 文件,這樣能夠根據環境來動態配置須要使用的 presets 和 plugins
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = []
 
if (process.env["ENV"] === "prod") {
  plugins.push(...)
}
 
module.exports = { presets, plugins }

四、編譯的結果

配置好後,咱們運行 npm run babel 命令,能夠看到 dist 文件夾下生成了 index.js 文件,內容以下所示:

// src/index.js
const add = (a, b) => a + b
 
// dist/index.js
"use strict";
 
var add = function add(a, b) {
  return a + b;
};

能夠看到,ES6 的 const 被轉化爲 var ,箭頭函數被轉化爲普通函數。同時打印出來以下日誌:

> babel src --out-dir dist
 
@babel/preset-env: `DEBUG` option
 
Using targets:
{}
 
Using modules transform: auto
 
Using plugins:
  proposal-nullish-coalescing-operator {}
  proposal-optional-chaining {}
  proposal-json-strings {}
  proposal-optional-catch-binding {}
  transform-parameters {}
  proposal-async-generator-functions {}
  proposal-object-rest-spread {}
  transform-dotall-regex {}
  proposal-unicode-property-regex {}
  transform-named-capturing-groups-regex {}
  transform-async-to-generator {}
  transform-exponentiation-operator {}
  transform-template-literals {}
  transform-literals {}
  transform-function-name {}
  transform-arrow-functions {}
  transform-block-scoped-functions {}
  transform-classes {}
  transform-object-super {}
  transform-shorthand-properties {}
  transform-duplicate-keys {}
  transform-computed-properties {}
  transform-for-of {}
  transform-sticky-regex {}
  transform-unicode-regex {}
  transform-spread {}
  transform-destructuring {}
  transform-block-scoping {}
  transform-typeof-symbol {}
  transform-new-target {}
  transform-regenerator {}
  transform-member-expression-literals {}
  transform-property-literals {}
  transform-reserved-words {}
  transform-modules-commonjs {}
  proposal-dynamic-import {}
 
Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
Successfully compiled 1 file with Babel.

4、Babel 工做原理

在瞭解瞭如何使用後,咱們一塊兒來探尋一下編譯背後的事情,同時會熟悉 Babel 的組成和進階用法。

一、Babel 工做流程

前面提到 Babel 其實就是一個純粹的 JavaScript 的編譯器,任何一個編譯器工做流程大體均可以分爲以下三步:

  • Parser 解析源文件
  • Transfrom 轉換
  • Generator 生成新文件

Babel 也不例外,以下圖所示:

(圖片來源於網絡)

由於 Babel 使用是acorn這個引擎來作解析,這個庫會先將源碼轉化爲抽象語法樹 (AST),再對 AST 做轉換,最後將轉化後的 AST 輸出,便獲得了被 Babel 編譯後的文件。

那 Babel 是如何知道該怎麼轉化的呢?答案是經過插件,Babel 爲每個新的語法提供了一個插件,在 Babel 的配置中配置了哪些插件,就會把插件對應的語法給轉化掉。插件被命名爲 @babel/plugin-xxx 的格式。

二、Babel 組成

(1)@babel/preset-env

上面提到過 @babel/preset-* 實際上是轉換插件的集合,最經常使用的就是 @babel/preset-env,它包含了 大部分 ES6 的語法,具體包括哪些插件,能夠在 Babel 的日誌中看到。若是源碼中使用了不在 @babel/preset-env 中的語法,會報錯,手動在 plugins 中增長便可。

例如 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。但如今有一個提案提供了類的靜態屬性,寫法是在實例屬性的前面,加上 static 關鍵字。

// src/index.js
const add = (a, b) => a + b
 
class Person {
  static a = 'a';
  static b;
  name = 'morrain';
  age = 18
}

編譯時就會報以下錯誤:

根據報錯的提示,添加 @babel/plugin-proposal-class-properties 便可。

npm install --save-dev @babel/plugin-proposal-class-properties
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }

@babel/preset-env 中還有一個很是重要的參數 targets,最先的時候咱們就提過,Babel 轉譯是按需的,對於環境支持的語法能夠不作轉換的。就是經過配置 targets 屬性,讓 Babel 知道目標環境,從而只轉譯環境不支持的語法。若是沒有配置會默認轉譯全部 ES6 的語法。

// src/index.js
const add = (a, b) => a + b
 
// dist/index.js  沒有配置targets
"use strict";
 
var add = function add(a, b) {
  return a + b;
};

按以下配置 targets

// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      targets: {
        chrome: '58'
      }
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }

編譯後的結果以下:

// src/index.js
const add = (a, b) => a + b
 
// dist/index.js  配置targets  chrome 58
"use strict";
 
const add = (a, b) => a + b;

能夠看到 const 和箭頭函數都沒有被轉譯,由於這個版本的 chrome 已經支持了這些特性。能夠根據需求靈活的配置目標環境。

爲後方便後續的講解,把  targets 的配置去掉,讓 Babel 默認轉譯全部語法。

(2)@babel/polyfill

polyfill 直譯是墊片的意思,又是 Babel 裏一個很是重要的概念。先看下面幾行代碼:

// src/index.js
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()

按以前的方法,執行 npm run babel 後,咱們驚奇的發現,Array.prototype.includes 和 Promise 居然沒有被轉譯!

// dist/index.js
"use strict";
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();

原來 Babel 把 ES6 的標準分爲 syntax 和 built-in 兩種類型。syntax 就是語法,像 const、=> 這些默認被 Babel 轉譯的就是 syntax 的類型。而對於那些能夠經過改寫覆蓋的語法就認爲是 built-in,像 includes 和 Promise 這些都屬於 built-in。而 Babel 默認只轉譯 syntax 類型的,對於 built-in 類型的就須要經過 @babel/polyfill 來完成轉譯。@babel/polyfill 實現的原理也很是簡單,就是覆蓋那些 ES6 新增的 built-in。示意以下:

Object.defineProperty(Array.prototype, 'includes',function(){
  ...
})

因爲 Babel 在 7.4.0 版本中宣佈廢棄 @babel/polyfill ,而是經過 core-js 替代,因此本文直接使用 core-js 來說解 polyfill 的用法。

  • 安裝 core-js
npm install --save core-js
  • 注意 core-js 要使用 --save 方式安裝,由於它是須要被注入到源碼中的,在執行代碼前提供執行環境,用來實現 built-in 的注入
  • 配置 useBuiltIns

    在 @babel/preset-env 中經過 useBuiltIns 參數來控制 built-in 的注入。它能夠設置爲 'entry'、'usage' 和 false 。默認值爲 false,不注入墊片。

    設置爲 'entry' 時,只須要在整個項目的入口處,導入 core-js 便可。

// src/index.js
import 'core-js'
 
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
 
// dist/index.js
"use strict";
 
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.promise");
//
// ……  這裏還有不少
//
require("regenerator-runtime/runtime");
var add = function add(a, b) {
  return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();
  • 編譯後,Babel 會把目標環境不支持的全部 built-in 都注入進來,無論是否是用到,這有一個問題,對於只用到比較少的項目來講徹底沒有必要,白白增長代碼,浪費包體大小。

設置爲 'usage' 時,就不用在項目的入口處,導入 core-js了,Babel 會在編譯源碼的過程當中根據 built-in 的使用狀況來選擇注入相應的實現。

// src/index.js
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
 
// dist/index.js
"use strict";
 
require("core-js/modules/es6.promise");
 
require("core-js/modules/es6.object.to-string");
 
require("core-js/modules/es7.array.includes");
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();
  • 配置 corejs 的版本

當 useBuiltIns 設置爲 'usage' 或者 'entry' 時,還須要設置 @babel/preset-env 的 corejs 參數,用來指定注入 built-in 的實現時,使用 corejs 的版本。不然 Babel 日誌輸出會有一個警告。

最終的 Babel 配置以下:

// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      useBuiltIns: 'usage',
      corejs: 3,
      targets: {}
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }

(3)@babel/plugin-transform-runtime

在介紹 @babel/plugin-transform-runtime 的用途以前,先前一個例子:

// src/index.js
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise(resolve=>resolve(10))
 
class Person {
  static a = 1;
  static b;
  name = 'morrain';
  age = 18
}
 
// dist/index.js
"use strict";
 
require("core-js/modules/es.array.includes");
 
require("core-js/modules/es.object.define-property");
 
require("core-js/modules/es.object.to-string");
 
require("core-js/modules/es.promise");
 
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
 
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
  return resolve(10);
});
 
var Person = function Person() {
  _classCallCheck(this, Person);
 
  _defineProperty(this, "name", 'morrain');
 
  _defineProperty(this, "age", 18);
};
 
_defineProperty(Person, "a", 1);
 
_defineProperty(Person, "b", void 0);

在編譯的過程當中,對於 built-in 類型的語法經過 require("core-js/modules/xxxx") polyfill 的方式來兼容,對於 syntax 類型的語法在轉譯的過程會在當前模塊中注入相似 _classCallCheck 和 _defineProperty 的 helper 函數來實現兼容。對於一個模塊而言,可能還好,但對於項目中確定是不少模塊,每一個模塊模塊都注入這些 helper 函數,勢必會形成代碼量變得很大。

而 @babel/plugin-transform-runtime 就是爲了複用這些 helper 函數,縮小代碼體積而生的。固然除此以外,它還能爲編譯後的代碼提供一個沙箱環境,避免全局污染。

使用 @babel/plugin-transform-runtime

  • ①安裝
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

其中 @babel/plugin-transform-runtime 是編譯時使用的,安裝爲開發依賴,而 @babel/runtime 其實就是 helper 函數的集合,須要被引入到編譯後代碼中,因此安裝爲生產依賴

  • ②修改 Babel plugins 配置,增長@babel/plugin-transform-runtime
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      useBuiltIns: 'usage',
      corejs: 3,
      targets: {}
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties',
  [
    '@babel/plugin-transform-runtime'
  ]
]
 
module.exports = { presets, plugins }
  • 以前的例子,再次編譯後,能夠看到,以前的 helper 函數,都變成相似require("@babel/runtime/helpers/classCallCheck") 的實現了。
// dist/index.js
"use strict";
 
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
 
require("core-js/modules/es.array.includes");
 
require("core-js/modules/es.object.to-string");
 
require("core-js/modules/es.promise");
 
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
 
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
  return resolve(10);
});
 
var Person = function Person() {
  (0, _classCallCheck2["default"])(this, Person);
  (0, _defineProperty2["default"])(this, "name", 'morrain');
  (0, _defineProperty2["default"])(this, "age", 18);
};
 
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);
  • 配置 @babel/plugin-transform-runtime

到目前爲止,對於 built-in 類型的語法仍是經過 require("core-js/modules/xxxx") polyfill 的方式來實現的,例如爲了支持 Array.prototype.includes 方法,須要 require

("core-js/modules/es.array.includes") 在 Array.prototype 中添加 includes 方法來實現的,但這會致使一個問題,它是直接修改原型的,會形成全局污染。若是你開發的是獨立的應用問題不大,但若是開發的是工具庫,被其它項目引用,而剛好該項目自身實現了 Array.prototype.includes 方法,這樣就出了大問題!而 @babel/plugin-transform-runtime 能夠解決這個問題,只須要配置 @babel/plugin-transform-runtime 的參數 corejs。該參數默認爲 false,能夠設置爲 2 或者 3,分別對應 @babel/runtime-corejs2 和 @babel/runtime-corejs3。

把 @babel/plugin-transform-runtime 的 corejs 的值設置爲3,把 @babel/runtime 替換爲 @babel/runtime-corejs3。

去掉 @babel/preset-env 的 useBuiltIns 和 corejs 的配置,去掉 core-js。由於使用 @babel/runtime-corejs3 來實現對 built-in 類型語法的兼容,不用再使用 useBuiltIns了。

npm uninstall @babel/runtime
npm install --save @babel/runtime-corejs3
npm uninstall core-js
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      targets: {}
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties',
  [
    '@babel/plugin-transform-runtime',
    {
      corejs: 3
    }
  ]
]
 
module.exports = { presets, plugins }
 
 
// dist/index.js
"use strict";
 
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
 
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
 
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
 
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
 
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = (0, _includes["default"])(arr).call(arr, 3);
new _promise["default"](function (resolve) {
  return resolve(10);
});
 
var Person = function Person() {
  (0, _classCallCheck2["default"])(this, Person);
  (0, _defineProperty2["default"])(this, "name", 'morrain');
  (0, _defineProperty2["default"])(this, "age", 18);
};
 
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);

能夠看到 Promise 和 arr.includes 的實現已經變成局部變量,並無修改全局上的實現。

三、Babel polyfill 實現方式的區別

截至目前爲止,對於 built-in 類型的語法的 polyfill,一共有三種方式:

  • 使用 @babel/preset-env ,useBuiltIns 設置爲 'entry'
  • 使用 @babel/preset-env ,useBuiltIns 設置爲 'usage'
  • 使用 @babel/plugin-transform-runtime

前兩種方式支持設置 targets ,能夠根據目標環境來適配。useBuiltIns 設置爲 'entry' 會注入目標環境不支持的全部 built-in 類型語法,useBuiltIns 設置爲 'usage' 會注入目標環境不支持的全部被用到的 built-in 類型語法。注入的 built-in 類型的語法會污染全局。

第三種方式目前不支持設置 targets,因此不會考慮目標環境是否已經支持,它是經過局部變量的方式實現了全部被用到的 built-in 類型語法,不會污染全局。

針對第三種方式不支持設置 targets 的問題,Babel 正在考慮解決,目前意向的方案是經過 Polyfill provider 來統一 polyfill 的實現:

  • 廢棄 @babel/preset-env 中 useBuiltIns 和 corejs 兩個參數,再也不經過 @babel/preset-env 實現 polyfill。
  • 廢棄 @babel/plugin-transform-runtime 中的 corejs 參數,也再也不經過 @babel/plugin-transform-runtime 來實現 polyfill。
  • 增長 polyfills 參數,相似於如今 presets 和 plugins,用來取代如今的 polyfill 方案。
  • 把 @babel/preset-env 中 targets 參數,往上提一層,和 presets、plugins、polyfills 同級別,並由它們共享。

這個方案實現後,Babel 的配置會是下面的樣子:

// babel.config.js
const targets = [
  '>1%'
]
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties'
]
const polyfills = [
  [
    'corejs3',
    {
      method: 'usage-pure'
    }
  ]
]
 
module.exports = { targets, presets, plugins, polyfills }

配置中的 method 值有 'entry-global'、'usage-global'、'usage-pure' 三種。

  • 'entry-global' 等價於 @babel/preset-env 中的 useBuiltIns: 'entry'
  • 'usage-global' 等價於 @babel/preset-env 中的 useBuiltIns: 'usage'
  • 'usage-pure' 等價於 @babel/plugin-transform-runtime 中的 corejs

本文爲了講解方便,都是用 Babel 原生的 @babel/cli 來編譯文件,實際使用中,更多的是結合 webpack、rollup 這樣第三方的工具來使用的。

因此下一節,咱們聊聊打包工具 webpack。

5、參考文獻

  1. 6to5 JavaScript Transpiler Changes Name to Babel
  2. Babel學習系列2-Babel設計,組成
  3. 初學 Babel 工做原理
  4. RFC: Rethink polyfilling story

更多內容敬請關注 vivo 互聯網技術 微信公衆號

注:轉載文章請先與微信號:Labs2020 聯繫。

相關文章
相關標籤/搜索