回顾 babel 6和7,来预测下 babel 8

13,328 阅读10分钟

babel 最开始叫 6to5,顾名思义,功能是 es6 转 es5。我们知道,es 版本一年一个,有了 es7(es2016)、es8(es2017)等等。显然,6to5 的名字已经不合适了,所以 6to5 改名为了 babel。

babel 来自巴别塔的典故:

当时人类联合起来兴建希望能通往天堂的高塔,为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。此事件,为世上出现不同语言和种族提供解释。这座塔就是巴别塔。

这个巴别塔的典故很符合 babel 的转译器的定位。

babel 的编译流程

babel 从最初到现在一直的目的都很明确,就是把源码中的新语法和 api 转成目标浏览器支持的。它采用了微内核的架构,整个流程比较精简,所有的转换功能都是通过插件来完成的。

babel 的编译流程就是 parse、transform、generate 3步, parse 是把源码转成 AST,transform 是对 AST 的转换,generate 是把 AST 转成目标代码,并且生成 sourcemap。

在 transform 阶段,会应用各种内置的插件来完成 AST 的转换。内置插件做的转换包括两部分,一是把不支持的语法转成目标环境支持的语法来实现相同功能,二是不支持的 api 自动引入对应的 polyfill。

babel 的编译流程和目的从没有变过,但是完成这个目的的方式却变化很大,我们来回顾一下 babel 6babel 7 都是怎么设计的,babel 8 又会怎么做,或许能帮你真正理解 babel。

babel 6

es 的标准一年一个版本,也就意味着 babel 插件要实时的去跟进,一年实现一系列插件。

新的语法和 api 进入 es 标准也是有个过程的,这个过程分为这几个阶段:

  • 阶段 0 - Strawman: 只是一个想法,可能用 babel plugin 实现
  • 阶段 1 - Proposal: 值得继续的建议
  • 阶段 2 - Draft: 建立 spec
  • 阶段 3 - Candidate: 完成 spec 并且在浏览器实现
  • 阶段 4 - Finished: 会加入到下一年的 es20xx spec

有这么多特性要 babel 去转换,每个特性用一个 babel 插件来做。但是特性多啊,也就是说插件多,总不能让用户自己去配一个个插件吧,所以 babel 6 引入了 preset 的概念,就是 plugin 的集合。

如果我们想用 es6 语法就用 babel-preset-es2015,es7 就在引入 babel-preset-es2016 等等。如果是想用还没加入标准的特性,则分别用 babel-preset-stage0、babel-preset-stage1 等来引入。这样通过选择不同的 preset,加上手动引入一些插件,就是所有 babel 会做的转换。

可以把这个过程理解为集合求并集的过程。

并集的结果就是所有支持的特性。

babel 6 就是通过这样的方式来支持各种目标环境不支持的特性转换的配置。

细想一下,这样的方式有没有问题?

这样虽然能达到目的,但是是有问题的,主要有两点:

  • es 的标准每年都在变,现在的 stage-0 可能很快就 stage-2 了,那 preset 怎么维护,要不要跟着变,用户怎么知道这个 stage-x 都支持什么特性?

  • 只能转成 es5,那目标环境支持一些 es6 特性了,那这些转换和 polyfill 岂不是无用功? 而且还增加了产物的体积。

  • polyfill 手动引入,比较麻烦,有没有更好的方式

这两个问题是 babel 6 的时候一直存在的。所以这种方案算是及格,但是还是有问题的,我们给 70 分不过分吧。 (能完成功能就可以给 60 分,多加 10 分是给 babel 6 引入的 preset,确实简化了很多配置)

那怎么解决 babel 6 的问题呢?babel 7 给出了答案。

babel 7

babel 7 改动挺大的,比如所有的包都迁移到了 @babel 的 scope 下,也就是 @babel/xxx,这些我们不管,只看 babel 7 是怎么解决 babel 6 的问题的,

babel 7 废弃了 preset-20xx 和 preset-stage-x 的 preset 包,而换成了 preset-env,preset-env 默认会支持所有 es 标准的特性,如果没进入标准的,不再封装成 preset,需要手动指定 plugin-proposal-xxx。

它的集合是这样的:

是不是比起 babel 6 更简单了。

(preset-react 等不是 es 标准语法,也没有啥变化,就不包括在里面了)。

但是 preset 和 plugin proposal 的改变只是解决了之前的 preset 经常变的问题。那么多转换了一些环境支持的特性,这个问题是怎么解决的呢?

答案是 compat-table,它给出了每个特性在不同浏览器或者 node 环境中的最低支持版本,babel 基于这个自己维护了一份数据库,在 @babel/compat-data 下。

其中有每个特性在不同环境的什么版本支持的数据:

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

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

配置方式比如:

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

这样就通过 preset-env 解决了转换了目标环境已经支持的特性的问题。其实 polyfill 也可以通过 targets 来过滤。

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

{
    "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 了。

polyfill 默认是全局引入的,有的时候不想污染全局变量就要用 @babel/plugin-transform-runtime 转换下。(这个插件 babel 6 就有了)。

这样就不再污染全局环境了,而是使用一个唯一的标识符来引入。

看起来,babel 7 好像已经很完美了,可以打 90 多分了?

不是的,babel 7 有 babel 7 的问题。

babel 7 的问题

@babel/plugin-transform-runtime 是不支持配置 targets 的,因为不知道目标环境支持啥,它只能全部做转换。你可能说不是有 preset-env 么?

babel 中插件的应用顺序是:先 plugin 再 preset,plugin 从左到右,preset 从右到左,这样 plugin-transform-runtime 是在 preset-env 前面的。

等 @babel/plugin-transform-runtime 转完了之后,再交给 preset-env 这时候已经做了无用的转换了。

我们来试验一下:

我们先看一下 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。

于是我就去问了下作者,提了个 feature request,作者说可以用最新的 babel polyfill 系列包解决了这个问题.

我去看了下,这个包还在试验阶段,确实解决了这个问题。

这个包估计在 babel 8 会内置到 babel。

那么给 babel 7 打个分吧,本来 preset-env 的引入使我们能更精准的转换代码和引入 polyfill,想给 90 分,但是 plugin-transform-runtime 的问题让我给它减了 10 分,综合给 80 分吧。

babel 8

babel 8 还没出来,但是我们知道 babel 再怎么更新也是围绕主线来的,也就是对目标环境不支持的特性自动进行精准的转换和 polyfill。每个版本都是解决了上个版本的问题的,babel 8 的 @babel/polyfills 包就解决了 babel 7 的 @babel/plugin-transform-runtime 的遗留问题,可以通过 targets 来按需精准引入 polyfill 了。

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

然后有了 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 也支持,但是现在不再需要插件了,而且还支持了 polyfill provider 的配置,所以到了 babel 8 的阶段, @babel/preset-env 才是功能完备的。

那么插件如果想用 targets 该怎么用呢?

因为我最近在写 《babel 插件通关秘籍》 的小册,所以比较关注对插件的影响,我就问了一下 babel 维护者是不是需要在 @babel/core 调用插件的时候注入到 api 中,让插件可以拿到 targets。

上午问的,下午我就惊喜的发现 babel 文档补充了 @babel/helper-compilation-targets 的文档。helper 是用于插件之间复用代码的方式,也就是给插件开发用的库。

我看了下,这个库提供了 3 个 api:

  • 根据 query 查询目标环境版本: getTargets
  • 过滤目标环境: filterItems
  • 判断某个插件是否需要:isRequired

分别对应我们前面聊到的需要 先通过 query 确定目标环境,然后对目标环境做过滤,之后判断某个插件是否需要的 3个阶段。

插件里面通过 api.targets() 拿到环境的配置,然后通过 isRequired 来确定某个插件有没有必要用。

这样,不管是内置 plugin 和 preset 的实现方式也好,还是插件所能用的 api 也好,都完美支持了 targets,到了这个阶段 targets 才算真正融入进了 babel 中。

这个阶段的 babel,我觉得已经可以给出 90 分的分数了:

支持按照配置的目标环境按需进行 polyfill 和 transform,支持 polyfill 的切换和自定义,配置方式也足够简单,插件中也可以用 targets,而且提供了方便的 helper 包。

babel 发展规律

babel 8 还在路上,但是我们已经能够隐约看到他会是什么样子了,其实 babel 从最开始到现在,核心的思路始终没有变过,就像最开始的名字 6to5 一样,就是为了 把目标环境中不支持的语法和 api 进行转换或 polyfill,尽量的准确、配置尽量的简单、插件更容易书写能做到更多事情

所以针对这个目标,babel 一路发展而来, 设计出了 preset(babel 6)、preset-env (babel 7)、polyfill provider(babel 8),plugin-transform-runtime (babel 6)等。

插件能够用的 api、helper 等也越来越丰富。

babel 一直在发展,但是目标和本质从未变过。我们去学习一个东西,也要去抓住它的本质来学,所以我写了《babel 插件通关秘籍》 的小册(即将上线),希望能帮你“通关” babel!