简易笔记:Babel 学习指南

86 阅读11分钟

Babel

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

Babel 主要做的两件事情:

  • Babel 通过语法转换器来支持新版本的 JavaScript 语法(转换 ES6 为 ES5 代码)

  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)

@babel/cli

@babel/cli 是一个能够从终端(命令行)使用的工具。在 Webpack 中使用 babel-loader 进行代替。

@babel/core

Babel 的核心功能包含在 @babel/core 模块中,是运行 Babel 的基础库。

@babel/preset-env

Babel 的代码转换功能是以插件的形式出现的,用于指导 Babel 如何对代码进行转换。

我们也可以编写自己的插件来实现自己想要的代码转换功能。

例如我们可以使用 @babel/plugin-transform-arrow-functions 插件将 ES6 的箭头函数转换为 ES5 兼容的函数表达式。

可以使用 @babel/plugin-transform-modules-commonjs 插件将 ES6 的 export 语句转换为 commonJS。

可以使用 @babel/plugin-transform-for-of 插件将 ES6 的 for..of 语句转换为 ES5 所兼容的代码。

实际项目开发过程中,我们的代码肯定不止少数几个 ES6+ 的特性,我们不可能一个接一个的去添加所有需要的插件。

这个时候我们就可以使用一组预先配置好的插件集合,我们称之为“preset”。

preset-env 则包含了支持所有最新的 JavaScript (ES2015+)特性的所有插件的集合。

另外我们还可以使用 babel.config.js 配置文件,为 preset 添加参数配置,使得代码转换结果达到项目所需的最优状态。

@babel/preset-react

此预设配置,用于支持转换 JSX 语法。主要包含以下插件:

@babel/plugin-syntax-jsx
@babel/plugin-transform-react-jsx
@babel/plugin-transform-react-display-name
@babel/plugin-transform-react-pure-annotations

@babel/preset-flow

此预设配置,用于支持 Flow 类型注释。主要包含以下插件:

@babel/plugin-transform-flow-strip-types

@babel/preset-typescript

此预设配置,用于支持 TypeScript 类型注释。主要包含以下插件:

@babel/plugin-transform-typescript

@babel/polyfill

默认情况下,Babel 只进行 JavaScript 代码的语法转换,比如箭头函数let/const块级作用域let..of遍历器async-await异步请求等。

而对于 ES6 新增的诸如 PromiseSet 等内置对象,以及 Array.fromObject.assign 之类的静态方法,还有 String.prototype.includes 之类的实例方法,均不会进行额外处理。

如果需要在较低版本中支持并使用它们,则需要加入对应的 polyfill 垫片,使得浏览器支持这些新的特性。

@babel/polyfill 模块包含 core-js 和一个自定义的 regenerator runtime 来模拟完整的 ES2015+ 环境。

我们可以安装 @babel/polyfill 并在 preset-env 中通过 useBuiltIns 参数进行搭配使用,可以按需引入相关的 polyfill,而不是全部导入。

babel.config.js 配置文件

babel.config.js 配置文件用于指导 Babel 如何工作,可以根据项目需求自定义配置。

module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        // 默认是 false 开启后控制台会看到 哪些语法做了转换,Babel的日志信息,开发的时候强烈建议开启
        "debug": false,
        // 用来指定转换需要支持哪些浏览器
        "targets": [ "> 1%", "last 2 versions", "not ie <= 8" ],
        // 指定引入 polyfill 的方式
        // false - 啥也不干,不会引入任何 polyfill
        // 'entry' - 会把所有的 polyfill 全部引入
        // 'usage' - 只会引入代码中用到的 polyfill
        "useBuiltIns": 'usage',
        // 指定core-js版本
        "corejs": "2.6.12",
        // import默认会被编译成了require,如果想要编译出来的模块引入规范还是import,则可以在preset-env的配置项中添加"modules": false即可。
        // modules的选项有:"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"
        "modules": false,
      }
    ]
  ]
}

Babel >= 7.4.0 弃用 @babel/polyfill 直接使用 core-js

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

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

@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 的配置来控制所需的版本。如果设置了useBuiltIns 选项(不为false)就得设置 corejs 版本,否则 babel 将会发出警告。

@babel/polyfill 是一个运行时包,主要是通过核心依赖 core-js@2 来完成对应浏览器不支持的新的全局和实例 api 的添加。在升级到 core-js@3 后,如果还要保留 @babel/polyfill 的使用,就要在@babel/polyfill 中添加 core-js@2core-js@3 切换的选项,这样 @babel/polyfill 中将包含core-js@2core-js@3 两个包,出于这个原因官方决定弃用 @babel/polyfill

Babel 7.4.0 版本开始,@babel/polyfill 已被弃用!

目前而言,配合 useBuiltIns 选项,有以下几种不同的组合使用方式:

useBuiltIns: false

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

编译前:

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;

useBuiltIns: 'entry'

需要在打包入口先行引入 core-js/stableregenerator-runtime/runtime,会将 browserslist 环境不支持的所有 polyfill 都导入。

编译前:

import "core-js/stable";  // yarn add core-js
import "regenerator-runtime/runtime"; // regenerator-runtime 会在安装 @babel/preset-env 的时候自动安装

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: 'usage'

代码中不用主动 importbabel 会自动将代码里已使用到的且 browserslist 环境不支持的 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.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);
});

@babel/preset-env 全局污染、重复的辅助函数、第三方库无法检测的问题

全局污染

@babel/preset-env 使用 polyfill 实现支持 PromiseWeaMap之类的内置对象,Array.fromObject.assign 之类的静态方法,还有 Array.prototype.includes 之类的实例方法,还有生成器函数(generator functions)等。

为了添加这些功能,polyfill 将添加到全局范围(global scope)和类似 String 这样的对象原型(native prototypes)中去,从而产生全局污染。

对于软件库/工具的作者来说,如果不需要类似 Array.prototype.includes 的实例方法,可以使用 transform runtime 插件而不是对全局范围(global scope)造成污染的 polyfill

重复的辅助函数

网上有很多人认为 @babel/polyfill 除了有全局污染的缺点外,还会让不同的文件中包含重复的代码,增加编译后的体积。举个例子:

编译前:

const key = 'babel'
const obj = {
  [key]: 'polyfill',
}

编译后:

"use strict";

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var key = 'babel';
var obj = _defineProperty({}, key, 'polyfill');

编译后的代码中插入了 _defineProperty 函数。

的确,如果多个文件中使用了对象的属性名表达式,则会插入多个 _defineProperty 函数。

但是,这件事情并不是 @babel/polyfill 这种 polyfill 方案实现的,而是 @babel/preset-env 本身在语法转换的时候,会使用一些辅助函数来实现一些语法的模拟。而事实的确,这只是一种语法转换。

第三方库无法检测

Babel 默认不处理 node_modules 中的第三方库,这也意味着如果其中一个依赖需要特殊的 polyfill,默认情况下 Babel 无法将其检测出来。

@babel/runtime 通过运行时解决全局污染的问题

在使用 @babel/preset-env 提供的语法转换和 api 添加的功能时,难免会造成文件的体积增加以及 api 的全局污染。为了解决这类问题,引入了 runtime 的概念,runtime 的核心思想是以引入替换的方式来解决兼容性问题。

runtime 包有三个:

@babel/runtime
@babel/runtime-corejs2
@babel/runtime-corejs3

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

只有在 @babel/preset-env 的帮助下,runtime 包的语法模拟替换功能才会发挥作用。

三个包不同的区别是:

  • @babel/runtime 只能处理语法替换。

  • @babel/runtime-corejs2 相比较 @babel/runtime 增加了 core-js@2 来支持全局构造函数和静态方法兼容。

  • @babel/runtime-corejs3 相比较 @babel/runtime-corejs2 支持了实例方法的兼容,同时还支持对ECMAScript 提案的 api 进行模拟。

@babel/runtime-corejs2 会从 core-js 中的 library 模块去加载对应的 runtime 代码:

// runtime-corejs2/core-js/array/from.js
module.exports = require("core-js/library/fn/array/from")

@babel/runtime-corejs3 会从 core-js-pure 这个包中去加载对应的 runtime 代码:

// runtime-corejs3/core-js/array/from.js
module.exports = require("core-js-pure/features/array/from")

对于数组的 includes 方法,@babel/runtime-corejs3 提供了模拟 api,而 @babel/runtime-corejs2没有:

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"))

(0, _includes.default)(_context = [1, 2, [3, 4]]).call(_context)

如果我们想在一个不支持 Promise 的环境下使用 Promise,可以这样:

// @babel/runtime-corejs2
// var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
// var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

// @babel/runtime-corejs3
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var instance = new _promise["default"](function (resolve, reject) {
  resolve(123);
});

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

@babel/plugin-transform-runtime 自动导入项目中所需的运行时

@babel/plugin-transform-runtime 就是为了方便 @babel/runtime 的使用。

通过 ast 的分析,自动识别并替换代码中的新 api,解决手动 require 的烦恼。

// babel.config.js
module.exports = {
  "presets": [
    [
      "@babel/preset-env"
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

corejs 选项来配置使用的是 @babel/runtime-corejs2 还是 @babel/runtime-corejs3

编译前:

const result = [1, 2, 3, 4, 5].copyWithin(0, 3)

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

const key = 'babel'
const obj = {
  [key]: 'polyfill',
}

使用 @babel/runtime-corejs2 编译后:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var result = [1, 2, 3, 4, 5].copyWithin(0, 3);
var instance = new _promise["default"](function (resolve, reject) {
  resolve(123);
});
var key = 'babel';
var obj = (0, _defineProperty2["default"])({}, key, 'polyfill');

使用 @babel/runtime-corejs3 编译后:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));

var _copyWithin = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/copy-within"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var _context;

var result = (0, _copyWithin["default"])(_context = [1, 2, 3, 4, 5]).call(_context, 0, 3);
var instance = new _promise["default"](function (resolve, reject) {
  resolve(123);
});
var key = 'babel';
var obj = (0, _defineProperty2["default"])({}, key, 'polyfill');

可以看到,使用 @babel/runtime-corejs3 可以模拟数组上的 copyWithin 方法,而 @babel/runtime-corejs2 则不能。

Babel 处理兼容性问题总结方案

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

1、@babel/preset-env + corejs@3 实现简单语法转换 + 复杂语法注入api替换 + 在全局和者构造函数静态属性、实例属性上添加api,支持全量加载和按需加载,我们简称 polyfill 方案

2、@babel/preset-env + @babel/runtime-corejs3 + @babel/plugin-transform-runtime 实现简单语法转换 + 引入替换复杂语法和api,只支持按需加载,我们简称 runtime 方案

两种方案一个依赖核心包 core-js,一个依赖核心包 core-js-pure,两种方案各有优缺点:

1、polyfill 方案很明显的缺点就是会造成全局污染,而且会注入冗余的工具代码;优点是可以根据浏览器对新特性的支持度来选择性的进行兼容性处理;

2、runtime 方案虽然解决了 polyfill 方案的那些缺点,但是不能根据浏览器对新特性的支持度来选择性的进行兼容性处理,也就是说只要在代码中识别到的 api,并且该 api 也存在 core-js-pure 包中,就会自动替换,这样一来就会造成一些不必要的转换,从而增加代码体积。

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