Babel 中 preset 和 plugin 的区别是什么?如何配置?
核心区别
Plugin 是 Babel 转换的最小单元,Preset 是 Plugin 的集合。
打个比方:Plugin 是单品菜,Preset 是套餐。@babel/plugin-transform-arrow-functions 只做一件事——把箭头函数转成普通函数;而 @babel/preset-env 是一份根据你的目标环境自动搭配的套餐,内部打包了几十个 Plugin。
这个区别决定了三件事:
- 粒度不同——Plugin 精确到单个语法转换,Preset 按场景批量组合
- 配置方式不同——Plugin 放
plugins数组,Preset 放presets数组 - 执行顺序不同——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 语法 |
执行顺序:面试高频追问
执行顺序是这道题最常被追问的点,记住三条规则:
- Plugin 先于 Preset 执行
- 多个 Plugin 按声明顺序从前到后执行
- 多个 Preset 按声明顺序从后到前执行
javascriptmodule.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,其中 useBuiltIns 和 corejs 两个参数经常被问到。
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.flat、Object.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 访问者模式实现转换:
javascriptmodule.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 数组的函数:
javascriptmodule.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 的组合。