1

写在前面的话

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 、PromiseMap 并没有处理,说明 @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)。


河马嘴不大
376 声望77 粉丝

生命比你想象的要短,多写两行代码吧!少年