Babel配置

650 阅读7分钟

babel是什么

babel是一个js编译器

随着时间推移,JavaScript也在慢慢进化,新的特性和语法随之出现,然而各个浏览器厂商并没有完全的支持,所以要有个工具,把新的特性和语法翻译成浏览器都认可的标准语法,Babel应运而生,它就是这个工具,ES6/ES7/ES8 => Babel => ES5。

babel配置

@babel/cli @babel/core

@babel/cli是babel的命令行工具,主要提供babel命令。

babel 的核心功能包含在 @babel/core 模块中。没有它,在 babel 的世界里注定寸步难行。不安装 @babel/core,无法使用 babel 进行编译。

命令行安装

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

现在你就可以在项目中使用 babel 进行编译啦

首先创建一个新项目,例如babel-config,使用 npm init -y 进行初始化,创建 src/index.js,文件内容如下

let func = () => { }

修改 package.json, 在 script 里面新增

// package.json
"scripts": {
    "babel": "babel src --out-dir dist"
}

使用 npm run babel 来执行编译,编译成功后看下dist 目录下的 index.js 文件:

let func = () => {};

编译前后的代码是完全一样的。

这是因为 babel 虽然开箱即用,但是什么动作也不做,如果想要 babel 做一些实际的工作,就需要为其添加插件(plugin)。

插件

我们需要创建一个配置文件babel.config.js文件

babel开发者为配置文件提供了多种形式, babel7官方推荐用babel.config.js的形式。也可以用.babelrc, .babelrc.js 或者放到package.json

babel所有功能都建立在各种的plugin上,使用方式是安装相应的plugin再去配置文件中去使用。例如箭头函数转换插件, 安装@babel/plugin-transform-arrow-functions,然后在babel.config.js配置文件中去指定对应的插件

//babel.config.js
{
  plugins: ["@babel/plugin-transform-arrow-functions"],
}

然后执行npm run babel,可以看到箭头函数已经被编译完成

let func = function () {};

但是let没有被转化,因为我们刚引入的插件是专门用来转化箭头函数的,所以 let 并没有被转化。那如果想转化 let 就需要引入新的插件。

es6 那么多功能,我得一个一个引入?当然不,babel为我们提供了预设preset

preset

preset就是一组插件的集合。

官方针对我们常用的环境编写了一些preset

  • @babel/preset-env
  • @babel/preset-react
  • @babel/preset-typescript

创建 Preset

如需创建preset,导出一份配置即可。

module.exports = function() {
  return {
    plugins: [
      "pluginA",
      "pluginB",
      "pluginC",
    ]
  };
}

preset 可以包含其他的 preset,以及带有参数的插件。

module.exports = () => ({
  presets: [
    require("@babel/preset-env"),
  ],
  plugins: [
    [require("@babel/plugin-proposal-class-properties"), { loose: true }],
    require("@babel/plugin-proposal-object-rest-spread"),
  ],
});

@babel/preset-env介绍

@babel/preset-env所包含的插件将支持所有最新的JS特性(不包含 stage 阶段),将其转换成ES5代码。允许我们使用最新的js语法,比如 let,const,箭头函数等等。

什么是stage呢?stage里面包含了当年最新规范的草案,每年更新。

//babel.config.js
{
    "presets": ["@babel/preset-env"]
}

再次编译,运行结果:

"use strict";

var func = function func() {};

看似很完美,我们修改下 src/index.js

let func = () => { }

let arr = [1, 2, 4]
arr.includes(3)

编译出来的结果为:

"use strict";

var func = function func() {};

var arr = [1, 2, 4];
arr.includes(3);

这个编译出来的代码在低版本浏览器中使用的话,显然是有问题的,因为低版本浏览器中数组实例上没有 includes 方法。

babel只转换新的js语法,如箭头函数等,但不转换新的API,这时候需要借助@babel/polyfill,把es的新特性都装进来。

新的API分类两类,一类是Promise、Map、Object等全局对象及其对象自身的方法,例如Object.assign,Promise.resolve;另一类是新的实例方法

@babel/polyfill

polyfill 的中文意思是垫片,用来垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。

@babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块,可以模拟完整的 ES2015+ 环境(不包含第4阶段前的提议)。

babel v7.4版之后,官方不推荐再使用@babel/polyfill了,需要直接安装core-jsregenerator-runtime去替代之前的@babel/polyfill

虽然@babel/polyfill还在进行版本升级,但其使用的core-js包为2.x.x版本,而core-js这个包本身已经发布到了3.x.x版本了,@babel/polyfill以后也不会使用3.x.x版本的包了。core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3

安装core-jsregenerator-runtime:

npm install --save core-js regenerator-runtime

注意:不使用 --save-dev,因为这是一个需要在源码之前运行的polyfill。

我们需要将完整的 polyfill 在代码之前加载,修改我们的 src/index.js:

//import '@babel/polyfill'; (babel v7.4版之后,不推荐使用)
import "core-js/stable";
import "regenerator-runtime/runtime";

let func = () => { }
let arr = [1, 2, 4]
arr.includes(3)

编译出来的结果为:

"use strict";

require("core-js/stable");
require("regenerator-runtime/runtime");

var func = function func() {};
var arr = [1, 2, 4];
arr.includes(3);

此时我们的代码在低版本浏览器中已经能够正常运行了。

@babel/runtime

修改src/index.js中的内容:

class Person {    
    sayname() {        
        return 'name'    
    }
}
var john = new Person()
console.log(john)

编译后生成的代码如下:

"use strict";

require("core-js/stable");
require("regenerator-runtime/runtime");

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 Person = /*#__PURE__*/function () {  function Person() {    _classCallCheck(this, Person);  }  _createClass(Person, [{    key: "sayname",    value: function sayname() {      return 'name';    }  }]);  return Person;}();var john = new Person();console.log(john);

可以看到转换后的代码上面增加了好几个函数声明,这就是注入的函数,我们称之为辅助函数。@babel/preset-env在做语法转换的时候,注入了这些函数声明,以便语法转换后使用。

但样这做存在一个问题。在我们正常的前端工程开发的时候,少则几十个js文件,多则上千个。如果每个文件里都使用了class类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会导致我们打包出来的包非常大。

那么怎么办?一个思路就是,我们把这些函数声明都放在一个npm包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过webpack这一类的构建工具打包的时候,我们只会把使用到的npm包里的函数引入一次,这样就做到了复用,减少了体积。

@babel/runtime就是上面说的这个npm包,@babel/runtime把所有语法转换会用到的辅助函数都集成在了一起。

我们先安装这个包:(@babel-runtime是代码运行时需要的依赖,所以需要作为生产依赖安装)

npm install --save @babel/runtime

然后到node_modules下查看@babel/runtime/helpers下的文件,我们发现_classCallCheck, _defineProperties与 _createClass这个三个辅助函数就在这个helpers下面,我们手动把辅助函数替换掉函数声明,之前文件的代码就变成如下所示:

"use strict";

require("core-js/stable");
require("regenerator-runtime/runtime");

var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var _defineProperties = require("@babel/runtime/helpers/defineProperties");
var _createClass = require("@babel/runtime/helpers/createClass");

var Person = /*#__PURE__*/function () {  function Person() {    _classCallCheck(this, Person);  }  _createClass(Person, [{    key: "sayname",    value: function sayname() {      return 'name';    }  }]);  return Person;}();
var john = new Person();
console.log(john);

这样就解决了代码复用和最终文件体积大的问题。不过,这么多辅助函数要一个个记住并手动引入,平常人是做不到的,我也做不到。这个时候,Babel插件@babel/plugin-transform-runtime就来帮我们解决这个问题。

@babel/plugin-transform-runtime

这个插件的作用就是自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。这样就减少了我们手动引入的麻烦。

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

现在,我们的Babel配置文件如下:

{    
    "presets": ["@babel/preset-env"],
    "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/stable");
require("regenerator-runtime/runtime");

var Person = /*#__PURE__*/function () {  function Person() {    (0, _classCallCheck2["default"])(this, Person);  }  (0, _createClass2["default"])(Person, [{    key: "sayname",    value: function sayname() {      return 'name';    }  }]);  return Person;}();
var john = new Person();
console.log(john);

可以看到,它生成的代码比我们完全手动引入@babel/runtime里的辅助函数更加优雅。

@babel/plugin-transform-runtime的另一个作用是创建一个沙盒环境来避免对全局环境的污染

以Promise举例子,编译前的代码:

var obj = Promise.resolve();

若使用了babel-polyfill或core-js/stable与regenerator-runtime/runtime来做全局的API补齐,那么编译后的代码仍然是

var obj = Promise.resolve();

polyfill只是补齐了浏览器的window.Promise对象。

若我们不使用polyfill,而开启@babel/plugin-transform-runtime的API转换功能。那么编译后的代码将是

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

var obj = _promise["default"].resolve();

@babel/plugin-transform-runtime把我们代码里的Promise变成了_promise[“default”],而_promise[“default”]拥有ES标准里Promise所有的功能。现在,即使浏览器没有Promise,我们的代码也能正常运行。

那么,上面讲的API转换有什么用,明明通过polyfill补齐API的方式也可以使代码在浏览器正常运行?

其实,API转换主要是给开发JS库或npm包等的人用的,我们的前端工程一般仍然使用polyfill补齐API。

可以想象,如果开发JS库的人使用polyfill补齐API,我们前端工程也使用polyfill补齐API,但JS库的polyfill版本或内容与我们前端工程的不一致,那么我们引入该JS库后很可能会导致我们的前端工程出问题。所以,开发JS库或npm包等的人会用到API转换功能。

如何开启@babel/plugin-transform-runtime的API转换功能

安装@babel/runtime-corejs3

npm install --save @babel/runtime-corejs3

修改babel.config.js

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3 //corejs取值是false、2和3,默认值是false
      }
    ]
  ]
}

@babel/runtime-corejs3和@babel/runtime的区别

@babel/runtime我们知道里面存放的是babel做语法转换的辅助函数,@babel/runtime-corejs3是@babel/runtime的进化版,这个npm包里除了包含babel做语法转换的辅助函数,也包含了core-js的API转换函数。

除了这两个包,还有一个@babel/runtime-corejs2的包。它和@babel/runtime-corejs3的功能是一样的,只是里面的函数是针对core-js2版本的。

对于@babel/runtime及其进化版@babel/runtime-corejs2、@babel/runtime-corejs3,我们只需要根据自己的需要安装一个。
如果你不需要对core-js做API转换,那就安装@babel/runtime并把corejs配置项设置为false即可。
如果你需要用core-js2做API转换,那就安装@babel/runtime-corejs2并把corejs配置项设置为2即可。
如果你需要用core-js3做API转换,那就安装@babel/runtime-corejs3并把corejs配置项设置为3即可。

Browserslist 集成

@babel/preset-env会根据配置的目标环境,生成插件列表来编译。

当不需要兼容所有的浏览器和环境时,通过指定目标环境,可以让编译代码保持最小。

官方推荐使用 .browserslistrc 文件来指定目标环境。

//.browserslistrc
> 1%
not ie <= 8

上面的配置含义是,目标环境是市场份额大于1%的浏览器并且不考虑IE8及以下的IE浏览器。

.browserslistrc 的内容配置为:

last 2 Chrome versions

然后再执行 npm run babel,你会发现箭头函数不会被编译成ES5,因为 chrome 的最新2个版本都能够支持箭头函数。

查看 browserslist 的更多配置

参考文章

不容错过的 Babel7 知识

babel教程