前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你可能并没有理解的 babel 配置的原理

你可能并没有理解的 babel 配置的原理

作者头像
神说要有光zxg
发布2022-06-06 08:42:33
5090
发布2022-06-06 08:42:33
举报
文章被收录于专栏:神光的编程秘籍

babel 是一个 JS、TS 的编译器,它能把新语法写的代码转换成目标环境支持的语法的代码,并且对目标环境不支持的 api 自动 polyfill。

babel 基本每个项目都用,大家可能对 @babel/preset-env 和 @babel/plugin-transform-runtime 都很熟悉了,但是你真的理解它们么?

相信很多同学只是知道它能干什么,但不知道它是怎么实现的,这篇文章我们就来深入下它们的实现原理吧。

首先,我们先来试一下 preset-env 和 plugin-transform-runtime 的功能:

功能测试

@babel/preset-env 的作用是根据 targets 的配置引入对应插件来实现编译和 polyfill。

比如这段代码:

代码语言:javascript
复制
class Dong {
}

在低版本浏览器不支持,会做语法转换。

我们把 targets 指定成比较低版本的浏览器,比如 chrome 30,并且打开 debug 选项,它的作用是会打印用到的 plugin。

代码语言:javascript
复制
{
    presets: [
        ['@babel/preset-env', {
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ]
}

执行 babel 就会发现它用到了这些插件:

这就是 @babel/preset-env 的意义,自动根据 targets 来引入需要的插件,不然要是手动写这么一堆插件不得麻烦死。

开启 polyfill 功能要指定它的引入方式,也就是 useBuiltIns。设置为 usage 是在每个模块引入用到的,设置为 entry 是统一在入口处引入 targets 需要的。

polyfill 的实现就是 core-js,需要再指定下 corejs 版本,一般是指定 3,这个会 polyfill 实例方法,而 corejs2 不会。

上面一段代码会转换成这样:

注入了 3 个 helper,也就是 _createClass 这种以下划线开头的辅助方法。

因为 helper 方法里用到了 Object.defineProperty 的 api,这里也会从 core-js 里引入。

我们再测试一下这样一段代码:

代码语言:javascript
复制
async function func() {
}

会被转换成这样:

除了注入 core-js、helper 代码外,还注入了 regenerator 代码,这个是 async await 的实现。

综上,babel runtime 包含的代码就 core-js、helper、regenerator 这三种。

@babel/preset-env 的处理方式是 helper 代码直接注入、regenerator、core-js 代码全局引入。

这样就会导致多个模块重复注入同样的代码,会污染全局环境。

解决这个问题就要使用 @babel/plugin-transform-runtime 插件了。

我们在配置文件里引入这个插件:

代码语言:javascript
复制
{
    presets: [
        ['@babel/preset-env', {
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ],
    plugins: [
        ['@babel/plugin-transform-runtime', {
            corejs: 3
        }]
    ]
}

注意,这个插件也是处理 polyfill ,也就同样需要指定 corejs 的版本。

然后测试下引入之后有什么变化:

先测试 class 那个案例:

之前是这样的:

现在变成了这样:

变成了从 @babel/runtime-corejs3 引入的形式,这样就不会多个模块重复注入同样的实现代码了,而且 core-js 的 api 也不是全局引入了,变成了模块化引入。

这样就解决了 corejs 的重复注入和全局引入 polyfill 的两个问题。

再测试 async function 那个案例:

之前是这样的:

同样有全局引入和重复注入的问题。

引入 transform-runtime 插件之后是这样的:

也是同样的方式解决了那两个问题。

再来测试一个 api 的,用这样一段代码:

代码语言:javascript
复制
new WeakMap();

当只配置 preset-env 时:

代码语言:javascript
复制
{
    presets: [
        ['@babel/preset-env', {
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ]
}

结果是这样的:

再加上 @babel/plugin-transform-runtime 后:

代码语言:javascript
复制
{
    presets: [
        ['@babel/preset-env', {
            targets: 'chrome 30',
            debug: true,
            useBuiltIns: 'usage',
            corejs: 3
        }]
    ],
    plugins: [
        ['@babel/plugin-transform-runtime',
            {
                corejs: 3
            }
        ]
    ]
}

结果是这样的:

这样我们就清楚了 @babel/plugin-transform-runtime 的功能,把注入的代码和 core-js 全局引入的代码转换成从 @babel/runtime-corejs3 中引入的形式。

@babel/runtime-corejs3 就包含了 helpers、core-js、regenerator 这 3 部分。

功能我们都清楚了,那它们是怎么实现的呢?

实现原理

preset-env 的原理之前讲过,就是根据 targets 的配置查询内部的 @babe/compat-data 的数据库,过滤出目标环境不支持的语法和 api,引入对应的转换插件。

targets 使用 browserslist 来解析成具体的浏览器和版本:

然后根据 @babel/compact-data 的数据来过滤出这些浏览器支持的语法和 api:

然后去掉这些已经支持的语法和 api 对应的插件,剩下的就是需要用的转换插件:

这就是 preset-env 的根据 targtes 来按需转换语法和 polyfill 的原理。

那 @babel/plugin-transform-runtime 呢?它是怎么实现的?

这个插件的原理是因为 babel 插件和 preset 生效的顺序是这样的(下面是官网文档的截图):

先插件后 preset,插件从左往右,preset 从右往左。

这就导致了 @babel/plugin-transform-runtime 是在 @babel/preset-env 之前调用的,提前做了 api 的转换,那到了 @babel/preset-env 就没什么可转了,也就实现了 polyfill 的抽取。

它的源码是这样的:

会根据配置来引入 corejs、regenerator 的转换插件,实现 polyfill 注入的功能。

并且还设置了一个 helperGenerator 的函数到全局上下文 file,这样后面 @babel/preset-env 就可以用它来生成 helper 代码。那自然也就是抽离的了。

这就是 @babel/plugin-transform-runtime 的原理:

因为插件在 preset 之前调用,所以可以提前把 polyfill 转换了,而且注入了 helpGenerator 来修改 @babel/preset-env 生成 helper 代码的行为。

原理我们理清了,但是大家有没有发现其中的问题:

现有方案的问题

我们通过 @babel/plugin-transform-runtime 提前把 polyfill 转换了,但是这个插件里没有 targets 的设置呀,不是按需转换的,那就会多做一些没必要的转换。

这个其实是已知问题,可以在 babel 的项目里找到这个 issue:

当然官方也提出了解决的方案,只不过这个得等 babel 新版本更新再用了,等 babel8 吧。

总结

babel7 以后,我们只需要使用 @babel/preset-env,指定目标环境的 targets,babel 就会根据内部的兼容性数据库查询出该环境不支持的语法和 api,进行对应插件的引入,从而实现按需的语法转换和 polyfill 引入。

但是 @babel/preset-env 转换用到的一些辅助代码(helper)是直接注入到模块里的,没有做抽离,多个模块可能会重复注入。并且用到的 polyfill 代码也是全局引入的,可能污染全局环境。为了解决这两个问题我们会使用 @babel/plugin-transform-runtime 插件来把注入的代码抽离,把全局的引入改为从 @babel/runtime-corejs3 引入的方式。

runtime 包包含 core-js、regenerator、helper 三部分。

@babel/plugin-transform-runtime 能生效的原理是因为插件先于 preset 被调用,提前把那些 api 做了转换,并且设置了 preset-env 生成 helper 的方式。

但是这个转换和 preset-env 是独立的,它没有 targets 的配置,这就导致了不能按需 polyfill,会进行一些不必要的转换。这个是已知的 issue,等 babel 版本更新吧。

看到这里,你对 babel 的配置和这些配置的原理是否有更深的理解了呢。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神光的编程秘籍 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 功能测试
  • 实现原理
  • 现有方案的问题
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档