编程崽

登录

一叶在编程苦海沉沦的扁舟之上,我是那只激情自射的崽

Babel 历史和原理

Babel 历史和原理

Babel文档:https://babeljs.io/docs/en/

只能说,Babel 的历史太厚重了。

之前的配置中,进行编译使用的是 babel-preset-es2015、babel-preset-stage-2、transform-object-rest-spread 、 @babel/polyfill 等包,现在已经不是这些了,有了新的配置方案,见后面。

以下历史参考自 https://juejin.cn/post/6976501655302832159

core-js

core-js 是 JavaScript 的模块化标准库,包括了 ECMAScript 新 api 的兼容实现。它和 babel 高度集成,是 babel 解决新特性在浏览器中兼容问题的核心依赖。

目前 core-js 的版本是 3.x,与 core-js@2 相比不仅在本身的架构上有重大调整,还对 babel 中的一些插件有重大影响。

这里为了后面方便理解,先说两个名字,一个叫做污染包,一个叫做清洁包,后面马上就会用到。

总之,阅读了下面 的 core-js@2 和 core-js@3 的说明后,会知道 core-js@2 和 core-js@3 各有一个污染包和一个清洁包。

具体污染包和清洁包的实现具体实例,见后面的举例

现阶段能够编译 api 的插件一共有三个,见下:

@babel/polyfill 只能使用 core-js@2 的污染包(@babel/polyfill 包已被 Babel 废弃,不推荐使用)。

@babel/preset-env,根据配置,能使用 core-js@2 或 core-js@3 的污染包。

@babel/plugin-transform-runtime 使用 @babel/runtime-corejs2 或 @babel/runtime-corejs3:

  • @babel/runtime-corejs2 使用 core-js@2 的清洁包。
  • @babel/runtime-corejs3 使用 core-js@3 的清洁包。

core-js@2

core-js@2 被 @babel/polyfill、@babel/preset-env 和 @babel/runtime-corejs2 (最终会由 @babel/plugin-transform-runtime 使用) 引入,来进行新 api 的兼容处理。

其中有两个核心的模块,其实就是一个是污染包,一个清洁包:

  • modules:污染全局的 polyfill 模块,供 @babel/polyfill 和 @babel/preset-env 引入。

    这个包我叫它 污染包,它主要是采用在全局和实例上添加 api 的方式解决兼容性问题,比如要兼容 Array 的 includes 方法,就直接给 Array 的原型链添加这个方法,会修改原型链。

  • library:不污染全局的 runtime 模块,供 @babel/runtime-corejs2 引入。

    这个包我叫它 清洁包,它主要是采用模拟替换api的方式解决兼容性问题,比如发现代码中使用了 [1, 2].includes(2) 方法,就引入一个方法叫做 _includes,然后把代码部分替换为 _includes.call([1, 2], 2) 的方式,不污染原型链。

core-js@3

core-js@3 放弃了对 @babel/polyfill 的支持,被 @babel/preset-env 和 @babel/runtime-corejs3(最终会由 @babel/plugin-transform-runtime 使用) 引入来进行新api的兼容处理。

由于 core-js@2 包的体积太大(约2M),并且有很多重复的文件被引用,所以 core-js@3 对包进行拆分,其中两个核心,仍然一个是污染包,一个清洁包:

  • core-js:污染全局的 polyfill 包,供 @babel/preset-env 引入,等价于 core-js@2/modules,这个包我叫它污染包;
  • core-js-pure:不污染全局的 runtime 包,供 @babel/runtime-corejs3 引入,等价于core-js@2/library,这个包我叫它清洁包。

core-js@2分支已经冻结,不会再添加新特性,新特性都会添加到 core-js@3。

为了可以使用更多的新特性,建议大家使用 core-js@3,但我们其实不会直接使用 core-js@3,我们需要使用的是那些引入了 core-js@3 的插件和配置。

关于 core-js 的内容大家先了解这么多,先有个印象,大家只需要记住一点:corejs 才是 api 兼容实现的提供者!

污染包和清洁包的实际效果举例

使用污染包的编译前后,编译后的使用 require 引入的 js,就是给 Array 添加了 from 这个静态方法:

js 复制代码
// 编译前
const array = Array.from([1, 2, 3, 4, 5])
console.log(array?.[6] || '无')

// 编译后
"use strict";

require("core-js/modules/es6.symbol.js");
require("core-js/modules/es6.array.from.js");
require("core-js/modules/es6.string.iterator.js");
require("core-js/modules/es6.object.to-string.js");
require("core-js/modules/es6.array.iterator.js");
require("core-js/modules/web.dom.iterable.js");
var array = Array.from([1, 2, 3, 4, 5]);
console.log((array === null || array === void 0 ? void 0 : array[6]) || '无');

使用清洁包的编译前后,可见对应编译前的 Array.form 方法,编译后是引入了一个 _from 来替换使用。

js 复制代码
// 编译前
const array = Array.from([1, 2, 3, 4, 5])
console.log(array?.[6] || '无')

// 编译后
"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _from = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/from"));
var array = (0, _from.default)([1, 2, 3, 4, 5]);
console.log((array === null || array === void 0 ? void 0 : array[6]) || '无');

@babel/polyfill

此包已被官方弃用,虽然仍然可以使用,但不建议使用,后面有完全取代的方案。

@babel/polyfill 是一个运行时包,主要是通过核心依赖 core-js@2 来完成对应浏览器不支持的新的全局和实例 api 的添加。

而在升级到 core-js@3 后,如果还要保留 @babel/polyfill 的使用,就要在 @babel/polyfill 中添加 core-js@2 和 core-js@3 切换的选项,这样 @babel/polyfill 中将包含 core-js@2 和 core-js@3 两个包。

出于这个原因,官方决定弃用 @babel/polyfill。

@babel/preset-env

编译语法时,有的环境下可能需要转换几十种不同语法的代码,则需要配置几十个 plugin ,这显然会非常繁琐。

所以,为了解决这种问题,Babel 提供了预设插件机制 preset,preset 中可以 预设置一组插件来便捷的使用这些插件所提供的功能。目前,Babel 官方推荐使用 @babel/preset-env 预设插件。

从 babel@7 开始,已经移除了 @babel/preset-stage-x,所以当前的新版本中直接使用 @babel/preset-env 就行。

@babel/preset-env 主要的作用是用来转换那些已经被正式纳入TC39 中的语法。所以它无法对那些还在提案中的语法进行处理,对于处在 stage 中的语法,需要安装对应的 plugin 进行处理。

意思就是刚刚提出的新语法还无法编译,需要安装语法对应的 plugin 来处理,不过那种太新的语法,不太关注这些的程序员几乎也都不知道。

除了语法转换,@babel/preset-env 另一个重要的功能是对 api 的处理,也就是在代码中引入 polyfill。

但是,@babel/preset-env 默认是不开启处理 api 的功能,只有设置了 useBuiltIns 选项(不为 false )才会开启。

@babel/preset-env 主要还是依赖 core-js 来处理api的兼容性,在升级到 7.4.0 以上的版本以后,既支持 core-js@2,也支持 core-js@3,所以增加了 corejs 的配置来控制所需的版本。

**但注意,根据上面 core-js 的描述,如果使用 @babel/preset-env 来编译API,那只能使用 core-js@2 或 core-js@3 的污染包! **

如果设置了useBuiltIns选项(不为false)就得设置 corejs 版本,否则 babel 将会发出警告。

json 复制代码
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage", // 也可配置成 entry,如果配置为 false 则不需要配置 corejs
        "corejs": 3 // 3: 使用 core-js@3,2: 使用 core-js@2
      }
    ]
  ]
}

下面是 useBuiltIns 的三个值的区别说明。

useBuiltIns 设置为 usage

注意,设置了此项,需要同时设置 corejs 选项。

代码中不需要开发人员主动引入 polyfill 垫片,babel 会自动将代码里已使用到、且 browserslist 环境不支持的 polyfill 导入。

js 复制代码
// 编译前
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)
const instance = new Promise((resolve, reject) => {
  resolve(123)
})

// 编译后
"use strict";

require("core-js/modules/es.array.copy-within.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");

var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new Promise(function (resolve, reject) {
  resolve(123);
});

useBuiltIns 设置为 entry

注意,设置了此项,需要同时设置 corejs 选项。

需要在代码运行之前导入,会将 browserslist 环境不支持的所有 polyfill 都导入。

js 复制代码
// 编译前
import "core-js/stable";
import "regenerator-runtime/runtime";
// 上面两行是需要开发人员手动引入的 polyfill
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)

const instance = new Promise((resolve, reject) => {
  resolve(123)
})

// 编译后
"use strict";

require("core-js/modules/es.symbol.js");
// ... 此处省略400+行代码,全是类似上下的导入语句
require("regenerator-runtime/runtime");

var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new Promise(function (resolve, reject) {
  resolve(123);
});

useBuiltIns 设置为 false

只做了语法转换,不会导入任何 polyfill 进来,并且 corejs 配置将无效。

js 复制代码
// 编译前
const result = [1, 2, 3, 4, 5].copyWithin(0, 3)

const instance = new Promise((resolve, reject) => {
    resolve(123)
})

const shen = result?.a

// 编译后
"use strict";

var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new Promise(function (resolve, reject) {
  resolve(123);
});
var shen = result === null || result === void 0 ? void 0 : result.a;

runtime 和 @babel/plugin-transform-runtime

在使用 @babel/preset-env 提供的全局 api 添加的功能时,由于使用的是污染包,难免会造成文件的体积增加以及api的全局污染。

为了解决这类问题,Babel 提供了另一种 api 转译的方式,引入了 runtime 的概念。

runtime

runtime 的核心思想是以引入方法替换的方式来解决兼容性问题

runtime 包其实有三个:

  • @babel/runtime:只能处理语法替换,且只有在 @babel/preset-env 的帮助下,runtime 包的语法模拟替换功能才会发挥作用,这个包相当于 @babel/preset-env 的语法替换功能的延伸。
  • @babel/runtime-corejs2:相比较 @babel/runtime,增加了 core-js@2 的清洁包,来支持全局构造函数(例如Promise)和静态方法(例如Array.from)兼容。
  • @babel/runtime-corejs3:相比较 @babel/runtime-corejs2,增加了 core-js@3 的清洁包,支持了实例方法的兼容,同时还支持对ECMAScript 提案的 api 进行模拟,例如 [].flat()。

三个包都依赖 helpers、regenerator-runtime 模块来实现语法的替换,helpers中 提供了一些语法模拟的函数,regenerator-runtime 中实现了 async/await 语法的转换。

所以实际使用时,直接使用 @babel/runtime-corejs3 就行,比如要使用数组的 includes 方法,可以像下面这么写:

js 复制代码
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";

_includesInstanceProperty(foo).call(foo, "a");

但这样一个个手动导入很麻烦,这个时候我们就需要借助自动导入插件来帮助我们完成这项工作。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 就是为了方便 @babel/runtime 的使用。通过ast的分析,自动识别并替换代码中的新api,解决手动 require 的烦恼。

@babel/plugin-transform-runtime 是开发依赖,编译时负责处理 @babel/runtime,两者是搭配使用的。

下面是使用 @babel/plugin-transform-runtime 来编译 api 的配置,其中 corejs 选项来配置使用的是 @babel/runtime-corejs2 还是 @babel/runtime-corejs3。

json 复制代码
{
  "presets": [
    [
      "@babel/preset-env"
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

总结

目前,Babel 处理兼容性问题有两种方案:

  • @babel/preset-env + corejs@3:实现语法转换 + 在全局和实例上添加api,支持全量加载和按需加载,我们简称 polyfill 方案;

    • 缺点:就是会造成全局污染。
    • 优点:可以根据浏览器对新特性的支持度,来选择性的进行兼容性处理。
  • @babel/preset-env + @babel/runtime-corejs3 + @babel/plugin-transform-runtime:实现语法转换 + 模拟替换 api,只支持按需加载,我们简称 runtime 方案。

    • 优点:不会存在全局污染。
    • 不能根据浏览器对新特性的支持度来选择性的进行兼容性处理,也就是说只要在代码中识别到的 api,并且该api也在 corejs3 的 core-js-pure 包中存在,就会自动替换,这样一来就会造成一些不必要的转换,从而增加代码体积。

两种方案都依赖核心包 corejs@3,只不过依赖的模块(一个污染包一个清洁包)不同,导致实现方式不同。

所以,polyfill 方案比较适合单独运行的业务项目,如果你是想开发一些供别人使用的第三方工具库,则建议你使用 runtime 方案来处理兼容性方案,以免影响使用者的运行环境。

两种配置的模板在 Babel 配置和使用 的文档中。