Babel 配置实验报告

写在前面的话

JavaScript语言的更新,也伴随着 Babel 的成长, 对于一个前端而言,JavaScript 新的 API 自然很香,但代价就是我们要转译它,比较常用工具的就是 Babel 。

ECMA有很多版本,6、7、8、9 ….,前端浏览器也有多种,每种也有着不同的版本,为了实现这多对多的关系,Babel 也表示非常难,最后的结果就是,随着 Babel 的升级,前端同学有一堆包要学习和了解,如 @babel/cli@babel/core@babel/polyfill@babel/preset-env 等等,Babel 目前最新的版本是 7.7.0,前一次比较重大的升级是 7.4.0,本着客户第一(Babel 很香)的原则, 下面对 Babel 的配置做了一些实验,本文主要是对于 Babel 的使用,不针对工作原理。

一、实验目的

测试 Babel 的不同配置对于 JavaScript 编译结果的影响

二、实验环境和要求

依赖包版本

@babel/core 7.7.0
@babel/cli 7.7.0
@babel/preset-env 7.7.1
@babel/runtime 7.7.0
@babel/plugin-transform-runtime 7.7.0
@babel/corejs@3 7.7.0

实验基础数据

创建实验基本的样本文件 index.js

1
2
3
4
const cat = 'May'
const arrow = () => `Name: ${cat}`.padStart(2);
const promise = new Promise();
let map = new Map();

我们通过对 Babel 的配置进行修改,实验在不同配置下的编译结果 。
本实验使用 babel-cli 命令行直接编译和输出文件,对应的命令如下:
npx babel index.js --out-file index_compile.js
将样本文件 index.js 编译输出到 index_compile.js
目标浏览器不配置,采用 Babel 默认,即转换所有 ECMAScript 2015+。

三、实验内容

初始化准备

首先安装实验用到的依赖包

1
npm install --save-dev @babel/core @babel/cli @babel/preset-env

项目根目录下创建 babel.config.js 文件,用于配置 Babel

1
module.exports = {};

####

配置一

不进行配置

1
module.exports = {};

实验结果:

1
2
3
4
5
6
const cat = 'May';

const arrow = () => `Name: ${cat}`.padStart(2);

const promise = new Promise();
let map = new Map();

除了多了两行空白,没有什么其它变化,说明 Babel 是基于插件架构的,假如你什么插件也不提供,那么 Babel 什么也不会做,即你输入什么输出的依然是什么。

配置二

增加 ES+ 转换集合包 @babel/preset-env

1
2
3
module.exports = {
presets: ['@babel/preset-env'],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
"use strict";

var cat = 'May';

var arrow = function arrow() {
return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

对样本中的 constlet 以及箭头函数和模板字符串语法进行了处理,但对于 padStartPromiseMap 并没有处理,说明 @babel/preset-env 只能处理 ES+ 中新增的基本语法,不能对新增类和类的扩展属性进行处理。

这里就需要我们使用 @babel/polyfill ,实现新的内置函数、实例方法的转换。在我们使用 @babel/preset-env 的同时,它有个 useBuiltIns 选项,用来控制怎么样处理 @babel/polyfill。这里的 useBuiltIns 有三个可选属性: 'entry' | 'usage' | false,默认是 false

为了实现编译结果的可运行,我们需要改变样本代码,如下:

1
2
3
4
5
6
import "@babel/polyfill";

const cat = 'May';
const arrow = () => `Name: ${cat}`.padStart(2);
const promise = new Promise();
let map = new Map();

配置三

1
2
3
4
5
6
7
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: false,
}]
],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

require("@babel/polyfill");

var cat = 'May';

var arrow = function arrow() {
return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

实验结果同配置二,做了基本语法的转译,直接引入了 @babel/polyfill 整个包。

配置四

1
2
3
4
5
6
7
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
}]
],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"use strict";

require("core-js/modules/web.dom.iterable");

require("core-js/modules/es6.array.iterator");

require("core-js/modules/es6.string.iterator");

require("core-js/modules/es6.map");

require("core-js/modules/es6.promise");

require("core-js/modules/es6.object.to-string");

require("core-js/modules/es7.string.pad-start");

var cat = 'May';

var arrow = function arrow() {
return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

同时我们得到了一些 Warning:

Warning1

1
WARNING: We noticed you re using the `useBuiltIns` option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the `corejs` option.

大致意思是,如果我们使用了 useBuiltIns 选项,建议配置 corejs 选项,如果不配置,默认提供 2.x 版本的 corejs

Warning2

1
2
When setting `useBuiltIns: 'usage'`, polyfills are automatically imported when needed.
Please remove the `import '@babel/polyfill'` call or use `useBuiltIns: 'entry'` instead.

这个警告是,让我们移除 import '@babel/polyfill ,polyfill 会被自动按需导入加载。所以我们在设置useBuiltIns'usage' 时,不需要手动引入 @babel/polyfill

这里的编译结果,不但对 ES+ 的新增语法进行了转译,而且对类和类的类的扩展属性也进行了转译,结果是比较符合我们期待的,能够直接运行在浏览器上。这里我们看不到 import "@babel/polyfill"; 它被拆成小模块,按需引入。

配置四

1
2
3
4
5
6
7
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'entry',
}]
],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"use strict";

require("core-js/modules/es6.array.copy-within");

require("core-js/modules/es6.array.fill");

require("core-js/modules/es6.array.find");

## 中间还有 200+ 个包省略。。。。。。

require("core-js/modules/web.immediate");

require("core-js/modules/web.dom.iterable");

require("regenerator-runtime/runtime");

var cat = 'May';

var arrow = function arrow() {
return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

这里同样也得到了同配置四中的 warning1,需要我们配置 corejs 选项,但没有 warning2,同时这里把 import "@babel/polyfill" 拆成小包,全量引入。

我们综合一下配置二三四,分别对 useBuiltIns 的三个可选 option 分别进行了实验,得出了如下结论

  • false:不处理 polyfill
  • ‘usage’:按需加载 polyfill,且不需要手动引入@babel/polyfill 文件
  • ‘entry’:必须手动引入 @babel/polyfill 文件,会把 @babel/polyfill 切为小包,全量引入,但要注意的是,这里的全量并不是真的全量,因为我们没有配置目标浏览器,Babbel 默认转了全量的 ECMAScript 2015+,如果配置了如: targets: "chrome>60" ,会在配置四的编译结果中,包减少到 20+ ,也就是 ‘entry’ 会加载目标浏览器所需的 polyfill

配置五

1
2
3
4
5
6
7
8
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]
],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"use strict";

require("core-js/modules/es.array.iterator");

require("core-js/modules/es.map");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("core-js/modules/es.string.iterator");

require("core-js/modules/es.string.pad-start");

require("core-js/modules/web.dom-collections.iterator");

var cat = 'May';

var arrow = function arrow() {
return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

为了解决配置三四中的warning1,我们手动手动设置了 corejs 选项,区别于默认值 2 ,我们设置了 3

配置四的编译结果相比,引用部分发生了变化,默认的 core-js:2 处理依赖是
require("core-js/modules/es6.map");
这里的 core-js:3
require("core-js/modules/es.map");

使用 core-js@3 的原因是,core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3。例如你使用了 Array.prototype.flat(),如果你使用的是 core-js@2,那么其不包含此新特性。为了可以使用更多的新特性,建议大家使用 core-js@3

到这里好像一切近乎完美,但还有个问题没有处理,抽象和剥离。

配置六

我们改变样本文件:

1
2
3
4
5
6
7
8
9
const promise = new Promise();
class Cat {
constructor(name){
this.name= name
}
getName(){
return
}
}

配置:

1
2
3
4
5
6
7
8
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]
],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"use strict";

require("core-js/modules/es.function.name");

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 _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 promise = new Promise();

var Cat =
/*#__PURE__*/
function () {
function Cat(name) {
_classCallCheck(this, Cat);

this.name = name;
}

_createClass(Cat, [{
key: "getName",
value: function getName() {
return;
}
}]);

return Cat;
}();

这里出现了很多公共方法, _classCallCheck_createClass 等,我们统称为 Babel 的注入帮助程序。如果每个文件都这么注入,必然是巨大的浪费资源,这个时候,需要使用 @babel/plugin-transform-runtime

首先安装依赖,@babel/plugin-transform-runtime 通常仅在开发时使用,但是运行时最终代码需要依赖 @babel/runtime,所以 @babel/runtime 必须要作为生产依赖被安装,如下 :

1
2
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

同时,它还会为代码创建一个沙箱环境,这在我们写类库或者工具库时是很有必要的,避免污染全局变量。

配置七

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
['@babel/plugin-transform-runtime']
],
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

require("core-js/modules/es.function.name");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var promise = new Promise();

var Cat =
/*#__PURE__*/
function () {
function Cat(name) {
(0, _classCallCheck2.default)(this, Cat);
this.name = name;
}

(0, _createClass2.default)(Cat, [{
key: "getName",
value: function getName() {
return;
}
}]);
return Cat;
}();

我们发现,之前的帮助函数,是直接定义在文件内部的,如 :

1
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

现在变成:

1
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

@babel/runtime 统一引入,减少了文件体积。并且对变量名做了处理,避免了全局污染,但同时又发现了新问题,编译后的文件,仅仅对 class 相关的函数做了变量名处理,但是对 Promise 相关的变量名并没有处理。

配置八

1
2
3
4
5
6
7
8
module.exports = {
presets: ['@babel/preset-env'],
plugins: [
['@babel/plugin-transform-runtime', {
'corejs': 3
}]
]
};

实验结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var promise = new _promise.default();

var Cat =
/*#__PURE__*/
function () {
function Cat(name) {
(0, _classCallCheck2.default)(this, Cat);
this.name = name;
}

(0, _createClass2.default)(Cat, [{
key: "getName",
value: function getName() {
return;
}
}]);
return Cat;
}();

这样感觉就比较完美了,即实现了对 polyfill 的按需加载,对注入的帮助函数的统一抽象剥离,又实现了对变量的处理,避免污染全局作用域,感觉很香。

四、实验结果和思考

我们通过对 Babel 中基本使用的 @babel/preset-env@babel/plugin-transform-runtime 进行配置,测试了不同配置下的实验结果,得出了比较合适的实践,但出现了一个灵魂的思考,既然 @babel/plugin-transform-runtime 能实现按需加载,沙箱环境,公用函数的统一抽象,还要在 useBuiltIns 里面搞三个参数干啥。这里猜测是考虑到包的体积大小。在 Babel 7.4.0 之后的版本,Babel官方明确建议了不再使用 @babel/polyfill ,建议使用 core-js/stable( polyfill ECMAScript features)和 regenerator-runtime/runtime,(needed to use transpiled generator functions)。