Skip to content

想弄懂 Babel?你必须得先弄清楚这几个包

GitHub package.json version (subfolder of monorepo)

GitHub package.json version (subfolder of monorepo)

babel 最常用的配置项

以 babel.config.js 为例:

js
module.exports = {
  // 最常用的两个
  plugins: [],
  presets: []
}

@babel/preset-env

@babel/preset-env 可以分成两部分来理解,preset 和 env。preset 的作用是将 ES6+ 语法编译为 ES5 语法。env 的作用是根据用户设置的目标环境,提供目标环境缺失的功能特性。

env 作用默认是关闭的,需要配置参数 usage 来打开。

preset

下面举一个 @babel/preset-env 将 ES6+ 语法编译为 ES5 语法的例子。这个例子可以让你感受到使用预设的方便。

涉及到的依赖包只有三个。

json
{
  "devDependencies": {
    // babel 命令行工具
    "@babel/cli": "^7.21.5", 
    // babel 核心包
    "@babel/core": "^7.22.1",
    // babel 预设(一系列插件的集合)
    "@babel/preset-env": "^7.22.4"
  }
}

假设有这样一个文件,使用了很多 ES6+ 语法:

js
// 使用 const
const people = ['xiaowang', 'xiaojun', 'xiaoming']

const obj = {
  // 属性简洁表示法
  people,
  like: 'eat1111',
  // 箭头函数,参数默认值,模板字符串,
  play: (sport = '篮球') => {
    console.log(`喜欢${sport}`)
  }
}

// 解构
const {like} = obj

在使用 @babel/preset-env 时,如果没有指定 targets,那么默认会将代码兼容到最低版本,将其所有 ES6+ 语法转为 ES5。

preset.config.js

js
const presets = [
  ['@babel/preset-env', {
    targets: '> 0.25%, not dead'
  }]
];

module.exports = {presets};

而如果通过 plugins 选项配置插件,需要配置很多个插件!

发现

只要下载了 @babel/preset-env 依赖,下面这些插件的依赖都不用安装。

原因是 @babel/preset-env 包的 dependencies 字段已经添加了这些依赖,在下载 preset-env,也会下载这些依赖。

plugins.config.js

js
module.exports = {
  plugins: [
    // 转换箭头函数
    '@babel/plugin-transform-arrow-functions',
    // 将 let、const 转为 var
    '@babel/plugin-transform-block-scoping',
    // 转换解构赋值
    '@babel/plugin-transform-destructuring',
    // 识别解构参数、默认参数、剩余参数
    '@babel/plugin-transform-parameters',
    // 识别属性缩写
    '@babel/plugin-transform-shorthand-properties',
    // 识别模板字符串
    '@babel/plugin-transform-template-literals'
  ]
};

编译过后的文件:

js
"use strict";

// 使用 const
var people = ['xiaowang', 'xiaojun', 'xiaoming'];
var obj = {
  // 属性简洁表示法
  people: people,
  like: 'eat1111',
  // 箭头函数,参数默认值,模板字符串,
  play: function play() {
    var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '篮球';
    console.log("\u559C\u6B22".concat(sport));
  }
};

// 解构
var like = obj.like;

env

后面会讲到。env 根据用户设置的环境,动态转化目标浏览器不兼容的语法和动态生成垫片 polyfill API。

提案

TC39 提案分为 0 1 2 3 4 共五个阶段。

  1. 阶段 0(stage-0)——草根(Strawman):只是一个想法,可能是 Babel 插件。
  2. 第一阶段(stage-1)——提案(Proposal):这是值得研究的。
  3. 第二阶段(stage-2)——草案(Draft):初步规范。
  4. 第三阶段(stage-3)——候选(Candidate):完整的规范和最初的浏览器实现。
  5. 第四阶段(stage-4)——完成(Finished):将被添加到下一年度的版本中。

再看看官网中这段话:

官网原话

Note: @babel/preset-env won't include any JavaScript syntax proposals less than Stage 3 because at that stage in the TC39 process, it wouldn't be implemented by any browsers anyway. Those would need to be included manually.

提示

我们平常说的 Babel 6、Babel 7 指的是 @babele/core 的版本。

大致意思是:

  1. 在 Babel 7 以后,@babel/preset-env 舍弃了 Stage presets(@babel/preset-stage-x)这种预设
  2. @babel/preset-env 只提供 TC39 大于 stage-3 的提案(即只包含最后一个阶段)。因此如果要用小于 stage 4 的提案语法,则必须先安装再手动引入对应插件

第一点比较好理解。这里说说第二点。如果我们想用一些小于 stage-4 阶段的语法的话,光安装 @babel/preset-env 这一个包是没有用的,因为这个包里只包含编译 stage-4 的预设,所以我们就得安装并配置相应的 plugin 去编译。

do-expressions

举个例子,在写这句话时,有一个新的语法do-expressions,还处于 TC39 的 stage-1 阶段。

我们写一个 index.js 文件:

js
let x = do {
  let tmp = 123
  tmp * tmp + 1
}

如果直接使用下面的配置文件编译的话,将会报错。

js
module.exports = {
  presets: [
    '@babel/preset-env'
  ]
}
SyntaxError: C:\Users\lukecheng\Desktop\code\course\docs\useful\babel\babel-test\test-stage-1\index.js: Support for the experimental syntax 'doExpressions' isn't currently enabled (1:9):

1 | let x = do { | ^ 2 | let tmp = 123 3 | tmp * tmp + 1 4 | }

解决方法,下载依赖 @babel/plugin-proposal-do-expressions,然后在 babel 配置文件中的 plugins 中声明。

shell
pnpm i @babel/plugin-proposal-do-expressions -D
js
module.exports = {
  presets: [
    // '@babel/preset-env'
  ],
  plugins: [
    '@babel/plugin-proposal-do-expressions'
  ]
}

再次执行打包脚本,发现编译成功!index.js 变为了 compiled.js。

js
let x = function () {
  let tmp = 123;
  return tmp * tmp + 1;
}();

查看预设有哪些插件

在 node_modules 下 @babel/preset-env 包的 package.json 的 dependencies 字段中查看。

注意,我们在下载 @babel/preset-env 包的时候,也会下载 dependencies 字段下声明的包。

这也就是前文中提到的,光下载一个 @babel/preset-env 依赖,就可以在配置文件中的 plugins 字段中添加插件,但是却不用专门下载插件的依赖。

polyfill 垫片

功能

ES6+ 除了提供很多简洁的语法(let、class、() => {} 等)外,还为我们提供了很多便捷的 API(Promise、Symbol、Array.prototype.includes 等)。但旧版本浏览器是不支持这些 API 的,而 polyfill 存放了这些 API 的方法与实现,所以它可以使得这些不支持的浏览器支持这些 API。

理解

我们可以把所有这种存放了 ES6+ API 的方法与实现的集合叫做 polyfill,也就是我们经常说的垫片。(如果把我们的旧版本浏览器缺失的 API 当做一个个坑,polyfill 就是用来把这些坑填平)

polyfill 也分种类,有些 polyfill 包含所有浏览器缺失的 API,有些 polyfill 只包含浏览器缺失的 API 的一个,例如 promise-polyfillproxy-polyfill

@babel/polyfill

这个包已经不再维护。

为什么不再维护?(来自 GPT)

自从 Babel 7.4.0 版本起,官方推荐使用 "core-js/stable" 和 "regenerator-runtime/runtime" 来替代 "@babel/polyfill"。

以前,"@babel/polyfill" 用于在旧版浏览器中提供缺失的 JavaScript 功能和 API 的垫片(polyfills)。它会将这些垫片添加到全局对象中,以便在不支持这些功能的浏览器中运行新的 JavaScript 代码。然而,这种全局污染的方法在某些情况下可能会引发一些问题。

为了解决这个问题,Babel 推荐使用更为细粒度的 polyfill 引入方式。"core-js/stable" 用于引入 ES 功能的垫片,而 "regenerator-runtime/runtime" 用于引入 Generator 函数和 Async/Await 的垫片。这种方式允许开发者选择需要的功能,而不是一次性引入整个 polyfill 包。

因此,尽管 "@babel/polyfill" 没有被废弃,但官方推荐使用更细粒度的 polyfill 引入方式,以减少对全局对象的影响,并提高代码的可靠性和可维护性。

虽然废弃,但还有有必要了解。

因为很多项目还在用它来给老浏览器提供 ES6+ API。

官网解释

🚨 从 Babel 7.4.0 开始,这个包已经被弃用,转而直接包含 core-js/stable(用于 polyfill ECMAScript 功能)

使用:

js
import "core-js/stable";

我们通过一个例子来认识 @babel/polyfill。创建一个项目,只下载 @babel/polyfill 这个包。

看看 @babel/polyfill 的依赖,发现只有两个:core-jsregenerator-runtime。并且 core-js 的版本为 2.6.5。

core-js@2 作用

它里面存放了实现 ES6+ API 的实现方法。

regenerator-runtime

它的作用是编译我们代码里使用到的 async 函数和 generator 函数。

core-js

我们使用 @babel/polyfill,相当于引入了 core-js@2 和 regenerator-runtime。

babel@7.18.0 后,我们可以直接引入 core-js/stable

js
import "core-js/stable";

提醒

If you are compiling generators or async function to ES5, and you are using a version of @babel/core or @babel/plugin-transform-regenerator older than 7.18.0, you must also load the regenerator-runtime package

如果你正打算把 generator 函数或 async 函数编译为 ES5,并且你使用的 @babel/helpers 版本小于 7.18.0 或者你使用的 @babel/plugin-transform-regenerator 版本小于 7.18.0,你必须在入口文件中引入 regeneraor-runtime 包。

@babel/helpers 或 @babel/plugin-transform-regenerator 版本小于 7.18.0。

js
import "core-js/stable";
import "regenerator-runtime/runtime.js";

我们实际举个例子!

两个包的版本都小于 7.18.0

不会内联 generator 辅助函数

创建一个项目,只下载下面这些包:

json
{
  "name": "import-regenerator-runtime",
  "scripts": {
    "compile": "babel index.js -o compiled.js"
  },
  "devDependencies": {
    "@babel/cli": "^7.20.7",
    "@babel/core": "7.16.7", // 注意这里版本小于 7.18.0 //
    "@babel/plugin-transform-regenerator": "7.16.7" // 注意这里版本小于 7.18.0
  }
}

然后配置 babel.config.js

js
module.exports = {
  plugins: [
    '@babel/plugin-transform-regenerator'
  ]
}

编译 index.js:

js
main() 
async function main() {
  await new Promise(() => {
    setTimeout((resolve) => {
      console.log('hello, babel regenerator runtime')
      resolve()
    }, 1000)
  })
}

main(); 
function main() {
  return regeneratorRuntime.async(function main$(_context) {
    while (1) switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return regeneratorRuntime.awrap(new Promise(() => {
          setTimeout(resolve => {
            console.log('hello, babel regenerator runtime');
            resolve();
          }, 1000);
        }));
      case 2:
      case "end":
        return _context.stop();
    }
  }, null, null, null, Promise);
}

可以看到被编译后的文件出现了一个 regeneratorRuntime 变量,但是这个变量并没有被声明。所以如果执行这段代码肯定会报错。

@babel/core 版本小于 7.18.0,@babel/plugin-transform-regenerator 版本大于 7.18.0

会内联 generator 辅助函数

编译后:

js
function _regeneratorRuntime() { "use strict"; 
  // 略 ...
}
main();
function main() {
  return _regeneratorRuntime().async(function main$(_context) {
    while (1) switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return _regeneratorRuntime().awrap(new Promise(() => {
          setTimeout(resolve => {
            console.log('hello, babel regenerator runtime');
            resolve();
          }, 1000);
        }));
      case 2:
      case "end":
        return _context.stop();
    }
  }, null, null, null, Promise);
}

编译后发现 regeneratorRuntime 替换为了一个 内联的 _regeneratorRuntime() 函数。

如果用 node 执行这个 js 文件,1 秒后会看到控制台输出 hello, babel regenerator runtime

@babel/core 版本大于 7.18.0,@babel/plugin-transform-regenerator 版本小于 7.18.0

不会内联 generator 辅助函数

未引入 generator 内联辅助函数。

js
main();
function main() {
  return regeneratorRuntime.async(function main$(_context) {
    while (1) switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return regeneratorRuntime.awrap(new Promise(resolve => {
          setTimeout(() => {
            console.log('hello, babel regenerator runtime');
            resolve();
          }, 1000);
        }));
      case 2:
      case "end":
        return _context.stop();
    }
  }, null, null, null, Promise);
}

总结

会不会生产 generator 函数,取决于 @babel/plugin-transform-regenerator 插件的版本。而与 @babel/core 的版本无关。

transform-regenerator 版本低于 7.18.0 不会生产 generator 辅助函数,高于等于 7.18.0 会生成辅助内联函数。

我认为官方文档在这里写错了,我已经提交了 PR 修改这处错误,等待官方合并。

@babel/runtime

在 Babel 编译的时候,会生成一些辅助函数。

@babel/runtime 里面存放着这些辅助函数。

比如我写这样一个文件:

js
class People {
    constructor() {
    }
}
const person  = new Person();

经过 babel 编译后就会生成很多内联的辅助函数。

但其实这些内联辅助函数都可以在 @babel/runtime 这个包里面找到。

@babel/plugin-transform-runtime

@babel/runtime 里只是定义实现了这些辅助函数,但怎么让辅助函数从内联的形式变为从 @babel/runtime 里引入的形式呢?

可以使用插件 @babel/plugin-transform-runtime。

TIP

只使用插件 @babel/plugin-transform-runtime,会将辅助函数从 @babel/runtime 引入。这时执行编译后的代码,会报错。只需要再下载依赖包 @babel/runtime 即可。

@babel/plugin-transform-runtime 的 devDependencies 如下:

json
  "devDependencies": {
    "@babel/core": "workspace:^",
    "@babel/helper-plugin-test-runner": "workspace:^",
    "@babel/helpers": "workspace:^",
    "@babel/preset-env": "workspace:^",
    "@babel/runtime": "workspace:^", 
    "@babel/runtime-corejs3": "workspace:^",
    "@babel/template": "workspace:^",
    "@babel/types": "workspace:^",
    "make-dir": "condition:BABEL_8_BREAKING ? : ^2.1.0"
  },

Released under the MIT License.