本文首发于 vivo互联网技术 微信公众号 
链接: https://mp.weixin.qq.com/s/plJewhUd0xDXh3Ce4CGpHg
作者:Morrain

一、前言

在上一节 《CommonJS:不是前端却反动了前端》中,咱们聊到了 ES6 Module,它是 ES6 中对模块的标准,ES6 是 ECMAScript 6.0 的简称,泛指 JavaScript 语言的下一代规范,它的第一个版本 ES2015 曾经在 2015 年 6 月正式公布,本文中提到的 ES6 包含 ES2015、ES2016、ES2017等等。在第一节的《Web:一路前行一路忘川》中也提到过,ES2015 从制订到公布历经了十几年,引入了很多的新个性以及新的机制,浏览器对 ES6 的反对进度远远赶不上前端开发小哥哥们应用 ES6 的激情,于是矛盾就日益显著……

二、Babel 是什么

先来看下它在官网上的定义:

Babel is a JavaScript compiler

没错就一句话,Babel 是 JavaScript 的编译器。至于什么是编译器,能够参考the-super-tiny-compiler这个我的项目,能够找到很好的答案。

本文是以 Babel 7.9.0 版本进行演示和解说的,另外倡议学习者浏览英文官网,中武官网会比原版网站慢一个版本,并且很多仍然是英文的。

Babel 就是一套解决方案,用来把 ES6 的代码转化为浏览器或者其它环境反对的代码。留神我的用词哈,我说的不是转化为 ES5 ,因为不同类型以及不同版本的浏览器对 ES6 新个性的反对水平都不一样,对于浏览器曾经反对的局部,Babel 能够不转化,所以 Babel 会依赖浏览器的版本,前面会讲到。这里能够先参考browerslist我的项目。

Babel 的历史

在学习任何一门常识前,我都习惯先理解它的历史,这样能力深刻理解它存在意义。

Babel 的作者是 FaceBook 的工程师 Sebastian McKenzie。他在 2014 年公布了一款 JavaScript 的编译器 6to5。从名字就能看进去,它次要的作用就是将 ES6 转化为 ES5。

这里的 ES6 指 ES2015,因为过后还没有正式公布, ES2015 的名字还未被正式确定。

于是很多人评估,6to5 只是 ES6 失去反对前的一个过渡计划,它的作者十分不批准这个观点,认为 6to5 不光会依照规范逐步完善,仍然具备十分大的后劲反过来影响并推动规范的制订。正因为如此 6to5 的团队感觉 '6to5' 这个名字并没有精确的传播这个我的项目的指标。加上 ES6 正式公布后,被命名为 ES2015,对于 6to5 来说更偏离了它的初衷。于是 2015 年 2 月 15 号,6to5 正式更名为 Babel。

(图片来源于网络)

Babel 是巴比伦文化里的通天塔,用来给 6to5 这个我的项目命名真得太贴切了!艳羡这些牛逼的人,不光代码写得好,还这么有文化,不像咱们,起个变量名都得憋上半天,吃了没有文化的亏。这也是为什么我把这篇文章起名为 《Babel:把 ES6 送入地的通天塔》的起因。

三、Babel 怎么用

理解了 Babel 是什么后,很显著咱们就要开始思考怎么应用 Babel 来转化 ES6 的代码了,除了 Babel 自身提供的 cli 等工具外,它还反对和其它打包工具配合应用,譬如 webpack、rollup 等等,能够参考官网对不同平台提供的配置阐明。

本文为了感触 Babel 最原始的用法,不联合其它任何工具,间接应用 Babel 的 cli 来演示。

1、构建 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"}

2、装置依赖包

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"  }}

3、减少 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 }

4、编译的后果

配置好后,咱们运行 npm run babel 命令,能够看到 dist 文件夹下生成了 index.js 文件,内容如下所示:

// src/index.jsconst 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.

四、Babel 工作原理

在理解了如何应用后,咱们一起来探寻一下编译背地的事件,同时会相熟 Babel 的组成和进阶用法。

1、Babel 工作流程

后面提到 Babel 其实就是一个纯正的 JavaScript 的编译器,任何一个编译器工作流程大抵都能够分为如下三步:

  • Parser 解析源文件
  • Transfrom 转换
  • Generator 生成新文件

Babel 也不例外,如下图所示:

(图片来源于网络)

因为 Babel 应用是acorn这个引擎来做解析,这个库会先将源码转化为形象语法树 (AST),再对 AST 作转换,最初将转化后的 AST 输入,便失去了被 Babel 编译后的文件。

那 Babel 是如何晓得该怎么转化的呢?答案是通过插件,Babel 为每一个新的语法提供了一个插件,在 Babel 的配置中配置了哪些插件,就会把插件对应的语法给转化掉。插件被命名为 @babel/plugin-xxx 的格局。

2、Babel 组成

(1)@babel/preset-env

下面提到过 @babel/preset-* 其实是转换插件的汇合,最罕用的就是 @babel/preset-env,它蕴含了 大部分 ES6 的语法,具体包含哪些插件,能够在 Babel 的日志中看到。如果源码中应用了不在 @babel/preset-env 中的语法,会报错,手动在 plugins 中减少即可。

例如 ES6 明确规定,Class 外部只有静态方法,没有动态属性。但当初有一个提案提供了类的动态属性,写法是在实例属性的后面,加上 static 关键字。

// src/index.jsconst 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.jsconst 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.jsconst add = (a, b) => a + b // dist/index.js  没有配置targets"use strict"; var add = function add(a, b) {  return a + b;};

按如下配置 targets

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

编译后的后果如下:

// src/index.jsconst 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.jsconst 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.jsimport '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.jsconst 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.jsconst 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.jsconst 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-runtimenpm install --save @babel/runtime

其中 @babel/plugin-transform-runtime 是编译时应用的,装置为开发依赖,而 @babel/runtime 其实就是 helper 函数的汇合,须要被引入到编译后代码中,所以装置为生产依赖

  • ②批改 Babel plugins 配置,减少@babel/plugin-transform-runtime
// babel.config.jsconst 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/runtimenpm install --save @babel/runtime-corejs3npm uninstall core-js
// babel.config.jsconst 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 的实现曾经变成局部变量,并没有批改全局上的实现。

3、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.jsconst 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。

五、参考文献

  1. 6to5 JavaScript Transpiler Changes Name to Babel
  2. Babel学习系列2-Babel设计,组成
  3. 初学 Babel 工作原理
  4. RFC: Rethink polyfilling story

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:Labs2020 分割。