WinddSnow

Babel-and-Code-Transpilation-Presets-Plugins-Browserslist-Polyfill

字数统计: 4.2k阅读时长: 17 min
2026/07/31

第83课:Babel 与代码转译——预设、插件、browserslistcore-js polyfill 策略

现代 JavaScript 语法日新月异:ES6+ 引入的箭头函数、模板字符串、Promiseasync/await 等特性极大地提升了开发效率,但浏览器对它们的支持进度参差不齐。Babel 是解决这个问题的标准工具:它把使用最新语法编写的代码转译为向后兼容的 JavaScript,让开发者无需等待浏览器支持即可使用新特性。Babel 本身是一个编译平台,核心转译能力由插件(Plugin) 提供,而预设(Preset) 则是插件的集合。本节课将深入讲解 Babel 的工作流程、如何通过 @babel/preset-envbrowserslist 精确控制转译范围、以及 core-js 提供的 polyfill 策略——useBuiltIns: 'usage' 如何做到按需自动补全缺失的 API,从而在转译体积和兼容性之间取得最佳平衡。


1. Babel 的核心机制:解析 → 转换 → 生成

Babel 是一个编译器。它将 JavaScript 代码解析为 AST(抽象语法树),然后通过插件对 AST 进行修改,最后从修改后的 AST 重新生成 JavaScript 代码。整个过程分为三步:

  • 解析(Parse):使用 @babel/parser 将源码解析为 AST。Babel 支持最新 ECMAScript 语法和 TypeScript/JSX 等扩展。
  • 转换(Transform)插件遍历 AST,对特定节点进行修改。例如,箭头函数插件将 ArrowFunctionExpression 节点转换为普通函数表达式。
  • 生成(Generate):使用 @babel/generator 将修改后的 AST 重新输出为 JavaScript 代码,并生成 Source Map。

Babel 本身不做任何转换——它仅仅是一个平台。所有的转译能力都来自插件(@babel/plugin-transform-*)。这种架构使得 Babel 极其灵活:你可以只选择需要的转译插件,也可以使用社区贡献的插件实现自定义语法扩展。


2. 插件(Plugin):转译的最小单元

每个 Babel 插件负责一项特定的语法转换。例如:

  • @babel/plugin-transform-arrow-functions:将箭头函数转换为普通函数。
  • @babel/plugin-transform-template-literals:将模板字符串转换为字符串拼接。
  • @babel/plugin-transform-classes:将 class 语法转换为基于原型的构造函数。

2.1 插件的配置方式

babel.config.js 中通过 plugins 数组声明使用哪些插件:

1
2
3
4
5
6
7
// babel.config.js
module.exports = {
plugins: [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-template-literals',
]
};

插件按声明顺序从前到后依次执行。每个插件可以是一个字符串,也可以是一个数组(用于传递选项):

1
2
3
4
5
6
7
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3,
helpers: true,
regenerator: true,
}]
]

2.2 插件与预设的执行顺序

  • 插件先于预设执行。
  • 多个插件按声明顺序从前到后执行。
  • 多个预设按声明顺序从后到前执行。

这一规则确保了插件的细粒度控制优先于预设的批量配置,预设之间后者覆盖前者。

重要:手动列举每个语法插件极其繁琐(ES6 就有几十个新语法特性)。因此社区提供了预设(Preset)——一组预先编排好的插件集合,一键启用整批转换。


3. 预设(Preset):插件的集合

预设就是一个插件的组合包。最核心的预设是 @babel/preset-env

3.1 @babel/preset-env:智能预设

@babel/preset-env 根据目标环境自动确定需要哪些插件和 polyfill。你不再需要手动挑选插件,只需声明目标浏览器,Babel 就会按需转译。

1
2
3
4
5
6
7
8
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: '> 0.5%, not dead', // 目标浏览器范围
}]
]
};

targets 支持多种写法

  • browserslist 查询字符串'> 0.5%, not dead'
  • 具体版本{ chrome: '80', firefox: '75', safari: '14' }
  • browserslist 配置文件:如果不写 targets,Babel 会自动读取项目根目录的 .browserslistrcpackage.json 中的 browserslist 字段。

3.2 其他常用预设

预设 用途
@babel/preset-react 处理 JSX 语法,可配置 runtime: 'automatic'(React 17+)。
@babel/preset-typescript 移除 TypeScript 类型注解(仅移除,不做类型检查)。
@babel/preset-flow 移除 Flow 类型注解。

3.3 预设与 TypeScript 的配合

当同时使用 @babel/preset-typescript@babel/preset-env 时,Babel 会先移除类型注解,再转译语法。由于 Babel 不做类型检查,构建流程通常配合 tsc --noEmit 单独进行类型校验。


4. browserslist:目标环境的统一配置

browserslist 是一个目标浏览器配置,被 Babel、PostCSS(autoprefixer)、ESLint 等多个工具共享。它让团队只需在一个地方声明支持哪些浏览器,所有工具自动按此范围优化。

4.1 配置方式

package.json 中添加 browserslist 字段,或在项目根目录创建 .browserslistrc 文件。

1
2
3
4
5
6
7
8
9
// package.json
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
1
2
3
4
5
# .browserslistrc
> 0.5%
last 2 versions
not dead
not ie 11

4.2 查询语法速览

查询语句 含义
> 1% 全球使用率超过 1% 的浏览器版本。
last 2 versions 每个浏览器最新的两个主版本。
not dead 排除官方已停止维护(或 24 个月未更新)的浏览器。
Chrome > 80 Chrome 版本大于 80。
Firefox ESR Firefox 延长支持版本。
cover 99.5% 覆盖全球 99.5% 用户的浏览器集合。
since 2020 自 2020 年以来发布的所有版本。
maintained node versions 当前仍在维护的 Node.js 版本。

可以通过 npx browserslist 命令在终端查看当前配置覆盖了哪些浏览器版本,验证配置是否符合预期。

4.3 browserslist 如何影响 Babel

@babel/preset-envtargets 选项默认从 browserslist 配置读取。如果没有显式设置 targets,Babel 会自动根据 browserslist 确定需要转译的语法和 polyfill。目标浏览器越新,需要转译的内容越少,打包产物体积越小。例如,如果目标只包含 Chrome 90+ 和 Safari 15+,几乎不需要转译 class、箭头函数等特性(因为这些浏览器已原生支持),Babel 会跳过对应的插件。


5. Polyfill 策略:core-jsuseBuiltIns

Babel 的转译分为两个层面:

  • 语法转换:将新语法(如 =>classasync/await)转换为旧语法。这是插件的职责。
  • API 补丁:为缺失的全局 API(如 PromiseArray.fromObject.assign)提供运行时补充。这是 polyfill 的职责。

Babel 默认只转译语法,不处理 API。也就是说,如果你的代码使用了 Promise,Babel 不会自动为你添加 Promise 的实现代码。如果目标浏览器不支持 Promise,运行时会报错。解决这个问题需要 **core-js**——一个模块化的 JavaScript 标准库 polyfill。

5.1 core-js:模块化的 polyfill 库

core-js 提供了几乎所有 ES 标准特性的 polyfill 实现。它可以全局注入(污染全局原型),也可以按需引入每个特性的模块。

@babel/preset-env 通过 useBuiltIns 选项控制如何引用 core-js,并结合 corejs 选项声明使用的 core-js 版本(推荐 3.x)。

5.2 useBuiltIns 的三种模式

行为 产物大小 污染全局
false(默认) 不自动注入任何 polyfill。需要手动 import 'core-js/stable' 或逐个引入。 最大 取决于引入
'entry' 根据目标环境,将手动引入的 import 'core-js' 替换为具体需要的模块。必须先在入口文件中手动 import 'core-js'import 'regenerator-runtime/runtime' 较大 ✅ 是
'usage' 自动按需引入。Babel 扫描所有代码,仅为实际使用且目标环境缺失的 API 注入对应 core-js 模块。无需手动引入任何 polyfill 最小 ✅ 是

5.3 usage 模式的工作示例

配置

1
2
3
4
5
6
7
8
9
10
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: '3.32',
targets: '> 0.5%, not dead',
}]
]
};

源代码

1
2
3
4
5
6
7
const arr = [1, 2, 3];
const doubled = Array.from(arr, x => x * 2);

async function fetchData() {
const res = await fetch('/api');
return res.json();
}

转译后(简化示意)

1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";

require("core-js/modules/es.array.from.js"); // 自动注入,因为 Array.from 在目标环境中缺失
require("core-js/modules/es.promise.js"); // async/await 依赖 Promise
require("core-js/modules/web.dom-collections.iterator.js");

const arr = [1, 2, 3];
const doubled = Array.from(arr, x => x * 2);

async function fetchData() {
const res = await fetch('/api');
return res.json();
}

Babel 自动扫描了代码中使用的 Array.fromasync/await(编译后依赖 Promise)等特性,并仅为目标环境缺失的特性注入了 core-js 的对应模块。开发者无需关心 polyfill 的细节。

5.4 regenerator-runtime

async/await 转译后需要一个运行时辅助函数 regeneratorRuntime。在 useBuiltIns: 'usage' 模式下,Babel 会自动注入 regenerator-runtime/runtime 的引用,无需手动引入。

5.5 @babel/plugin-transform-runtime vs polyfill

@babel/plugin-transform-runtime 是另一种 polyfill 方案。它与 preset-env 的 polyfill 区别在于:

方案 工作方式 污染全局原型 适用场景
preset-env + core-js 注入全局 polyfill,修改 PromiseArray.prototype 等。 ✅ 是 应用项目(非库项目)
@babel/plugin-transform-runtime Promiseclass 等特性转换为对运行时模块函数的引用,不修改全局对象 ❌ 否 库开发(避免污染用户全局)

选择原则:

  • 开发一个Web 应用:使用 @babel/preset-envuseBuiltIns: 'usage' + core-js
  • 开发一个npm 库:使用 @babel/plugin-transform-runtime,避免污染使用方的全局环境。

6. Babel 配置文件的组织

Babel 支持多种配置文件格式,按优先级排序:

  1. **babel.config.js**(项目根目录,推荐):适用于 monorepo 和需要根据环境动态生成配置的场景。
  2. **.babelrc / .babelrc.json**:适用于单包项目,相对于文件所在目录解析。
  3. package.json 中的 babel 字段:与 .babelrc 等价。

**推荐使用 babel.config.js**,因为它具有项目范围的全局配置,更容易与 Jest、Webpack 等工具共享。


7. 综合实战:一个生产就绪的 Babel 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 按需引入 polyfill
corejs: '3.32', // core-js 版本
modules: false, // 保留 ES Modules(交给 Webpack 处理 Tree Shaking)
targets: {
browsers: '> 0.5%, not dead',
},
}],
['@babel/preset-react', {
runtime: 'automatic', // React 17+ 新 JSX 转换
}],
'@babel/preset-typescript',
],

plugins: [
// 仅在生产环境移除 PropTypes,减小体积
process.env.NODE_ENV === 'production' && ['babel-plugin-transform-react-remove-prop-types', { removeImport: true }],
].filter(Boolean),
};

关键配置说明

  • modules: false:告诉 Babel 不要将 import/export 转换为 CommonJS(保留 ESM 语法),由 Webpack 负责模块解析,从而保留 Tree Shaking 能力。
  • corejs: '3.32':必须与已安装的 core-js 版本匹配。
  • runtime: 'automatic':启用 React 17+ 的新 JSX 转换,无需在每个文件顶部手动 import React
  • 插件条件过滤:通过 .filter(Boolean) 仅在满足条件时包含插件。

8. Babel 的局限性与性能考量

  • Babel 不进行类型检查@babel/preset-typescript 仅移除类型注解,不做类型验证。必须配合 tsc --noEmit 或 IDE 的类型检查。
  • 转译体积与目标环境的权衡:目标浏览器范围越宽(包含更多老旧浏览器),生成的代码体积越大。应结合产品实际用户群体和业务需求设定合适的 browserslist
  • 编译缓存:在 Webpack 中使用 babel-loader 时,配合 cacheDirectory: true 选项可以显著加快重新编译速度,避免重复编译未变更的文件。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Babel 的核心架构分为三个阶段,正确的顺序是?
    A. 转换 → 解析 → 生成
    B. 解析 → 转换 → 生成
    C. 解析 → 生成 → 转换
    D. 生成 → 解析 → 转换

  2. (单选)@babel/preset-envuseBuiltIns 选项中,哪个值可以实现按需自动引入 polyfill,无需手动导入?
    A. false
    B. 'entry'
    C. 'usage'
    D. 'auto'

  3. (填空) 要确保 Babel 保留 ES Modules(不转换为 CommonJS),以便 Webpack 进行 Tree Shaking,应在 @babel/preset-env 中设置 modules: ______

  4. (多选) 以下哪些工具可以共享 browserslist 配置?
    A. Babel(@babel/preset-env
    B. PostCSS(autoprefixer
    C. ESLint(eslint-plugin-compat
    D. Webpack(通过 TerserPlugin)

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为一个 Vue 3 + Vite 项目配置 Babel,使其支持以下功能:

  • 使用 @babel/preset-env,目标浏览器为 > 1%, last 2 versions, not dead
  • 启用 useBuiltIns: 'usage',使用 core-js@3 进行按需 polyfill。
  • 保留 ES Modules(modules: false),交由 Vite 处理。
  • 使用 @babel/preset-typescript 处理 TypeScript 语法(移除类型注解)。
  • babel.config.js 中添加合适的注释解释每个选项的作用。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 babel.config.js 配置文件。提示词中需明确指定预设名称、选项和配置理由。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请为一个 Vue 3 + TypeScript + Vite 项目创建完整的 Babel 配置。需要生成以下文件:

  1. babel.config.js
    • 使用 @babel/preset-env,设置 useBuiltIns: 'usage', corejs: '3.32', modules: false
    • targets 使用 browserslist 查询字符串 '> 1%, last 2 versions, not dead'
    • 添加 @babel/preset-typescript
    • 添加 @babel/plugin-transform-runtime(避免 polyfill 污染全局,但注意与 preset-env 的 polyfill 策略冲突,此处仅用于辅助函数复用,设置 corejs: false 或使用默认值)。
    • 说明为什么在 Vite 项目中不需要 modules: 'auto' 等配置。
  2. package.json:添加必要的 devDependencies:@babel/core, @babel/preset-env, @babel/preset-typescript, @babel/plugin-transform-runtime, core-js
  3. 在配置文件中添加详细的注释,解释每个选项的用途和与 Vite 的配合。
    完成后列出所有文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请解释 Babel 中 preset-envuseBuiltIns: 'usage'useBuiltIns: 'entry' 的区别,以及它们各自的工作机制。为什么 usage 模式通常能生成更小的打包产物?另外,如果开发一个 npm 库,为什么推荐使用 @babel/plugin-transform-runtime 而不是 preset-env 的 polyfill 方案?

参考答案

useBuiltIns: 'entry' 需要在入口文件中手动 import 'core-js/stable'(或类似导入),Babel 会将这个引入语句替换为多个针对目标环境的具体 polyfill 模块。它基于整个应用的入口进行全局替换,无法得知代码中实际使用了哪些 API,因此会引入大量可能未使用的 polyfill。

useBuiltIns: 'usage' 则不需要任何手动导入。Babel 在转译过程中扫描所有源代码,检测代码中实际使用的 ES API(如 PromiseArray.fromObject.assign),并与目标环境的兼容性数据对比,仅在目标环境缺失且代码确实使用该 API 时,才自动注入对应的 core-js 模块引用。这种按需引入的方式避免了不必要的 polyfill 代码,因此通常生成更小的打包产物。

对于 npm 库开发,不推荐使用 preset-env 的 polyfill 方案(无论是 entry 还是 usage),因为这些方案会污染全局原型——例如修改 Array.prototype.includes 或挂载全局 Promise。如果库的使用方自己的代码或依赖的其他库也引入了 polyfill,可能产生冲突。此外,库无法预知使用方的目标环境,不应替使用方决定 polyfill 策略。@babel/plugin-transform-runtime 将全局 API(如 Promiseclass 语法依赖的 createClass 辅助函数)转换为对 @babel/runtime 模块的引用,完全隔离在模块作用域内,不修改任何全局对象,从而避免了污染问题。这是库开发的标准实践。


课后练习答案

一、概念自测答案

  1. B

    • 解析:Babel 的工作流程为解析(Parse)→ 转换(Transform)→ 生成(Generate)。
  2. C

    • 解析:useBuiltIns: 'usage' 会根据目标环境按需自动注入 polyfill。'entry' 需要手动引入入口 polyfill,false 不注入。
  3. false

    • 解析:modules: false 告诉 Babel 保留 ES Modules 语法,不转换为 CommonJS,以便 Webpack 等打包工具进行 Tree Shaking。
  4. A、B、C

    • 解析:Babel 的 preset-env 和 PostCSS 的 autoprefixer 可直接读取 browserslist。ESLint 的 eslint-plugin-compat 也使用它检查兼容性。Webpack 的 TerserPlugin 不直接读取 browserslist

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请生成一个 Babel 配置文件 babel.config.js,适用于 Vue 3 + Vite 项目。要求:

  • 使用 @babel/preset-env,设置 useBuiltIns: 'usage', corejs: '3.32', modules: false
  • targets 使用字符串 '> 1%, last 2 versions, not dead'
  • 添加 @babel/preset-typescript 以移除类型注解。
  • 不添加 @babel/plugin-transform-runtime,因为 polyfill 方案选择 preset-env
  • 为每个选项添加注释解释用途。
  • 直接输出完整文件内容。”
CATALOG
  1. 1. 第83课:Babel 与代码转译——预设、插件、browserslist、core-js polyfill 策略
    1. 1.1. 1. Babel 的核心机制:解析 → 转换 → 生成
    2. 1.2. 2. 插件(Plugin):转译的最小单元
      1. 1.2.1. 2.1 插件的配置方式
      2. 1.2.2. 2.2 插件与预设的执行顺序
    3. 1.3. 3. 预设(Preset):插件的集合
      1. 1.3.1. 3.1 @babel/preset-env:智能预设
      2. 1.3.2. 3.2 其他常用预设
      3. 1.3.3. 3.3 预设与 TypeScript 的配合
    4. 1.4. 4. browserslist:目标环境的统一配置
      1. 1.4.1. 4.1 配置方式
      2. 1.4.2. 4.2 查询语法速览
      3. 1.4.3. 4.3 browserslist 如何影响 Babel
    5. 1.5. 5. Polyfill 策略:core-js 与 useBuiltIns
      1. 1.5.1. 5.1 core-js:模块化的 polyfill 库
      2. 1.5.2. 5.2 useBuiltIns 的三种模式
      3. 1.5.3. 5.3 usage 模式的工作示例
      4. 1.5.4. 5.4 regenerator-runtime
      5. 1.5.5. 5.5 @babel/plugin-transform-runtime vs polyfill
    6. 1.6. 6. Babel 配置文件的组织
    7. 1.7. 7. 综合实战:一个生产就绪的 Babel 配置
    8. 1.8. 8. Babel 的局限性与性能考量
    9. 1.9. 课后练习
      1. 1.9.1. 一、概念自测(选择题 / 填空题)
      2. 1.9.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.9.3. 三、Agent 模式下的提示词示例
      4. 1.9.4. 四、面试真题与参考答案
    10. 1.10. 课后练习答案
      1. 1.10.1. 一、概念自测答案
      2. 1.10.2. 二、AI 编程任务参考答案(提示词示例)