Babel常用模块讲解

Babel常用模块讲解

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
1、转换语法
2、通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
3、源代码转换(codemods)
更多...

上面的内容是引用自babel@7.7.0 官方文档 。babel的用法想必大家都已经很熟悉了,文章主要针对其中几个比较常用的模块,通过简单的使用,阐述其 基本的使用方法 以及它们 在代码转换中所起的作用

内容较长,建议跟着讲解一步步实践。

准备工作

开始之前,先做一下准备工作。首先我们创建一个简单的工作目录,目录结构如下:

|——test
    |——node_modules
    |——src
        |——index.js
    |——.babelrc
    |——package.json

然后我们在 src/index.js 中添加如下代码,都是我们经常使用的ES6中的新的语法和API:

// let声明 & 箭头函数
let fn = () => { 
  console.log(1)
// 实例方法
[2, 3, 4].includes(1)
// 静态方法
Object.assign({ a: 1, b: 2 }, { c: 3 });
// 新的API
let promise = new Promise((res, rej) => { 
  setTimeout(res, 1000)
let set = new Set()
let map = new Map()
// 新的语法
class Person {
  static get name() { 
    return 'Person'
  say() {
    console.log('hello world!')
}

这里我们采用 cli 的方式来对上面的代码进行转换,所以需要先安装以下 dev依赖

 @babel/cli
 @babel/core

然后在 package.json 中添加如下命令,将 src 目录下的源文件转换后输出到 lib 目录下:

"scripts": {
    "babel": "babel src --out-dir lib --watch"
}

最后说下,现在我们的 .babelrc 配置文件内容如下:

{}

没错,我们还没有做任何配置。

到这里,我们的准备工作已经都做好了,下面就开始介绍以下几个模块了。

@babel/preset-env

预设其实是Babel插件的组合,而 env 预设目前已经使用了一年多,并且现在已经完全替代了先前 babel 提出并建议的一些预设,比如:

babel-preset-es2015
babel-preset-es2016
babel-preset-es2017
babel-preset-latest

这意味着,不需要我们再去一个一个去手动引入多个插件,一个 @babel/preset-env 就可以cover大部分场景。

ok,现在我们把它引入到我们的项目中来。

使用

  • 安装
yarn add @babel/preset-env -D
  • 配置
{
  "presets": [ "@babel/preset-env"]
}
  • 执行 npm run babel ,查看转换结果:
"use strict";
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; }
// let声明 & 箭头函数
var fn = function fn() {
  console.log(1);
 // 实例方法
[2, 3, 4].includes(1);
 // 静态方法
Object.assign({
  a: 1,
// 新的API
var promise = new Promise(function (res, rej) {
  setTimeout(res, 1000);
var set = new Set();
var map = new Map(); 
// 新的语法
var Person =
/*#__PURE__*/
function () {
  function Person() {
    _classCallCheck(this, Person);
  _createClass(Person, [{
    key: "say",
    value: function say() {
      console.log('hello world!');
  }], [{
    key: "name",
    get: function get() {
      return 'Person';
  return Person;
}();

从转换结果可以看出,babel只转换了下面这三个地方:

  • let → var
  • 箭头函数语法
  • class语法

为什么?

以下内容摘自 Babel学习系列4 【侵删】

Babel 把 Javascript 语法 分为 Syntax API
1、 API :指那些我们可以通过 函数重新覆盖的语法 ,类似 includes / map / includes / Promise 凡是我们能想到重写的都可以归属到 API
2、 Syntax :像 let / const / class / Arrow Function / Decorators 等等这些,我们在 Javascript 在运行时无法重写的

Babel 默认只是转换 Syntax 层语法,所以需要 @babel/polyfill 来处理 API 兼容

除此之外

刚才我们说,一个 @babel/preset-env 基本上就可以满足我们的需求。但是,官方文档也说了,该预设并不支持 stage-x 相关的 plugin 。也就是说,如果我们在项目中使用了TC39制定的提案阶段的语法,该预设并不能帮我们转换。

It is important to note that @babel/preset-env does not support stage-x plugins.

比如,我们使用了stage-3阶段的 类的静态属性 提案语法:

class Person {
  static Version = 1.0
}

如果我们不做其他额外配置,直接运行 npm run babel ,终端会报错:

从Babel v7开始,所有的stage预设都已经弃用了。想要转换上述语法,其实报错信息里提示了,就是需要我们安装一个叫 @babel/plugin-proposal-class-properties 的插件。

我们按照提示安装并配置:

{
  "presets": [
    "@babel/preset-env"
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

然后再次转换就可以成功了,结果如下:

var Person = function Person() {
  _classCallCheck(this, Person);
_defineProperty(Person, "Version", 1.0);

配置

有关 @babel/preset-env 的相关配置,这里不多说,在后面介绍 @babel/polyfill 的时候,会着重讲一下 useBuiltIns 配置项,其他的请查看 官方文档

@babel/polyfill

上面也提到了 @babel/polyfill 这个模块是用来做什么的。接下来就看一下它的一些简单应用。不过用之前先说明一下:

As of Babel 7.4.0, this package has been deprecated in favor of directly including core-js/stable (to polyfill ECMAScript features) and regenerator-runtime/runtime (needed to use transpiled generator functions):

从Babel 7.4.0开始,官方就不再推荐使用此模块了,而是让用户按需引入下面这两个模块:

  • core-js/stable (polyfill的核心,以充实ECMAScript功能)
  • regenerator-runtime/runtime (需要转译生成器函数)
import "core-js/stable";
import "regenerator-runtime/runtime";

其实这两个模块本身也是 @babel/polyfill 的依赖项。

使用

  • 安装

需要安装为生产依赖 。因为这是一个运行时依赖,我们在代码运行的时候需要用到它。

yarn add @babel/polyfill
  • 配置

@babel/polyfill 的配置方式有很多,不一一叙述,感兴趣的直接去看 官方文档 ,写的很清楚。这里只介绍与 @babel/preset-env 配合使用的场景。

首先我们在入口文件中将 @babel/polyfill 引入进来:

import "@babel/polyfill";
// let声明 & 箭头函数
let fn = () => { 
  console.log(1)
// 下面的代码就不贴了
...

这里用到了 @babel/preset-env 模块的 useBuiltIns 配置项,现在就针对它的三种不同取值来看几个例子:

  1. false :这是默认值。

就是什么也不做:

  • 不要为每个文件自动添加 polyfill
  • 也不要将 import "core-js" import "@babel/polyfill" 转换为单独的 polyfill
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "false"
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

执行 npm run babel ,查看转换结果:

"use strict";
require("@babel/polyfill");
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; }
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; }
// let声明 & 箭头函数
var fn = function fn() {
  console.log(1);
}; // 实例方法
[2, 3, 4].includes(1); // 静态方法
Object.assign({
  a: 1,
}); // 新的API
var promise = new Promise(function (res, rej) {
  setTimeout(res, 1000);
var set = new Set();
var map = new Map(); // 新的语法
var Person =
/*#__PURE__*/
function () {
  function Person() {
    _classCallCheck(this, Person);
  _createClass(Person, [{
    key: "say",
    value: function say() {
      console.log('hello world!');
  }], [{
    key: "name",
    get: function get() {
      return 'Person';
  return Person;
_defineProperty(Person, "Version", 1.0);

可以看到,和一开始的转换结果并没有什么区别,的确什么也没做。

2. entry : 就是把全部 @babel/polyfill 全部一次性引入,如果配置了 target 属性,会引入满足 target 属性值的所有 polyfill

{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "entry"
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

执行 npm run babel ,查看转换结果:

"use strict";
require("core-js/modules/es6.array.copy-within");
require("core-js/modules/es6.array.every");
// 内容太多,就省略了,大家可以自己尝试
require("core-js/modules/web.dom.iterable");
require("regenerator-runtime/runtime");
// let声明 & 箭头函数
var fn = function fn() {
  console.log(1);
}; // 实例方法
[2, 3, 4].includes(1); // 静态方法
Object.assign({
  a: 1,
}); // 新的API
var promise = new Promise(function (res, rej) {
  setTimeout(res, 1000);
var set = new Set();
var map = new Map(); // 新的语法
var Person =
/*#__PURE__*/
function () {
  function Person() {
    _classCallCheck(this, Person);
  _createClass(Person, [{
    key: "say",
    value: function say() {
      console.log('hello world!');
  }], [{
    key: "name",
    get: function get() {
      return 'Person';
  return Person;
_defineProperty(Person, "Version", 1.0);

可以看到,很多东西,不管我们用不用得到都给我们引入了进来,这肯定不是我们想要的,因为这会让我们项目变得很臃肿。

3. usage : 这个选项值实现了 polyfill 的按需加载,解决了咱们刚刚在 entry 部分提到的问题。

不过当我们按照下面的配置来运行时,终端会给我们提示:

意思就是,使用 usage 这个选项时,应当在入口文件中移除对 polyfill 的手动引入。当然你不移除也没什么影响,只不过这个提示看着会难受一些...

{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage"
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

执行 npm run babel ,查看转换结果:

"use strict";
require("core-js/modules/es6.object.define-property");
require("core-js/modules/es6.map");
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.set");
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.object.assign");
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
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; }
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; }
// let声明 & 箭头函数
var fn = function fn() {
  console.log(1);
}; // 实例方法
[2, 3, 4].includes(1); // 静态方法
Object.assign({
  a: 1,
}); // 新的API
var promise = new Promise(function (res, rej) {
  setTimeout(res, 1000);
var set = new Set();
var map = new Map(); // 新的语法
var Person =
/*#__PURE__*/
function () {
  function Person() {
    _classCallCheck(this, Person);
  _createClass(Person, [{
    key: "say",
    value: function say() {
      console.log('hello world!');
  }], [{
    key: "name",
    get: function get() {
      return 'Person';
  return Person;
_defineProperty(Person, "Version", 1.0);

这次的结果看起来干净了很多,不过,我明明只用了Array实例的includes方法,为啥String实例上的includes方法的polyfill也引入了进来?

其实我们知道, js是一门动态语言,变量的类型只有在运行时才能确定,所以babel在编译阶段并不知道调用includes方法的实例究竟是一个Array实例还是一个String实例,所以只好根据方法名称(未验证)都引入进来了。

@babel/runtime

背景

介绍这个模块之前,咱们需要在 src 目录下新增一个 utils.js 文件,文件内容很简单:

export class Animal {
  makeSound() {
    console.log('ga~ga~ga~')
}

然后我们在 index.js 中把它文件引入进来:

import { Animal } from './utils'
// ...省略了原来的内容
class Duck extends Animal {}
let duck = new Duck()
duck.makeSound()

babel 配置我们还是沿用的上一节介绍 polyfill 时的 usage 配置,我们看一下此时编译出来的 utils.js 文件内容是什么样的:

"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
exports.Animal = void 0;
require("core-js/modules/es6.object.define-property");
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 Animal =
/*#__PURE__*/
function () {
  function Animal() {
    _classCallCheck(this, Animal);
  _createClass(Animal, [{
    key: "makeSound",
    value: function makeSound() {
      console.log('ga~ga~ga~');
  return Animal;
exports.Animal = Animal;

可以看到,与之前 index.js 编译出的文件相比,这两个文件中编译 class 语法时,都使用了相同的辅助方法: _createClass _classCallCheck 。试想一下,如果我们项目中有一百个文件都用到了class语法,那这些辅助方法就要在每一个文件中出现一次...这显然是一个问题。

使用

那怎么解决这个问题呢?很简单,就是把这些通用的方法提取出来,单独维护成一个公用的模块,而这个模块就是 @babel/runtime 。迫不及待要实践一番了。不过,别着急,这个模块需要和接下来要介绍的 @babel/plugin-transform-runtime 模块配合使用。

@babel/plugin-transform-runtime

使用

刚才已经介绍过背景了,这里我们直接介绍使用:

  • 安装
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime
  • 配置
{
  "presets": [
      "@babel/preset-env",
        "useBuiltIns": "usage"
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    ["@babel/plugin-transform-runtime"]
}
  • 执行 npm run babel ,查看转换结果
// index.js
"use strict";
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
// utils.js
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

可以看到相同的辅助方法,已经不在每个文件中单独定义了,而是从 @babel/runtime/helpers 中统一引入。

作用域隔离

看到这里,貌似一切问题都已经得到了很好的解决:

  • 转译新的语法、API
  • polyfill按需加载
  • 用到的辅助方法统一引入
  • ...

无论是从代码的兼容性还是代码的体积上都进行了精心的优化,可以说,当前的解决方案已经可以满足大部分项目的需求场景了。但如果我们开发的是一个工具包,未来需要提供给其他人使用的话,做到这样是完全不够的。

If you directly import core-js or @babel/polyfill and the built-ins it provides such as Promise , Set and Map , those will pollute the global scope .

回想一下我们之前在介绍 @babel/polyfill 中的示例,我们在 index.js 中使用了 Promise 这个API,而转译过后的文件中,为了兼容,babel为我们引入了它的polyfill实现:

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

这个实现是作用在全局作用域下的。如果某人在项目中使用了你提供的工具包,而他也自定义了一个全局Promise,那后果可想而知。

如何解决

其实 @babel/plugin-transform-runtime 这个插件,除了我们上面提到的作用之外,还有一个功能就是:为你的代码创建一个沙盒环境。

Another purpose of this transformer is to create a sandboxed environment for your code.

这个插件可以接收很多配置参数,具体的内容可以查看 官方文档 。我们这里只说即将用到的 corejs 这个配置项。

可选值与需要安装的模块

false :npm install --save @babel/runtime

2 :npm install --save @babel/runtime-corejs2

3 :npm install --save @babel/runtime-corejs3

看上面的表格可以很清楚的知道,这个配置项接受三个值。值不同,需要配合安装的模块也不同。

  • false

当使用这个选项值时, @babel/runtime 只提供共用的帮助方法,高级语法和API仍然需要安装 @babel/polyfill 这个生产依赖来转译。

// ====================== .babelrc ====================== //
  "presets": [
      "@babel/preset-env",
        "useBuiltIns": "usage"
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    ["@babel/plugin-transform-runtime",  {
      "corejs": false
// ====================== lib/index.js ====================== //
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
require("core-js/modules/es6.map");
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.set");
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.object.assign");
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
// .... 省略了源代码转译结果部分
  • 2

使用这个选项值,需要手动安装 @babel/runtime-corejs2 为生产依赖,而 @babel/runtime 这个依赖就可以去掉了。因为后者的功能前者已经包含了。

而且,除此之外, @babel/runtime-corejs2 这个模块还包含了部分 polyfill 的功能,这部分功能是 除实例上的方法(如: 'hello world'.includes('llo') )之外 的其他功能 。这点很重要,因为这也是 core-js@2 core-js@3 两个版本的主要不同。

// ====================== .babelrc ====================== //
  "presets": [
      "@babel/preset-env",
        "useBuiltIns": "usage"
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    ["@babel/plugin-transform-runtime",  {
      "corejs": 2
// ====================== lib/index.js ====================== //
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/inherits"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/map"));
var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
// .... 省略了源代码转译结果部分

从结果中我们也可以看出来,像源代码中使用的 Map / Set / Object.assign / Promise 等的 polyfill 都已经引用自 @babel/runtime-corejs2 这个模块;而实例上的方法,如 includes 仍然引用自 @babel/polyfill 模块。

  • 3

2 这个选项值类似, 3 需要安装手动安装 @babel/runtime-corejs3 为生产依赖。而这两者的最大不同就是我们刚才说的, 3 这个版本提供了实例上方法的 polyfill 。这意味着什么?意味着,我们现在连 @babel/polyfill 这个模块也可以去掉了。一个 @babel/runtime-corejs3 即可解决这一切问题。

// ====================== .babelrc ====================== //
  "presets": [
      "@babel/preset-env"
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    ["@babel/plugin-transform-runtime",  {
      "corejs": 3
// ====================== lib/index.js ====================== //
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/inherits"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));
var _set = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set"));
var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));