Babel学习指南

844 阅读7分钟

babel 是什么

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

配置babel

Babel 也有配置文件!许多其他工具都有类似的配置文件:ESLint (.eslintrc)、Prettier (.prettierrc)。

所有 Babel API 参数 都可以被配置。然而,如果该参数需要用到 JavaScript 代码,你可能需要使用 JavaScript 代码版的 配置文件。

1.在package.json中设置babel字段。

可以选择将babelrc中的配置信息作为 babel 键(key)的值添加到 package.json 文件中,如下所示:

{
  "name": "my-package",
  "version": "1.0.0",
  "babel": {
    "presets": [ ... ],
    "plugins": [ ... ],
  }
}

2..babelrc文件或.babelrc.js

.babelrc和.babelrc.js是同一种配置方式,只是文件格式不同,一个是json文件,一个是js文件。

在你的项目中创建名为.babelrc或者.babelrc.js的文件,并输入以下内容。

//.babelrc
{
  "presets": [...],
  "plugins": [...]
}
//.babelrc.js
module.exports = {
    presets: [...],
    plugins: [...]
};

babel.config.js文件

babel.config.js写法和.babelrc.js一样,但是babel.config.js是针对整个项目,一个项目只有一个放在项目根目录。如果两种类型的配置文件都存在,.babelrc会覆盖babel.config.js的配置。

//babel.config.js
module.exports = {
    presets: [...],
    plugins: [...]
};

使用babel

我们使用npm init -y 来创建一个例子,然后需要安装@babel/cli 和 @babel/core这两个babel插件。分别是babel编译核心包和命令行执行babel命令工具。

npm install --save-dev @babel/core @babel/cli

项目结构如下:

通过命令把 index 文件用 babel 编译成compiled.js。

npx babel index.js --out-file compiled.js

或者编译整个 src 目录下的文件并输出到 dist 目录,输出目录可以通过 --out-dir 或 -d 指定。这不会覆盖 dist 目录下的任何其他文件或目录。

npx babel src --out-dir dist

这里我们使用npx babel src -d dist进行编译,得到编译后的文件目录:

我们对比src/index.js和编译后的dist/index.js:

编译好的文件没有任何变化,这是因为我们没有提供编译转化的插件。接下来我们通过配置babel,将index.js中的let和箭头函数转化为var和function。

npm i @babel/plugin-transform-arrow-functions -D

配置babel,这里我在根目录创建babel.config.js进行全局配置babel。

module.exports = {
  plugins: ['@babel/plugin-transform-arrow-functions']
};

配置好了之后,继续使用npx babel src -d dist进行编译

这次我们发现,箭头函数已经被编译成普通函数了。如果我们需要将let转化成var,可以继续安装对应的插件。

但是有个问题,我们总不能一个个的引入这些插件,来对应转化我们用到的每个新特性,这是非常麻烦的,于是有了一个东西叫做预设(Presets)。

Babel 的预设(preset)可以被看作是一组 Babel 插件和/或 options 配置的可共享模块。使用一个预设就是将这个预设规定的全部插件安装并使用。

preset-env

我们安装一下preset-env这个预设:

npm install --save-dev @babel/preset-env

在babel.config.js进行配置:

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

我们在配置中将之前的转化箭头函数的插件删除了,加入了presets,然后进行编译:

发现let和和箭头函数通过预设都转化为了var和普通函数。

我们在index.js添加一个map方法,再次进行编译:

发现语法层面都发生了变化,但是对于新的内置函数(Promise,Set,Map),静态方法(Array.from,Object.assign),实例方法(Array.prototype.includes)等就无法转化,这时我们就需要加入@bable/polyfill。

babel-polyfill

Babel 包含一个polyfill,其中包含一个自定义的regenerator 运行时和core-js。

这将模拟一个完整的 ES2015+ 环境,并且旨在用于应用程序而不是库/工具。

比如我们代码中用到了map方法,但是目标浏览器不支持map,引入babel-polyfill就会给数组添加一个map方法,代码执行的时候使用的map其实是babel-polyfill模拟出来功能,这样虽然污染了数组的静态方法,但是确实实现了兼容。

这意味着您可以使用新的内置函数,如Promiseor WeakMap、静态方法(如Array.fromor Object.assign)、实例方法(如Array.prototype.includes)和生成器函数(前提是您使用了regenerator插件)。String为了做到这一点,polyfill 添加到全局范围以及原生原型。

安装babel-polyfill:

npm install --save @babel/polyfill

我们可以对babel-polyfill进行按需加载,因为@babel/polyfill体积比较大,整体引入既增加项目体积,又污染了过多的变量,所以更推荐使用preset-env来按需引入polyfill。

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage'// usage-按需引入 entry-入口引入(整体引入) false-不引入polyfill
        corejs: 3  // 2-corejs@2  3-corejs@3
      }
    ]
  ]
};

🚨 从 Babel 7.4.0 开始,@babel/polyfill已经被弃用,取而代之的是直接包含core-js/stable(以填充 ECMAScript 特性)和regenerator-runtime/runtime(需要使用转译的生成器函数)。

corejs 是一个给低版本的浏览器提供接口的库,也是polyfill功能实现的核心,此处指定的是引入corejs的版本,需要通过npm安装指定版本的corejs库作为生产依赖。

npm i core-js@3

执行编译:

可以看到在使用使用map方法之前,提前从core-js引入了相应的polyfill。map方法其实是babel-polyfill模拟出来功能,这样虽然污染了Array的静态方法,但是确实实现了兼容。

plugin-transform-runtime

当我们编译一个class时,会发现多了好多自定义的函数。

我们在index.js里面加入一个class,进行编译:

"use strict";

require("core-js/modules/es.object.define-property.js");

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

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 = trueif ("value" in descriptor) descriptor.writable = trueObject.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor"prototype", { writablefalse }); return Constructor; }

var fn = function fn() {
  console.log('这是一个箭头函数');
};

var list = [123].map(function (item) {
  return item * 2;
});

var Student = /*#__PURE__*/function () {
  function Student(age) {
    _classCallCheck(thisStudent);

    this.age = age;
  }

  _createClass(Student, [{
    key"sayAge",
    valuefunction sayAge() {
      console.log(this.age);
    }
  }]);

  return Student;
}();

现在只有一个index文件需要转换,实际项目中可能有无数个文件需要转化,如果每一个转换后的文件中都存在相同的函数,那岂不是浪费了,怎么才能把重复的函数去掉呢?

上面出现的_classCallCheck,_defineProperties,_createClass三个函数叫做工具函数,是在编译阶段辅助 Babel 的函数。

这个时候plugin-transform-runtime就起到作用了。这个插件会将这些工具函数转换成引入的形式。

安装plugin-transform-runtime:

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

使用plugin-transform-runtime还需要安装@babel/runtime作为生产依赖项(因为它用于“运行时”)。

npm install --save @babel/runtime

配置babel.config.js:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage'// usage-按需引入 entry-入口引入(整体引入) false-不引入polyfill
        corejs: 3// 2-corejs@2  3-corejs@3
      },
    ],
  ],
  plugins: ['@babel/plugin-transform-runtime'],
};

我们看下编译之后的内容:

"use strict";

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

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

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

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

var fn = function fn() {
  console.log('这是一个箭头函数');
};

var list = [123].map(function (item) {
  return item * 2;
});

var Student = /*#__PURE__*/function () {
  function Student(age) {
    (0, _classCallCheck2["default"])(thisStudent);
    this.age = age;
  }

  (0, _createClass2["default"])(Student, [{
    key"sayAge",
    valuefunction sayAge() {
      console.log(this.age);
    }
  }]);
  return Student;
}();

我们看到babel编译时工具函数统一隔离到babel-runtime提供的helper模块中,直接从helper模块加载,不在每个文件中重复的定义辅助函数,从而减少包的尺寸。

babel-polyfill是通过向全局对象和内置对象的prototype上添加方法来实现的,有一个问题就是引入文件会污染变量,其实plugin-transform-runtime也提供了一种runtime的polyfill。

我们修改下babel.config.js:

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 _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map"));

var _context;

var fn = function fn() {
  console.log('这是一个箭头函数');
};

var list = (0, _map["default"])(_context = [123]).call(_context, function (item) {
  return item * 2;
});

var Student = /*#__PURE__*/function () {
  function Student(age) {
    (0, _classCallCheck2["default"])(this, Student);
    this.age = age;
  }

  (0, _createClass2["default"])(Student, [{
    key: "sayAge",
    value: function sayAge() {
      console.log(this.age);
    }
  }]);
  return Student;
}();

可以看到这次编译结果和之前的区别是创建了一个_map方法来模拟数组的map方法,使用数组map方法会调用我们创建的_map,这样就不会污染数组上的map方法。

plugin-transform-runtime 插件借助babel-runtime实现了下面两个重要的功能:

  1. 对工具函数的复用,解决转译语法层时出现的代码冗余
  2. 解决转译api(方法)层出现的全局变量污染