babel 到底该怎么配置

330 阅读8分钟

babael 是什么

官网上说,babel 是一个工具链,主要用来对 es5+ 的语法做向后兼容,以便我们能在编码的过程中愉快的使用 js 的新特性,而不用担心坑爹的老旧版浏览器执行出错。

官方大大还列了一下 babel 干的具体的事儿:

  1. 语法转换

    这个语法转换实际来讲,就是语法糖的逆向工程反糖化,被举得最多的就是 es6 真香系列的箭头函数:

    // 转换前
    () => {
      console.log('babel is a der')
    }
    
    // 转换后
    (function () {
      console.log('babel is a der')
    })
    
  2. API 兼容

    官方说的是,通过 Polyfill 方式在目标环境中添加缺失的特性,我更倾向于理解成就是对一些 API 的补充兼容。这里的 Polyfill 也是一个插件包,这个包是专门针对 API 的。

    有的同学可能听的就有点迷,我一直在强调 API ,那我嘴里讲的 API 兼容到底是个啥呢?

    比如说,在 es6 之前,Array 原始对象和 Object 原始对象上,是没有像 Array.formObject.assign 这些静态方法的,什么是没有?就是 Array.form 会提示.formundefined,即当前 js 环境下不存在这个东西。

    那么 Polyfill 干了啥呢,Polyfill 可以在代码中给你塞个垫片进去:

    // 转换前
    Array.from('babel is a der')
    
    // 转换后
    var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
    var _from = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/from"));
    
    (0, _from.default)('babel is a der')
    

    看到没有,它相当于是给你手动引入了一个实现,就是你没有就给你塞一个进去,然后再让你去执行,这就是垫片。

  3. 源码转换

    babel 代码转换的核心,其实就是通过 AST 语法树来实现的,至于 AST 语法树的相关知识,我们在这里不做过多展开。

Babel 到底要怎么配置 ?

这是一个最初令我异常头疼的问题,它里面由于历史原因夹杂着各种错综复杂的线,很难分清楚什么是什么。今天,我们就来把它有条有理的归个类。

在讲这个之前,我们还得补充一个知识点,那就是,babel 所有的转换工作,都是依赖与插件包的,它甚至将自己的核心库 babel-core 都解耦出去变成了一个插件包。

presets 式配置

闲话少说,咱们先上代码:

// 包安装
~ yarn add @babel/core -D // 核心转换插件
~ yarn add @babel/preset-env -D // 语法转化插件集成库
~ yarn add core-js@3 // API 转化插件集成库(presets 模式下)
~ yarn add @babel/plugin-transform-runtime -D // 插件模块化工具,优化垫片 inject,全局变量污染

/*
 * 注:`core-js@2` 分支中已经不会再添加新特性,新特性都会添加到 `core-js@3`
*/

// .babelrc.json 文件
{
 "presets": [
   ["@babel/preset-env", {
     "useBuiltIns": "usage",
     "corejs": 3
   }]
 ],
 plugins: ["@babel/plugin-transform-runtime"]
}

// .browserslistrc 文件0.25%
not dead

安装包

首先,我们先来看一下我们需要的安装包

@babel/core

core-js 其实就是 @babel/polyfill 的后代产物,core-js npm 上的定义

  • 它是 js 的一个标准模块化库
  • 它包含了 ECMAScript 2021 年最新更新的 PromiseSymbolCollectionIterator 等一系列的最新特性,也就是 API
  • 另外还包含 WHATWG/W3C 关于跨平台的一些提案。

所以,我们不难看出,core-js 这个包,干的就是以前 @babel/polyfill 干的活儿

core-js 是以预设配置项的形式配置在 env 包的配置项里的,它是以 env 包的工具形式存在的,也就是说,在进行语法转换的时候,env 包自己会转换新语法糖,API 的转换则是通过控制调用 core-js 去实现的。

@babel/preset-env

预设选项,它是一个插件包,它里面集成了一系列的语法转换依赖插件,用来转换像 () => {}2 ** 3async { await } 等新语法糖。

targets

首先,这个 targets 是干什么的呢 ?

babel 官网对它的定义是,指定目标环境。也就是说,这个参数是让开发者告诉 babel ,我们的代码最后是要放到哪些浏览器环境下去执行的,这样 babel 才能根据目标环境去判断出当前需要兼容的特性,然后按需去引入单个插件。

这个参数在上面的配置里没有出现,但是并不是说没有用到它,我们配置文件里单独的 .browserslistrc 文件,就是对它的一个配置使用。它其实是可以以参数的形式存在:

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

官方推荐是使用 .browserslistrc 文件去指明编译的 target,因为这个配置文件还可以和autoprefixerstylelint等工具一起共享配置。

presets

注释:@babel/preset-env 后文简称 env

presets 在配置里叫做插件预设配置,它的可选参数目前就只有一个,那就是 envenv 是一个插件包,它里面集成了一系列的语法转换依赖插件,用来转换像 () => {}2 ** 3async { await } 等新语法糖。

[
  ["@babel/preset-env", {
    "useBuiltIns": "usage",
    "corejs": 3
  }]
]

上面的这种写法,其实是对 env 做的一个单独的配置,这两个配置的作用是告诉 env 怎么去处理 polyfills 的。

在讲 useBuiltIns 之前,我们还得去补一个知识点,那就是 env 的另一个参数,

经过多方取证,我发现,@babel/preset-env 这个包,其实还是偏语法转换的,API 兼容它其实还是做不了,证据是,在 presets 式配置里,必须要安装core-js 这个依赖,我用 babel-cli 单独打包出来的文件里,兼容插件也都是从 core-js 这个包里导出来的。

好,那么 core-js 这个包,到底是个啥呢 ?

useBuiltIns

useBuiltIns 一共有 3 个参数:falseentryusage, 默认为 false

首先,useBuiltIns 正在有实际作用的前提是,我们引入过 core-js

  1. 当参数为 false 时不做任何处理;

  2. 当参数为 entry 时,我们需要在入口文件开头处手动引入 core-jsregenerator-runtime ,然后,env 会根据我们配置的目标浏览器环境把 core-js 进行拆包,再将当前环境下需要的垫片在入口文件处全部引入;

    这个配置会有两个问题,1 是包冗余会比较严重,很多包引入进来后其实没有发挥真正的作用;2 是垫片会直接加进全局变量或对象原型,造成比较严重的变量污染。

    ~ yarn add regenerator-runtime
    
    // index.js 入口文件
    import "core-js/stable"
    import "regenerator-runtime/runtime"
    
  3. 当参数为 usage 时,不需要手动引入 core-jsenv 会在解析过程中会对单个文件进行分析,然后按需导入当前文件需要的包。

    这个配置解决了包冗余的问题,但是变量污染依然存在。

plugin-transform-runtime

plugin-transform-runtimebabel 基于 useBuiltIns 中所出现的问题,给出的解决方案,首先,plugin-transform-runtime 是一个插件,需要配置在配置在配置文件的 plugins 选项里,然后,plugin-transform-runtime 具体干了以下两件事:

  1. 将垫片的直接注入改为了引用模式,也就是说,当多个文件同时用到一个垫片时,就不会有多次代码注入,而是将该垫片模块化后保存在 @babel/runtime/helpers/ 下的空间里,然后在需要的文件里引入。

  2. 模块化解决了全局变量污染问题,并且提供了不污染原型的解决方案。

plugins 式配置

同样,我们先上代码

// 包安装
 ~ yarn add @babel/core -D
 ~ yarn add @babel/preset-env -D
 ~ yarn add @babel/runtime-corejs3 // 作用同 presets 模式下的 core-js@3
 ~ yarn add @babel/plugin-transform-runtime -D
 
// .babelrc.json 文件
{
  "presets": ["@babel/preset-env"]
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs" 3
    }]
  ]
}

plugins 式配置的 .browserslistrc 文件不用修改,它与 presets 最大的差别是,将 core-js 的处理权交给了 plugin-transform-runtime 插件。

语法部分的转换当然还是交给了 env 下面我来着重看看 plugins

plugins

plugins 接收一个数组作为参数,数组里装的是单个插件,也就是说,plugins 是更细粒度化的插件配置,plugins 不同于 presets 的是:

  1. plugins 的路子更广,可以配置一些构建层的工具;
  2. plugins 执行的顺序是优先于 presets 的。

plugin-transform-runtime

presets 式配置方案里,我们聊过这个插件,但是它的其实还能通过参数配置,去做一些扩展,即,它也能处理 core-js。扩展的方法也很简单,只要给 plugin-transform-runtime 配置上 corejs 即可。

runtime-corejs3

plugin-transform-runtime 配置 core-js 时,不能使用 presets 配置里的 core-js@3 这个包了,它依赖的是 @babel/runtime-corejs3 这个包。

几点补充

给参数添加配置项

presetsplugins 里,如果要给参数单独添加配置,首先要把参数占位变成一个数组,然后再将原有参数放在该数组的第一位,在第二位上补一个对象,然后再将配置写在对象里:

// 配置前
{
  "presets": ["@babel/preset-env"]
}

// 配置后
{
  "presets": ["@babel/preset-env", {
    "useBuiltIns": "usage",
    "corejs": 3
  }]
}

配置的执行顺序

plugins 的配置会在 presets 前运行;plugins 里的配置会从前往后执行,presets 里的配置会从后往前执行。

参考文献:

  1. 一口(很长的)气了解 babel

  2. 不容错过的 Babel7 知识

  3. 「前端基建」带你在Babel的世界中畅游

  4. [译]Babel文档之@babel/preset-env

  5. core-js@3, babel展望未来