写在前面的话
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
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+。
三、实验内容
初始化准备
首先安装实验用到的依赖包
npm install --save-dev @babel/core @babel/cli @babel/preset-env
项目根目录下创建 babel.config.js
文件,用于配置 Babel
module.exports = {};
配置一
不进行配置
module.exports = {};
实验结果:
const cat = 'May'; const arrow = () => `Name: ${cat}`.padStart(2); const promise = new Promise(); let map = new Map();
除了多了两行空白,没有什么其它变化,说明 Babel 是基于插件架构的,假如你什么插件也不提供,那么 Babel 什么也不会做,即你输入什么输出的依然是什么。
配置二
增加 ES+ 转换集合包 @babel/preset-env
module.exports = { presets: ['@babel/preset-env'], };
实验结果:
"use strict"; var cat = 'May'; var arrow = function arrow() { return "Name: ".concat(cat).padStart(2); }; var promise = new Promise(); var map = new Map();
对样本中的 const
和 let
以及箭头函数和模板字符串语法进行了处理,但对于 padStart
、Promise
、Map
并没有处理,说明 @babel/preset-env
只能处理 ES+ 中新增的基本语法,不能对新增类和类的扩展属性进行处理。
这里就需要我们使用 @babel/polyfill
,实现新的内置函数、实例方法的转换。在我们使用 @babel/preset-env
的同时,它有个 useBuiltIns
选项,用来控制怎么样处理 @babel/polyfill
。这里的 useBuiltIns
有三个可选属性: 'entry'
| 'usage'
| false
,默认是 false
。
为了实现编译结果的可运行,我们需要改变样本代码,如下:
import "@babel/polyfill"; const cat = 'May'; const arrow = () => `Name: ${cat}`.padStart(2); const promise = new Promise(); let map = new Map();
配置三
module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: false, }] ], };
实验结果:
"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
整个包。
配置四
module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', }] ], };
实验结果:
"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
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
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";
它被拆成小模块,按需引入。
配置四
module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'entry', }] ], };
实验结果:
"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
配置五
module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }] ], };
实验结果:
"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
。
到这里好像一切近乎完美,但还有个问题没有处理,抽象和剥离。
配置六
我们改变样本文件:
const promise = new Promise(); class Cat { constructor(name){ this.name= name } getName(){ return } }
配置:
module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }] ], };
实验结果:
"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
必须要作为生产依赖被安装,如下 :
npm install --save-dev @babel/plugin-transform-runtime npm install --save @babel/runtime
同时,它还会为代码创建一个沙箱环境,这在我们写类库或者工具库时是很有必要的,避免污染全局变量。
配置七
module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }] ], plugins: [ ['@babel/plugin-transform-runtime'] ], };
实验结果:
"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; }();
我们发现,之前的帮助函数,是直接定义在文件内部的,如 :
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
现在变成:
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
从 @babel/runtime
统一引入,减少了文件体积。并且对变量名做了处理,避免了全局污染,但同时又发现了新问题,编译后的文件,仅仅对 class
相关的函数做了变量名处理,但是对 Promise
相关的变量名并没有处理。
配置八
module.exports = { presets: ['@babel/preset-env'], plugins: [ ['@babel/plugin-transform-runtime', { 'corejs': 3 }] ] };
实验结果:
"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)。