第83课:Babel 与代码转译——预设、插件、browserslist、core-js polyfill 策略
现代 JavaScript 语法日新月异:ES6+ 引入的箭头函数、模板字符串、Promise、async/await 等特性极大地提升了开发效率,但浏览器对它们的支持进度参差不齐。Babel 是解决这个问题的标准工具:它把使用最新语法编写的代码转译为向后兼容的 JavaScript,让开发者无需等待浏览器支持即可使用新特性。Babel 本身是一个编译平台,核心转译能力由插件(Plugin) 提供,而预设(Preset) 则是插件的集合。本节课将深入讲解 Babel 的工作流程、如何通过 @babel/preset-env 和 browserslist 精确控制转译范围、以及 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 | // babel.config.js |
插件按声明顺序从前到后依次执行。每个插件可以是一个字符串,也可以是一个数组(用于传递选项):
1 | plugins: [ |
2.2 插件与预设的执行顺序
- 插件先于预设执行。
- 多个插件按声明顺序从前到后执行。
- 多个预设按声明顺序从后到前执行。
这一规则确保了插件的细粒度控制优先于预设的批量配置,预设之间后者覆盖前者。
重要:手动列举每个语法插件极其繁琐(ES6 就有几十个新语法特性)。因此社区提供了预设(Preset)——一组预先编排好的插件集合,一键启用整批转换。
3. 预设(Preset):插件的集合
预设就是一个插件的组合包。最核心的预设是 @babel/preset-env。
3.1 @babel/preset-env:智能预设
@babel/preset-env 根据目标环境自动确定需要哪些插件和 polyfill。你不再需要手动挑选插件,只需声明目标浏览器,Babel 就会按需转译。
1 | // babel.config.js |
targets 支持多种写法:
browserslist查询字符串:'> 0.5%, not dead'- 具体版本:
{ chrome: '80', firefox: '75', safari: '14' } browserslist配置文件:如果不写targets,Babel 会自动读取项目根目录的.browserslistrc或package.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 | // package.json |
1 | # .browserslistrc |
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-env 的 targets 选项默认从 browserslist 配置读取。如果没有显式设置 targets,Babel 会自动根据 browserslist 确定需要转译的语法和 polyfill。目标浏览器越新,需要转译的内容越少,打包产物体积越小。例如,如果目标只包含 Chrome 90+ 和 Safari 15+,几乎不需要转译 class、箭头函数等特性(因为这些浏览器已原生支持),Babel 会跳过对应的插件。
5. Polyfill 策略:core-js 与 useBuiltIns
Babel 的转译分为两个层面:
- 语法转换:将新语法(如
=>、class、async/await)转换为旧语法。这是插件的职责。 - API 补丁:为缺失的全局 API(如
Promise、Array.from、Object.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 | // babel.config.js |
源代码:
1 | const arr = [1, 2, 3]; |
转译后(简化示意):
1 | ; |
Babel 自动扫描了代码中使用的 Array.from、async/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,修改 Promise、Array.prototype 等。 |
✅ 是 | 应用项目(非库项目) |
@babel/plugin-transform-runtime |
将 Promise、class 等特性转换为对运行时模块函数的引用,不修改全局对象。 |
❌ 否 | 库开发(避免污染用户全局) |
选择原则:
- 开发一个Web 应用:使用
@babel/preset-env的useBuiltIns: 'usage'+core-js。 - 开发一个npm 库:使用
@babel/plugin-transform-runtime,避免污染使用方的全局环境。
6. Babel 配置文件的组织
Babel 支持多种配置文件格式,按优先级排序:
- **
babel.config.js**(项目根目录,推荐):适用于 monorepo 和需要根据环境动态生成配置的场景。 - **
.babelrc/.babelrc.json**:适用于单包项目,相对于文件所在目录解析。 package.json中的babel字段:与.babelrc等价。
**推荐使用 babel.config.js**,因为它具有项目范围的全局配置,更容易与 Jest、Webpack 等工具共享。
7. 综合实战:一个生产就绪的 Babel 配置
1 | // babel.config.js |
关键配置说明:
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选项可以显著加快重新编译速度,避免重复编译未变更的文件。
课后练习
一、概念自测(选择题 / 填空题)
(单选) Babel 的核心架构分为三个阶段,正确的顺序是?
A. 转换 → 解析 → 生成
B. 解析 → 转换 → 生成
C. 解析 → 生成 → 转换
D. 生成 → 解析 → 转换(单选) 在
@babel/preset-env的useBuiltIns选项中,哪个值可以实现按需自动引入 polyfill,无需手动导入?
A.false
B.'entry'
C.'usage'
D.'auto'(填空) 要确保 Babel 保留 ES Modules(不转换为 CommonJS),以便 Webpack 进行 Tree Shaking,应在
@babel/preset-env中设置modules: ______。(多选) 以下哪些工具可以共享
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 配置。需要生成以下文件:
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'等配置。package.json:添加必要的 devDependencies:@babel/core,@babel/preset-env,@babel/preset-typescript,@babel/plugin-transform-runtime,core-js。- 在配置文件中添加详细的注释,解释每个选项的用途和与 Vite 的配合。
完成后列出所有文件内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请解释 Babel 中
preset-env的useBuiltIns: '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(如 Promise、Array.from、Object.assign),并与目标环境的兼容性数据对比,仅在目标环境缺失且代码确实使用该 API 时,才自动注入对应的 core-js 模块引用。这种按需引入的方式避免了不必要的 polyfill 代码,因此通常生成更小的打包产物。
对于 npm 库开发,不推荐使用 preset-env 的 polyfill 方案(无论是 entry 还是 usage),因为这些方案会污染全局原型——例如修改 Array.prototype.includes 或挂载全局 Promise。如果库的使用方自己的代码或依赖的其他库也引入了 polyfill,可能产生冲突。此外,库无法预知使用方的目标环境,不应替使用方决定 polyfill 策略。@babel/plugin-transform-runtime 将全局 API(如 Promise、class 语法依赖的 createClass 辅助函数)转换为对 @babel/runtime 模块的引用,完全隔离在模块作用域内,不修改任何全局对象,从而避免了污染问题。这是库开发的标准实践。
课后练习答案
一、概念自测答案
B
- 解析:Babel 的工作流程为解析(Parse)→ 转换(Transform)→ 生成(Generate)。
C
- 解析:
useBuiltIns: 'usage'会根据目标环境按需自动注入 polyfill。'entry'需要手动引入入口 polyfill,false不注入。
- 解析:
false- 解析:
modules: false告诉 Babel 保留 ES Modules 语法,不转换为 CommonJS,以便 Webpack 等打包工具进行 Tree Shaking。
- 解析:
A、B、C
- 解析:Babel 的
preset-env和 PostCSS 的autoprefixer可直接读取browserslist。ESLint 的eslint-plugin-compat也使用它检查兼容性。Webpack 的 TerserPlugin 不直接读取browserslist。
- 解析:Babel 的
二、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。- 为每个选项添加注释解释用途。
- 直接输出完整文件内容。”