5月28日 02:34

Babel 中 preset 和 plugin 的区别是什么?如何配置?

核心区别

Plugin 是 Babel 转换的最小单元,Preset 是 Plugin 的集合。

打个比方:Plugin 是单品菜,Preset 是套餐。@babel/plugin-transform-arrow-functions 只做一件事——把箭头函数转成普通函数;而 @babel/preset-env 是一份根据你的目标环境自动搭配的套餐,内部打包了几十个 Plugin。

这个区别决定了三件事:

  1. 粒度不同——Plugin 精确到单个语法转换,Preset 按场景批量组合
  2. 配置方式不同——Plugin 放 plugins 数组,Preset 放 presets 数组
  3. 执行顺序不同——Plugin 先于 Preset 执行;多个 Plugin 从前往后,多个 Preset 从后往前

配置方式

Plugin 配置

javascript
// babel.config.js module.exports = { plugins: [ // 无参数 '@babel/plugin-transform-arrow-functions', // 带参数,用数组包裹 ['@babel/plugin-transform-runtime', { corejs: 3, helpers: true }] ] };

单独使用 Plugin 的场景不多,通常只在 Preset 覆盖不到时补充,比如自定义转换逻辑或处理实验性语法。

Preset 配置

javascript
// babel.config.js module.exports = { presets: [ // 带参数配置 ['@babel/preset-env', { targets: '> 0.25%, not dead', useBuiltIns: 'usage', corejs: 3 }], // 无参数,直接写字符串 '@babel/preset-react' ] };

三个常用 Preset 的职责:

Preset作用
@babel/preset-env根据目标环境自动选择需要的转换插件
@babel/preset-react处理 JSX 语法
@babel/preset-typescript处理 TypeScript 语法

执行顺序:面试高频追问

执行顺序是这道题最常被追问的点,记住三条规则:

  1. Plugin 先于 Preset 执行
  2. 多个 Plugin 按声明顺序从前到后执行
  3. 多个 Preset 按声明顺序从后到前执行
javascript
module.exports = { plugins: [ 'plugin-a', // 第 1 个执行 'plugin-b' // 第 2 个执行 ], presets: [ 'preset-b', // 第 4 个执行(逆序) 'preset-a' // 第 3 个执行 ] }; // 实际顺序:plugin-a → plugin-b → preset-a → preset-b

Preset 为什么逆序? 这不是设计失误,而是实用考量。在 Babel 6 时代,Stage Preset 按提案阶段编号命名(stage-0 到 stage-3),stage-0 包含所有提案语法,stage-3 只包含最成熟的。逆序执行意味着写在后面的 Preset 先跑,这样 presets: ['stage-3', 'stage-0'] 中 stage-0 先执行(包含最多),stage-3 后执行(覆盖最成熟的部分),符合"先宽后窄"的直觉。Babel 7 虽然废弃了 Stage Preset,但逆序规则保留了下来。

preset-env 的两个关键配置

@babel/preset-env 是日常使用最多的 Preset,其中 useBuiltInscorejs 两个参数经常被问到。

useBuiltIns

控制如何注入 polyfill:

  • false:不注入,需要手动引入 @babel/polyfill(已废弃)
  • 'entry':在入口文件 import 'core-js' 处,根据 targets 替换为精确的 polyfill
  • 'usage':按需注入,代码中用到了哪个 API 就自动引入对应的 polyfill

corejs

指定 core-js 版本:

javascript
['@babel/preset-env', { targets: { chrome: '80' }, useBuiltIns: 'usage', corejs: 3 // 必须显式声明,否则 polyfill 不生效 }]

corejs: 3 对应 core-js@3,支持更多 API(如 Array.flatObject.fromEntries)。如果设为 2,很多新 API 的 polyfill 不会注入。

useBuiltIns 的局限与 plugin-transform-runtime

useBuiltIns: 'usage' 注入的 polyfill 是模块级别的,会污染全局作用域。在开发库(library)时,这会影响使用方项目的全局环境。@babel/plugin-transform-runtime 解决了这个问题——它将 polyfill 以引用方式注入,不污染全局:

javascript
// useBuiltIns: 'usage' 的输出(污染全局) require("core-js/modules/es.array.includes.js"); [1, 2, 3].includes(2); // plugin-transform-runtime 的输出(不污染全局) var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes")); (0, _includes.default)([1, 2, 3]).call([1, 2, 3], 2);

简单原则:应用项目用 useBuiltIns,库项目用 plugin-transform-runtime

自定义 Plugin 和 Preset

自定义 Plugin

Plugin 本质是一个返回 visitor 对象的函数,通过 AST 访问者模式实现转换:

javascript
module.exports = function(babel) { const { types: t } = babel; return { name: 'remove-console-plugin', visitor: { CallExpression(path) { const callee = path.node.callee; if ( t.isMemberExpression(callee) && callee.object.name === 'console' ) { path.remove(); } } } }; };

自定义 Preset

Preset 是返回 Plugin 数组的函数:

javascript
module.exports = function() { return { plugins: [ ['@babel/plugin-transform-runtime', { corejs: 3 }], 'remove-console-plugin' ] }; };

团队内可以将通用配置封装为自定义 Preset,在不同项目中复用。

面试常见追问

Q:Babel 的编译流程是什么?

Babel 的编译分为三步:parse(将源码转成 AST)→ transform(Plugin 在这步遍历并修改 AST)→ generate(将修改后的 AST 生成目标代码)。Plugin 和 Preset 都作用于 transform 阶段。

Q:如何查看 Babel 实际使用了哪些 Plugin?

在终端执行 npx babel --debug your-file.js,或在代码中设置环境变量 DEBUG=babel* 运行构建,可以看到每个 Plugin 的加载和执行情况。

Q:Babel 7 相比 Babel 6 在配置上有哪些变化?

三个主要变化:所有包统一到 @babel 作用域下(babel-preset-env@babel/preset-env);废弃 Stage Preset,实验性语法需单独安装 Plugin;@babel/polyfill 被废弃,改用 core-js + useBuiltIns 的组合。

标签:Babel