Skip to content

上一节我们学习了 babel 插件的分类、babel 的 preset、helper、runtime。babel 的功能基本都建立在这些之上。

我们通过插件完成了各种代码(es next、proposal、typescript/flow/jsx...)到 es5 的转换,然后把不同的转换插件封装到不同的 preset (preset-env、preset-typescript、preset-react...)里,而且还把插件内部的公共逻辑抽成 helper 来复用,并且提供了 runtime 包用于注入运行时的 api。这样已经能够达到不同语法的代码转 es5 同时对 api 进行 polyfill 的目标了。

平时我们使用 babel 并不需要了解 runtime、helper 都是什么,plugin 怎么写,只需要会用 preset 就行了。

由 preset 引入一系列 plugin,我们只需要选择不同的 preset 即可。

那 babel 的 preset 都是怎么设计的呢?

preset-es20xx 到 preset-env

babel6 支持的 preset 是 preset-es2015、preset-es2016、preset-stage-x 等。

也就是根据目标语言版本来指定一系列插件。

但是这样的 preset 设计有个问题:

指定了目标环境支持 es5,但如果目标环境支持了部分 es6(es2015)、es7(es2016)等,那岂不是做了很多没必要的转换?

还有,reset-es2015、preset-es2016、preset-stage-x 这种 preset 跟随版本走的,那岂不是经常变,得经常改这些 preset 的内容 (当某个提案从 stage 0 进入到 stage 1 就得改下),这样多麻烦啊,而且用户也得经常改配置,stage-x 用到了啥对用户来说也是黑盒。

怎么解决这些问题呢?

babel6 到 babel7 的变化给出了答案:

babel7 废弃了 stage-x 和 es20xx 的 preset,改成 preset-env 和 plugin-proposal-xx 的方式。

这样就不需要指定用的是 es 几了,默认会全部支持,包含所有的已经是语言标准特性的 transform plugin

而且 stage-x 有哪些不再是黑盒,用户想用啥 proposal 的特性直接显示引入对应的 proposal plugin。

做了很多无用的转换的问题通过指定目标环境来解决。

但是目标环境那么多,浏览器版本、node 版本、electron 版本每年都在变,怎么做到精准?

comat-table

答案是 compat-table 的数据,compat-table 提供了每个特性在不同环境中的支持版本。

比如默认参数这个 es2015 的特性,可以查到在 babel6 且 corejs2 以上支持,在 chrome 中是 49 以上支持,chrome48 中还是实验特性,在 node6 以上支持,等等。

光是这些数据还不够,electron 有自己的版本,要支持 electron 得需要 electron 版本和它用的 chromuim 的版本的对应关系。

万幸有 electron-to-chromium 这个项目,它维护了 electron 版本到 chromium 版本的映射关系

也可以反过来查询 chromium 版本在哪些 electron 版本中使用

有了这些数据,我们就能知道每一个特性在哪些环境的什么版本支持。

babel7 在 @babel/compat-data 这个包里面维护了这种特性到环境支持版本的映射关系,包括 plugin 实现的特性的版本支持情况(包括 transform 和 proposal ),也包括 corejs 所 polyfill 的特性的版本支持情况

比如:

这样我们就知道每一个特性是在什么环境中支持的了,接下来只要用户指定一个环境,我们就能做到按需转换!

browserslist

那开发者怎么指定环境呢?

让开发者写每个环境的版本是啥肯定不靠谱,这时候就要借助 browerslist 了,它提供了一个从 query (查询表达式) 到对应环境版本的转换。

比如我们可以通过 last 1 version 来查询最新的各环境的版本

也可以通过 supports es6-module 查询所有支持 es module 的环境版本

具体查询的语法有很多,可以去 browserslist 的 query 文档中学习,这里就不展开了。

@babel/preset-env

现在有了什么特性在什么环境版本中支持,有了可以通过 query 指定目标环境版本的工具,那么就可以上手改了,从都转成 es5 到根据目标环境确定不支持的特性,只转换这部分特性,这就是 @babel/preset-env 做的事情。

有了 @babel/compat-data 的数据,那么只要用户指定他的目标环境是啥就可以了,这时候可以用 browserslist 的 query 来写,比如 last 1 version, > 1% 这种字符串,babel 会使用 brwoserslist 来把它们转成目标环境具体版本的数据。

有了不同特性支持的环境的最低版本的数据,有了具体的版本,那么过滤出来的就是目标环境不支持的特性,然后引入它们对应的插件即可。这就是 preset-env 做的事情(按照目标环境按需引入插件)。

配置方式比如:

javascript
{
    "presets": [["@babel/preset-env", { "targets": "> 0.25%, not dead" }]]
}

这样就通过 preset-env 解决了多转换了目标环境已经支持的特性的问题。

其实 polyfill 也可以通过 targets 来过滤。

不再手动引入 polyfill,那么怎么引入? 当然是用 preset-env 自动引入了。但是不是默认就会启用这个功能,需要配置。

javascript
{
    "presets": [["@babel/preset-env", { 
        "targets": "> 0.25%, not dead",
        "useBuiltIns": "usage",// or "entry" or "false"
        "corejs": 3
    }]]
}

配置下 corejs 和 useBuiltIns。

  • corejs 就是 babel 7 所用的 polyfill,需要指定下版本,corejs 3 才支持实例方法(比如 Array.prototype.fill )的 polyfill。

  • useBuiltIns 就是使用 polyfill (corejs)的方式,是在入口处全部引入(entry),还是每个文件引入用到的(usage),或者不引入(false)。

配置了这两个 option 就可以自动引入 polyfill 了。

@babel/preset-env 的配置

这个包的配置比较多,首先我们要指定的是 targets,也就是 browserslist 的 query,这个同样可以在 .browserslistrc 的配置文件中指定(别的工具也可能用到)。

具体有啥配置可以看 @babel/preset-env 的文档,这里简单讲几个:

targets

targets 是指定编译的目标环境的,可以配 query 或者直接指定环境版本(query 的结果也是环境版本)。

环境有这些:

chrome, opera, edge, firefox, safari, ie, ios, android, node, electron

可以指定 query:

javascript
{
  "targets": "> 0.25%, not dead"
}

也可以直接指定环境版本;

javascript
{
  "targets": {
    "chrome": "58",
    "ie": "11"
  }
}
include & exclude

通过 targets 的指定,babel 会自动引入一些插件,但如果觉得自动引入的不大对,也可以手动指定。

当需要手动指定要 include 或者 exclude 什么插件的时候可以使用这个 option。

不过这个只是针对 transform plugin,对于 proposal plugin,要在 plugins 的 option 单独引入。

一般情况下用 preset-env 自动引入的就可以了。

modules

babel 转换代码自然会涉及到模块语法的转换。

modules 就是指定目标模块规范的,取值有 amd、umd、systemjs、commonjs (cjs)、auto、false。

  • amd、umd、systemjs、commonjs (cjs) 这四个分别指定不同的目标模块规范

  • false 是不转换模块规范

  • auto 则是自动探测,默认值也是这个。

其实一般这个 option 都是 bundler 来设置的,因为 bundler 负责模块转换,自然知道要转换成什么模块规范。我们平时就用默认值 auto 即可。

auto 会根据探测到的目标环境支持的模块规范来做转换。依据是在 transform 的时候传入的 caller 数据。

javascript
babel.transformFileSync("example.js", {
  caller: {
    name: "my-custom-tool",
    supportsStaticESM: true,
  },
});

比如在调用 transformFile 的 api 的时候传入了 caller 是支持 esm 的,那么在 targets 的 modules 就会自动设置为 esm。

debug

我们知道 preset-env 会根据 targets 支持的特性来引入一系列插件。

想知道最终使用了啥插件,那就可以把 debug 设为 true,这样在控制台打印这些数据。

比如

javascript
const sourceCode = `
  import "core-js";
  new Array(5).fill('111');
`;

const { code, map } = babel.transformSync(sourceCode, {
    filename: 'a.mjs',
    targets: {
        browsers: 'Chrome 45',
    },
    presets: [
        ['@babel/env', {
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ]
});

设置 debug 为 true,会打印 targets 和根据 tragets 过滤出的的 plugin 和 preset:

@babel/preset-env: `DEBUG` option

Using targets:
{
  "chrome": "45"
}

Using modules transform: auto

Using plugins:
  proposal-numeric-separator { chrome < 75 }
  proposal-logical-assignment-operators { chrome < 85 }
  proposal-nullish-coalescing-operator { chrome < 80 }
  proposal-optional-chaining { chrome }
  proposal-json-strings { chrome < 66 }
  proposal-optional-catch-binding { chrome < 66 }
  transform-parameters { chrome < 49 }
  proposal-async-generator-functions { chrome < 63 }
  proposal-object-rest-spread { chrome < 60 }
  transform-dotall-regex { chrome < 62 }
  proposal-unicode-property-regex { chrome < 64 }
  transform-named-capturing-groups-regex { chrome < 64 }
  transform-async-to-generator { chrome < 55 }
  transform-exponentiation-operator { chrome < 52 }
  transform-function-name { chrome < 51 }
  transform-arrow-functions { chrome < 47 }
  transform-classes { chrome < 46 }
  transform-object-super { chrome < 46 }
  transform-for-of { chrome < 51 }
  transform-sticky-regex { chrome < 49 }
  transform-unicode-regex { chrome < 50 }
  transform-spread { chrome < 46 }
  transform-destructuring { chrome < 51 }
  transform-block-scoping { chrome < 49 }
  transform-new-target { chrome < 46 }
  transform-regenerator { chrome < 50 }
  proposal-export-namespace-from { chrome < 72 }
  transform-modules-commonjs
  proposal-dynamic-import
corejs3: `DEBUG` option

Using targets: {
  "chrome": "45"
}

Using polyfills with `usage-global` method:
regenerator: `DEBUG` option

Using targets: {
  "chrome": "45"
}

Using polyfills with `usage-global` method:

  When setting `useBuiltIns: 'usage'`, polyfills are automatically imported when needed.
  Please remove the direct import of `core-js` or use `useBuiltIns: 'entry'` instead.

[/Users/zhaixuguang/code/research/babel/a.mjs]
Based on your code and targets, the corejs3 polyfill did not add any polyfill.

[/Users/zhaixuguang/code/research/babel/a.mjs]
Based on your code and targets, the regenerator polyfill did not add any polyfill.

用到了哪些插件一目了然,开发时可以开启这个配置项。

我们知道了 preset-env 能够根据目标环境引入对应的插件,最终会注入 helper 到代码里,但这样还是有问题的:

从 helper 到 runtime

preset-env 会在使用到新特性的地方注入 helper 到 AST 中,并且会引入用到的特性的 polyfill (corejs + regenerator),这样会导致两个问题:

  • 重复注入 helper 的实现,导致代码冗余
  • polyfill 污染全局环境

解决这两个问题的思路就是抽离出来,然后作为模块引入,这样多个模块复用同一份代码就不会冗余了,而且 polyfill 是模块化引入的也不会污染全局环境。

使用 transform-runtime 之前:

使用 transform-runtime 之后:

这个逻辑是在 @babel/plugin-transform-runtime 包里实现的。它可以把直接注入全局的方式改成模块化引入。

比如使用 preset-env 的时候是全局引入的:

当引入 @babel/plugin-transform-runtime 就可以模块化引入:

这样就不再污染全局环境了。

babel7 通过 preset-env 实现了按需编译和 polyfill,还可以用 plugin-transform-runtime 来变成从 @babel/runtime 包引入的方式。

但这也不是完美的,还有一些问题:

babel7 的问题

我们先来试验一下:

看一下 Array.prototype.fill 的环境支持情况:

可以看到在 Chrome 45 及以上支持这个特性,而在 Chrome 44 就不支持了。

我们先单独试一下 preset-env:

当指定 targets 为 Chrome 44 时,应该自动引入polyfill:

当指定 targets 为 Chrome 45 时,不需要引入polyfill:

结果都符合预期,44 引入,45 不引入。

我们再来试试 @babel/plugin-transform-runtime:

是不是发现问题了,Chrome 45 不是支持 Array.prototype.fill 方法么,为啥还是引入了 polyfill。

因为 babel 中插件的应用顺序是:先 plugin 再 preset,plugin 从左到右,preset 从右到左,这样 plugin-transform-runtime 是在 preset-env 前面的。等 @babel/plugin-transform-runtime 转完了之后,再交给 preset-env 这时候已经做了无用的转换了。而 @babel/plugin-transform-runtime 并不支持 targets 的配置,就会做一些多余的转换和 polyfill。

这个问题在即将到来的 babel8 中得到了解决。

babel8

babel8 提供了 一系列 babel polyfill 的包 ,解决了 babel7 的 @babel/plugin-transform-runtime 的遗留问题,可以通过 targets 来按需精准引入 polyfill。

babel8 支持配置一个 polyfill provider,也就是说你可以指定 corejs2、corejs3、es-shims 等 polyfill,还可以自定义 polyfil。

有了 polyfill 源之后,使用 polyfill 的方式也把之前 transform-runtime 做的事情内置了,从之前的 useBuiltIns: entry、 useBuiltIns: usage 的两种,变成了 3 种:

  • entry-global: 这个和之前的 useBuiltIns: entry 对标,就是全局引入 polyfill。

  • usage-entry: 这个和 useBuiltIns: usage 对标,就是具体模块引入用到的 polyfill。

  • usage-pure:这个就是之前需要 transform-runtime 插件做的事情,使用不污染全局变量的 pure 的方式引入具体模块用到的 polyfill.

其实这三种方式 babel 7 也支持,但是 babel8 不再需要 transform-runtime 插件了,而且还支持了 polyfill provider 的配置。

babel 的功能都是通过插件完成的,但是直接指定插件太过麻烦,所以设计出了 preset,我们学习 babel 的内置功能基本等价于学习 preset 的使用。主要是 preset-env、preset-typescript 这些。

但是一些 proposal 的插件需要单独引入,并且 @babel/plugin-transform-runtime也要单独引入。

学习内置功能的话 preset 是重点,但是最终完成功能的还是通过插件。

总结

上一节我们基于 plugin 和 preset 已经能够完成 esnext 等代码转目标环境 js 代码的功能,但是还不完美。

这一节我们介绍了 @babel/preset-env,它基于每种特性的在不同环境的最低支持版本的数据和配置的 targets 来过滤插件,这样能减少很多没必要的转换和 polyfill。

如果希望把一些公共的 helper、core-js、regenerator 等注入的 runtime 函数抽离出来,并且以模块化的方式引入,那么需要用 @babel/plugin-transform-runtime 这个包。

@babel/plugin-transform-runtime 不支持根据 targets 的过滤,和 @babel/preset-env 配合时有问题,这个在 babel8 中得到了解决。babel8 提供了很多 babel polyfill 包,支持了 polyfill provider 的配置,而且还可以选择注入方式。不再需要 @babel/plugin-transform-runtime 插件了。

学完这一节,我们知道了 babel 如何基于 targets 的配置做到精准的转换,我们平时开发主要是使用 preset,了解下 preset 设计和演变还是很有意义的。