关于前端:前端科普系列4Babel-把-ES6-送上天的通天塔

36次阅读

共计 13995 个字符,预计需要花费 35 分钟才能阅读完成。

本文首发于 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.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.

四、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.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 的实现曾经变成局部变量,并没有批改全局上的实现。

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

五、参考文献

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

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

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

正文完
 0