# Babel 配置用法解析

写前面:babel 默认是只会去转义 js 语法的,不会去转换新的 API,比如像 Promise、Generator、Symbol 这种全局 API 对象,babel 是不会去编译的。在我学会了 babe 配置 l 大法之后,看我一会儿怎么把这些新的 API 给它编译出来就完事儿了。


# 本文基于 babel7.8.0。我主要记录下 babel 配置需要的一些重要的模块儿包,来一步步进行 babel 的一个配置解析 (以 babel.config.js 方式配置为例)。
# 本文主要涉及到的一些 babel 包:
  • # @babel/core
  • # @babel/cli
  • # @babel/plugin*
  • # @babel/preset-env
  • # @babel/polyfill
  • # @babel/runtime
  • # @babel/plugin-transform-runtime
# 那,话不多说,发车?

# @babel/core

@babel/core 这个包里主要都是一些去对代码进行转换的核心方法,具体里面的一些 api 方法我就不做介绍了;引用官网的一句话:“所有转换将使用本地配置文件”,所以待会儿我们的 babel.config.js 配置文件就很重要了;再一个 core 就是核心的意思嘛,所以我们话不多说先把它装起来,gogogo

Copynpm install --save-dev @babel/core

# @babel/cli

这个 @babel/cli 就是 babel 带有的内置 cli,可以用来让我们从命令行来编译我们的文件,有了他我们就很方便的对 babel 进行学习了,那话不多说,先装起来?

Copynpm install --save-dev @babel/cli

装完之后你就可以这样来编译你的文件:

Copynpx babel study.js --watch --out-file study-compiled.js

简单介绍下上面命令用到的几个参数: --out-file 用来把编译后的目标文件输出到对应的文件;如果希望在每次更改目标文件时都进行编译,可以加上 --watch 选项;当然还有一些别的选项,不过在我学习 babel 以及配置的话,这两个选项已经够我用了。

在操作的过程中如果改了 babel 配置发现编译出来的文件并没有实时编译的情况,需要注意下,如果改了配置文件那就需要重新执行这段命令,要不然 babel 读不到新的配置。

如果你已经创建了 study.js 文件并且执行了这段命令,你会发现,对应的 study-compiled.js 还没发生变化,因为我们还没开始写 babel 的配置文件,莫慌,马上开始。

# @babel/plugin * 和 @babel/preset-env

babel 插件和 babel 预设是 babel 配置的两个主要模块,所以我就放在一起说了。

# @babel/plugin*

首先我们先来说下 babel Pluginsbabel 官网也说了,人 babel 是基于插件化的,大概就是说全是插件,所以说我们配置文件里如果什么插件也不配的话,那输入和输出就是一样的,插件插件,你得插上我才让你用。我来编译一个最简单的箭头函数来看下这个 babel 的插件怎么用,来了,这波我们就需要配置文件了,以下所有的配置都是说的在 babel.config.js 文件里,相应的插件记得 install

Copy/* babel.config.js */
module.exports = {
  presets: [
  ],
  plugins: [
    "@babel/plugin-transform-arrow-functions"
  ]
}

然后执行我们上面那段 cli 的命令,就会得到这种效果:

Copy/* study.js */
const study = () => {}
/* study-compiled.js */
const study = function () {};

当然,如果我们想要使用 es6 给数值新增的指数运算符怎么办,只需要添加相应的 @babel/plugin-transform-exponentiation-operator 插件即可:

Copy/* babel.config.js */
module.exports = {
  presets: [
  ],
  plugins: [
    "@babel/plugin-transform-arrow-functions",
    "@babel/plugin-transform-exponentiation-operator"
  ]
}

对应的 es6 语法就会变成:

Copy/* study.js */
const exponentiation = 2 ** 2
/* study-compiled.js */
const exponentiation = Math.pow(2, 2);

# @babel/preset-env

从上面的插件化我们就知道需要哪个插件就去引入就完事儿,那么问题来了,es6 新增的语法那么多,我总不能一个插件一个插件去添加吧,这样也太麻烦了,这个时候就要用到 babel 提供的 presets 了。

presets 也就是预设的意思,大概意思就是可以预先设定好一些东西,就省得我们一个个的去引入插件了。官方提供了很多 presets,比如 preset-env(处理 es6 + 规范语法的插件集合)、preset-stage(一些处理尚在提案阶段的语法的插件集合,当然这种预设的方式在 babel 7+ 版本已经被废弃了)、preset-react(处理 react 语法的插件集合)等等。

我们主要来介绍下 preset-envpreset-env 是一个智能预设,配置了它就可以让你用 es6 + 去书写你的代码,而且他会按需去加载所需要的插件,让你的生活更加美好。。。接下来我们记得先 install 这个 @babel/preset-env 一波,不配任何插件,然后我们再来看看效果如何:

Copy/* babel.config.js */
module.exports = {
  presets: [
    "@babel/preset-env"
  ],
  plugins: [
  ]
}

对应的 es6 语法就会变成:

Copy/* study.js */
const study = () => {}
const arr1 = [1, 2, 33]
const arr2 = [...arr1]
const exponentiation = 2 ** 2
// 新增 API
new Promise(() => {})
new Map()
/* study-compiled.js */
var study = function study() {};
var arr1 = [1, 2, 33];
var arr2 = [].concat(arr1);
var exponentiation = Math.pow(2, 2);
// 新增 API
new Promise(function () {});
new Map();

你会发现 es6 + 的语法都被编译了,我们并没有设置任何插件哦,应该也看到了新增的 API 方法并没有被编译,在这里我们埋下伏笔,等下文讲到 polyfill 的时候再治他。

# Browserslist 集成

关于 preset-env,我们还可以提供一个 targets 配置项指定运行环境,就是我们可以配置对应目标浏览器环境,那么 babel 就会编译出对应目标浏览器环境可以运行的代码。相信有同学遇到过在低版本系统 ios 手机里自己的项目会白屏,其实是某些语法在 ios 低版本系统里不支持,这个时候我们可以直接配置 ios 7 浏览器环境都可以支持的代码:

Copy/* babel.config.js */
module.exports = {
  presets: [
    [
      "@babel/preset-env", {
        'targets': {
          'browsers': ['ie >= 8', 'iOS 7'] // 支持 ie8,直接使用 iOS 浏览器版本 7
        }
      }
    ]
  ],
  plugins: [
  ]
}

当然 babel Browserslist 集成还支持在 package.json 文件里或者新建一个 .browserslistrc 文件来指定对应目标环境。browserslist 配置源

# @babel/polyfill (由 core-js2 和 regenerator-runtime 组成的一个集成包)

上文也提到了像 Promise 这种 API 咱们的 babel 并没有给转义,那是因为 babel 默认是只会去转义 js 语法的,不会去转换新的 API,比如像 Promise、Generator、Symbol 这种全局 API 对象,babel 是不会去编译的,这个时候就要掏出 @babel/polyfill 了。用法很简单,先安装一波,然后我们只需要在入口文件顶部引入 @babel/polyfill 就可以使用新增的 API 了。

Copy/* study.js */
import '@babel/polyfill'
// 新增 API
new Promise(function () {});
/* study-compiled.js */
require("@babel/polyfill");
// 新增 API
new Promise(function () {});

小细节:import 被编译成了 require,如果想要编译出来的模块引入规范还是 import,则可以在 preset-env 的配置项中添加 "modules": false 即可。
modules 的 options:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为 "auto"

但是问题又来了,有时候我们项目里并没有用到那么多的新增 API,但是 @babel/polyfill 会把所有浏览器环境的的 polyfill 都引入,整个包的体积就会很大,我们想要对目标环境按需引入相应的 polyfill 应该怎么办呢,这个时候我们就可以使用 preset-env 的配置项中的 useBuiltIns 属性来按需引入 polyfill。

Copy/* babel.config.js */
module.exports = {
  presets: [
    [
      "@babel/preset-env", {
        "modules": false,
        "useBuiltIns": "entry",
        'targets': {
          'browsers': ['ie >= 8', 'iOS 7'] // 支持 ie8,直接使用 iOS 浏览器版本 7
        }
      }
    ]
  ],
  plugins: [
  ]
}

这个时候就会在入口处只把所有 ie8 以上以及 iOS 7 浏览器不支持 api 的 polyfill 引入进来。

最终效果:

Copy/* study.js */
import '@babel/polyfill'
// 新增 API
new Promise(function () {});
/* study-compiled.js */
import "core-js/modules/es6.array.copy-within";
import "core-js/modules/es6.array.every";
...// 省略若干
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";
import "regenerator-runtime/runtime";
// 新增 API
new Promise(function () {});

此时你会发现,import '@babel/polyfill' 没有了,引入的是我们目标环境相应的 polyfill。但是有没有发现引入的都是 import 'core-js/...' 的内容,标题已经说啦,@babel/polyfil 是由 core-js2 和 regenerator-runtime 组成的一个集成包。

这个时候你又会想,假如我的项目里面只用到了 Promise 这个 API,能不能只给我引入 Promise 相应的 API 呢?答案是必可以!,让我们先来好好了解下 preset-env 的配置项中的 useBuiltIns 属性。

# useBuiltIns

选项:"usage"| "entry"| false,默认为 false。

entry 我们已经用过了,意义就是在入口处将根据我们配置的浏览器兼容,将目标浏览器环境所有不支持的 API 都引入。

usage 就很 nb 了,当配置成 usage 的时候,babel 会扫描你的每个文件,然后检查你都用到了哪些新的 API,跟进我们配置的浏览器兼容,只引入相应 API 的 polyfill,我们把 useBuiltIns 属性设置为 usage 再来看下编译效果:

Copy/* study.js */
import '@babel/polyfill'
// 新增 API
new Promise(function () {});
/* study-compiled.js */
import "core-js/modules/es.object.to-string";
import "core-js/modules/es.promise";
// 新增 API
new Promise(function () {});

我就问你帅不帅!完全的按需引入,牛逼了!

相信你也看到了一个东西,当我们使用 useBuiltIns 选项的时候,你的命令行里面是不是显示了一坨这样的警告,大概是在配置文件中未指定 core-js 版本时,默认会使用 core-js2:

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.

前面也说到了 @babel/polyfil 是由 core-js2 和 regenerator-runtime 组成的一个集成包,现在 core-js3 已经发布了,而且很稳定。但是 core-js2 在 18 年的时候已经不再维护了;@babel/polyfil 引入的是 2 不是 3,并且 @babel/polyfill 在 babel7.4.0 已经不再推荐使用了,要废掉 (好像是因为 @babel/polyfill 不支持 core-js2 平滑的过渡到 core-js3)。所以 core-js 官方现在推荐我们使用 polyfill 的时候直接引入 core-js 和 regenerator-runtime/runtime 这两个包完全取代 @babel/polyfil 来为了防止重大更改。

当然,我们需要在 preset-env 配置项中指定 core-js 版本,这样就不会再有警告⚠️了:

Copy/* babel.config.js */
module.exports = {
  presets: [
    [
      "@babel/preset-env", {
        "modules": false,
        "useBuiltIns": "entry",
        "corejs": "3",
        'targets': {
          'browsers': ['not ie >= 8', 'iOS 7'] // 支持 ie8,直接使用 iOS 浏览器版本 7
        }
      }
    ]
  ],
  plugins: [
  ]
}

# @babel/runtime (依赖 @babel/helpers 和 regenerator-runtime)

有的时候一些语法的转换会比较复杂,babel 会引入一些 helper 函数,比如说对 es6 的 class 进行转换:

Copy/* study.js */
class Test {}
/* study-compiled.js */
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Test = function Test() {
  _classCallCheck(this, Test);
};

可以看到上面引入了 helper 函数来处理 class 的转换。但是问题又来了,如果好多文件都使用到了复杂语法的转换,这个还是简单点的,有些 helper 函数是很复杂代码量很多的,那岂不是每个文件都会定义一遍这些个函数,每个文件的代码会很多?如果说可以把这些 helper 函数都抽离到一个公共的包里,用到的地方只需要引入对应的函数即可,我们的编译出来的代码量会大大滴减少,这个时候就需要用到 @babel/plugin-transform-runtime 插件来配合 @babel/runtime 进行使用。记得先安装一波,然后在插件选项中加入 @babel/plugin-transform-runtime 这个插件,然后我们来看看编译后的效果:

Copy/* study.js */
class Test {}
/* study-compiled.js */
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
var Test = function Test() {
  _classCallCheck(this, Test);
};

当然如果我们只是为了减少编译出来的文件中代码量而使用这个插件的话就太小看他了,而且也没有必要。

@babel/plugin-transform-runtime 还有一个最重要的作用:比如说像上面我们说的 Promise 就需要提供相应的 polyfill 去解决,这样做会有一个副作用,就是会污染全局变量。如果我们只是在一个业务项目这样搞还好,也没别人要用到。但是如果我们是在维护一个公共的东西,比如公共组件库,我们这样搞,你的一些 polyfill 可能会把一些全局的 api 给改掉,副作用就会很明显,别人用你的组件库的时候就可能会出问题。@babel/plugin-transform-runtime 插件为我们提供了一个配置项 corejs,他可以给这些 polyfill 提供一个沙箱环境,这样就不会污染到全局变量,无副作用你说美不美。

记得安装 @babel/runtime-corejs2 这个包 (稳定版用 2 就可以),注意如果不配置的话,是不会提供沙箱环境的。然后在 @babel/plugin-transform-runtime 插件配置 corejs:

Copy/* babel.config.js */
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "useBuiltIns": "usage",
        "corejs": "3",
        'targets': {
          'browsers': ["ie >= 8", "iOS 7"] // 支持 ie8,直接使用 iOS 浏览器版本 7
        }
      }
    ]
  ],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 2
      }
    ]
  ]
}

我们来看下编译后的效果:

Copy/* study.js */
new Promise(() => {})
class Test {}
/* study-compiled.js */
import _classCallCheck from "@babel/runtime-corejs2/helpers/classCallCheck";
import _Promise from "@babel/runtime-corejs2/core-js/promise";
new _Promise(function () {});
var Test = function Test() {
  _classCallCheck(this, Test);
};

# 小节

  • 在你修改了 babel 配置项之后一定要记得重启编译命令,否则不会生效
  • 维护公共组件库或者一些别的公共库推荐要使用 @babel/runtime 配合 @babel/plugin-transform-runtime 来建立沙箱环境