跳到主要内容
预计阅读 38 分钟

工程化篇 | Babel

前言

2015 年 ES6 发布的时候,浏览器的支持情况可以用”惨不忍睹”来形容——箭头函数、let/const、解构赋值、Promise……这些新语法在大部分浏览器上都跑不了。但开发者们迫不及待地想用上这些好东西,怎么办?

Babel 应运而生。

Babel 的核心能力就一句话:把新语法的 JavaScript 代码转换成旧语法的 JavaScript 代码,让它可以在老浏览器上运行。

但到了今天,面试中被问到 Babel,很多人的回答还停留在”它是个编译器,把 ES6 转成 ES5”。面试官继续追问:

  • Babel 内部的三个阶段是什么?每个阶段做了什么?
  • @babel/preset-env 是怎么知道应该转换哪些语法的?
  • polyfill 是什么?useBuiltIns: 'usage'useBuiltIns: 'entry' 有什么区别?
  • SWC 比 Babel 快那么多,Babel 会被淘汰吗?

就开始含糊了。

本章,我们从 Babel 的三阶段工作流程讲起,深入理解 preset-env、browserslist 和 polyfill 策略,再对比 SWC,最后简单入门自定义 Babel 插件。


诊断自测

Q1:Babel 编译代码的三个阶段是什么?

点击查看答案
  1. 解析(Parse):把源代码解析成 AST(抽象语法树)。使用 @babel/parser(前身是 Babylon)
  2. 转换(Transform):遍历 AST,应用各种插件对 AST 节点进行增删改。这是 Babel 的核心阶段,所有的语法转换都在这里发生
  3. 生成(Generate):把修改后的 AST 重新生成为代码字符串,同时生成 source map。使用 @babel/generator

Q2:@babel/preset-env 中的 useBuiltIns: 'usage'useBuiltIns: 'entry' 有什么区别?

点击查看答案

两者都是控制 polyfill 注入方式的选项:

  • entry:你需要在入口文件中手动 import 'core-js',Babel 会根据目标浏览器把这个 import 替换为该浏览器需要的所有 polyfill。不管你实际用没用到,只要目标浏览器不支持的特性都会被引入。
  • usage:不需要手动 import,Babel 会按需注入——分析你的代码实际使用了哪些新特性,只引入那些目标浏览器不支持的 polyfill。产物更小。

推荐使用 usage,因为它生成的代码体积更小。

Q3:Babel 只做语法转换,那 PromiseArray.from 这些新 API 怎么办?

点击查看答案

Babel 默认只转换语法(如箭头函数 → 普通函数、letvar),不处理新的 API(如 PromiseArray.fromObject.assign)。

新的 API 需要通过 polyfill 来补充。polyfill 的本质是”在运行时给全局环境或原型链上添加缺失的 API 实现”。常用的 polyfill 库是 core-js,通过 @babel/preset-envuseBuiltIns 选项来控制注入方式。


一、Babel 的三阶段

1.1 解析(Parse)

源代码字符串 → @babel/parser → AST

@babel/parser 把 JavaScript 源代码解析成 AST(Abstract Syntax Tree,抽象语法树)。AST 是代码的结构化表示——把”文本”变成”数据”,方便程序分析和修改。

例如,这行代码:

const greet = (name) => `Hello, ${name}`;

会被解析成类似这样的 AST(简化版):

{
  "type": "VariableDeclaration",
  "kind": "const",
  "declarations": [{
    "id": { "type": "Identifier", "name": "greet" },
    "init": {
      "type": "ArrowFunctionExpression",
      "params": [{ "type": "Identifier", "name": "name" }],
      "body": {
        "type": "TemplateLiteral",
        "quasis": [
          { "value": { "raw": "Hello, " } },
          { "value": { "raw": "" } }
        ],
        "expressions": [{ "type": "Identifier", "name": "name" }]
      }
    }
  }]
}

1.2 转换(Transform)

AST → [Plugin 1] → [Plugin 2] → ... → [Plugin N] → 修改后的 AST

这是 Babel 的核心。转换阶段会遍历 AST 的每个节点,依次交给各个**插件(Plugin)**处理。每个插件负责一种转换。

例如:

  • @babel/plugin-transform-arrow-functions:把箭头函数转成普通函数
  • @babel/plugin-transform-template-literals:把模板字符串转成字符串拼接
  • @babel/plugin-transform-block-scoping:把 let/const 转成 var

Babel 使用 @babel/traverse 来遍历 AST。插件通过**访问者模式(Visitor Pattern)**注册对特定节点类型的处理函数:

// 简化版的箭头函数转换插件
module.exports = function () {
  return {
    visitor: {
      ArrowFunctionExpression(path) {
        // 把箭头函数节点转成普通函数表达式
        path.replaceWith(
          t.functionExpression(
            null,
            path.node.params,
            path.node.body
          )
        );
      }
    }
  };
};

1.3 生成(Generate)

修改后的 AST → @babel/generator → 目标代码字符串 + Source Map

@babel/generator 把修改后的 AST 重新生成为代码字符串。同时会生成 Source Map,方便调试时映射回原始代码。

1.4 完整流程示例

输入代码:
const greet = (name) => `Hello, ${name}`;

↓ Parse(@babel/parser)
AST(包含 ArrowFunctionExpression、TemplateLiteral 节点)

↓ Transform(plugins)
AST(ArrowFunction → FunctionExpression,TemplateLiteral → 字符串拼接)

↓ Generate(@babel/generator)
输出代码:
var greet = function (name) {
  return "Hello, " + name;
};

二、@babel/preset-env 与 browserslist

2.1 插件太多了怎么办?

ES6+ 有几十个新特性,每个特性对应一个 Babel 插件。手动配置这些插件既麻烦又容易遗漏。@babel/preset-env 就是为了解决这个问题的——它是一个插件集合(preset),会根据你指定的目标环境,自动确定需要启用哪些插件。

2.2 browserslist:告诉 Babel 你的目标浏览器

// .browserslistrc
last 2 versions
> 1%
not dead
not ie 11

或者在 package.json 中:

{
  "browserslist": [
    "last 2 versions",
    "> 1%",
    "not dead"
  ]
}

@babel/preset-env 会读取 browserslist 配置,查询 compat-table 数据库,确定哪些语法特性在目标浏览器中不被支持,然后只启用对应的转换插件。

例如: 如果你的目标浏览器都支持箭头函数,@babel/preset-env 就不会启用箭头函数转换插件——没必要转换已经支持的语法。这直接减小了产物体积。

2.3 配置示例

// babel.config.json
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "> 0.25%, not dead",
      "useBuiltIns": "usage",
      "corejs": 3,
      "modules": false
    }]
  ]
}

配置项解释:

配置项作用
targets目标浏览器(也可以用 .browserslistrc
useBuiltInspolyfill 注入方式(false / entry / usage
corejscore-js 版本号(使用 polyfill 时必填)
modules输出模块格式。设为 false 保留 ESM,方便 Webpack/Rollup 做 Tree Shaking

三、Polyfill 策略

3.1 语法 vs API

Babel 需要处理两类东西:

类型例子Babel 的处理方式
语法箭头函数、let/const、解构、可选链语法转换(编译时)
APIPromise、Array.from、Object.assign、Map/SetPolyfill(运行时)

语法转换在编译时完成——改变代码结构。API 补充在运行时完成——给全局或原型链添加新的方法。

3.2 三种 polyfill 策略

策略一:useBuiltIns: false(默认)

不注入任何 polyfill。你需要自己手动引入所有需要的 polyfill。

策略二:useBuiltIns: 'entry'

你在入口文件中写一行 import 'core-js/stable',Babel 会根据 browserslist 把它替换为目标浏览器缺失的所有 polyfill。

// 你写的
import 'core-js/stable';

// Babel 转换后(假设目标浏览器不支持 Promise 和 Array.from)
import 'core-js/modules/es.promise';
import 'core-js/modules/es.promise.finally';
import 'core-js/modules/es.array.from';
// ... 可能有上百行

优点: 简单、全面 缺点: 可能引入很多你实际没用到的 polyfill,产物偏大

策略三:useBuiltIns: 'usage'(推荐)

不需要手动 import。Babel 会分析你的代码,只引入你实际使用了的新 API 的 polyfill。

// 你写的
const arr = Array.from(new Set([1, 2, 3]));
const p = Promise.resolve(42);

// Babel 自动在文件顶部添加
import 'core-js/modules/es.array.from';
import 'core-js/modules/es.set';
import 'core-js/modules/es.promise';

优点: 按需引入,产物最小 缺点: 如果你的代码通过动态方式使用新 API(如 window[methodName]()),Babel 分析不出来,可能会遗漏

3.3 @babel/plugin-transform-runtime

上面的 polyfill 方式有一个问题:它们会污染全局环境——直接在 Array.prototype 上添加方法、在全局添加 Promise 等。

对于应用项目来说,这通常没问题。但如果你是在开发一个,污染全局环境就不合适了——你的库可能被引入到其他项目中,不应该改变别人的全局环境。

@babel/plugin-transform-runtime 解决这个问题:

npm install -D @babel/plugin-transform-runtime
npm install @babel/runtime-corejs3
{
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs": 3
    }]
  ]
}

它会把 polyfill 从”修改全局”变成”引入局部模块”:

// 不用 transform-runtime(污染全局)
Array.from([1, 2, 3]);
// 编译后:全局 Array.from 被 polyfill 修改

// 用了 transform-runtime(不污染全局)
import _from from '@babel/runtime-corejs3/core-js-stable/array/from';
_from([1, 2, 3]);

四、Babel 与 SWC 的对比

4.1 SWC 是什么?

SWC(Speedy Web Compiler)是一个用 Rust 编写的 JavaScript/TypeScript 编译器,由韩国开发者 Dongjoon Lee 创建。它的目标是做一个”和 Babel 功能相当但快得多”的编译器。

4.2 性能对比

SWC 宣称比 Babel 快 20-70 倍。这个差距来自:

维度Babel(JavaScript)SWC(Rust)
执行效率V8 JIT 编译,有 GC 开销原生编译,无 GC
并行能力单线程原生多线程
启动速度需要加载大量 JS 模块原生二进制,启动快

4.3 功能对比

功能BabelSWC
语法转换完善完善
TypeScript 转译通过 @babel/preset-typescript原生支持
JSX 转换支持支持
Polyfillcore-js 生态完善支持(通过插件)
自定义插件JavaScript 插件生态丰富Rust 插件(开发难度高)
Minification不是 Babel 的职责支持(替代 Terser)

4.4 SWC 的局限

  1. 插件生态:Babel 有海量的 JavaScript 插件,SWC 的 Rust 插件开发门槛高,生态远不如 Babel
  2. 自定义转换:如果你需要自定义 AST 转换,用 Babel 写 JavaScript 插件比用 SWC 写 Rust 插件简单得多
  3. 部分边界行为不一致:SWC 在某些边界情况下的转换结果可能和 Babel 不完全一致

4.5 SWC 的采用情况

  • Next.js 12+ 默认使用 SWC 替代 Babel
  • Vite 在开发模式下使用 esbuild(和 SWC 类似的思路)
  • Rspack 内置 SWC 作为默认编译器
  • Deno 使用 SWC 进行 TypeScript 转译

4.6 Babel 会被淘汰吗?

短期内不会。原因:

  1. 存量项目依赖 Babel 的生态(大量自定义插件)
  2. 需要精细控制 AST 转换的场景,Babel 的 JS 插件更灵活
  3. core-js polyfill 生态和 Babel 深度绑定

但长期来看,在不需要自定义插件的场景中,SWC 和 esbuild 正在逐步取代 Babel 的角色。


五、自定义 Babel 插件入门

5.1 插件的本质

Babel 插件就是一个函数,它返回一个包含 visitor 对象的配置。visitor 中的每个方法对应一种 AST 节点类型,Babel 遍历 AST 时会在遇到对应节点时调用你的方法。

5.2 一个简单的示例:去除 console.log

// babel-plugin-remove-console.js
module.exports = function () {
  return {
    visitor: {
      CallExpression(path) {
        const callee = path.node.callee;

        // 检查是否是 console.log / console.warn / console.error
        if (
          callee.type === 'MemberExpression' &&
          callee.object.name === 'console'
        ) {
          path.remove(); // 删除这个节点
        }
      }
    }
  };
};

使用:

// babel.config.json
{
  "plugins": ["./babel-plugin-remove-console"]
}

5.3 核心 API

API作用
path.node当前 AST 节点
path.parent父节点
path.remove()删除当前节点
path.replaceWith(node)替换当前节点
path.insertBefore(node)在当前节点前插入
path.insertAfter(node)在当前节点后插入
t.identifier('name')创建一个标识符节点
t.stringLiteral('hello')创建一个字符串字面量节点

t@babel/types,提供了创建各种 AST 节点的工厂方法。

5.4 调试技巧

推荐使用 AST Explorerhttps://astexplorer.net/)来查看代码对应的 AST 结构。它支持选择 Babel 解析器,还可以实时编写和测试 Babel 插件。

1. 打开 astexplorer.net
2. 左上角选择 JavaScript + @babel/parser
3. 在左侧面板输入你的代码
4. 右侧面板会显示对应的 AST
5. 点击某个节点,可以看到它的类型和属性

常见误区

误区一:“Babel 就是把 ES6 转成 ES5”

这个说法太笼统。Babel 转换的范围远不止 ES6 → ES5。它可以转换 JSX、TypeScript、Flow、甚至可以通过自定义插件做任意的代码变换。而且随着目标浏览器的更新,Babel 需要转换的东西越来越少——如果你的目标浏览器都支持 ES6,Babel 可能什么都不转换。

误区二:“Babel 能处理所有兼容性问题”

Babel 只处理语法转换。新的 API(如 PromisefetchIntersectionObserver)需要 polyfill。而且 Babel 也不处理 CSS 兼容性、HTML 兼容性等问题。它只是 JavaScript 编译器。

误区三:“useBuiltIns: 'usage' 能完美按需引入所有 polyfill”

usage 模式是基于静态代码分析的,如果你通过动态方式使用新 API(比如 obj[dynamicMethod]()),Babel 分析不出来。另外,第三方库中使用的新 API 也可能不会被 Babel 分析到(取决于 Babel 是否被配置为处理 node_modules 中的代码)。所以 usage 模式虽然好,但不是 100% 完美的。

误区四:“SWC 比 Babel 快,所以所有项目都该换 SWC”

速度只是一个维度。如果你的项目依赖了自定义 Babel 插件(如特定的代码转换、国际化方案的编译时处理等),SWC 可能做不到。迁移前要评估你的项目是否依赖了 Babel 特有的插件能力。如果只是标准的语法转换和 TypeScript 转译,那切换 SWC 通常是值得的。


小结

本章我们从 Babel 的三阶段工作流程(解析 → 转换 → 生成)讲起,深入理解了 @babel/preset-env 和 browserslist 的配合机制,对比了三种 polyfill 策略,讨论了 SWC 和 Babel 的优劣,并简单入门了自定义 Babel 插件。

核心要点

  1. Babel 三阶段:Parse(源码 → AST)→ Transform(插件修改 AST)→ Generate(AST → 代码)
  2. @babel/preset-env:根据 browserslist 自动确定需要哪些转换,避免手动管理几十个插件
  3. 语法 vs API:Babel 默认只转换语法,API 需要 polyfill(core-js)
  4. polyfill 策略usage(按需,推荐)> entry(全量)> false(不处理)
  5. transform-runtime:不污染全局,适合库开发
  6. SWC:Rust 实现,快 20-70 倍,但插件生态不如 Babel
  7. 自定义插件:访问者模式(Visitor Pattern),通过 path 操作 AST 节点

本章思维导图

Babel
  • 三阶段
    • Parse:源码 → AST(@babel/parser)
    • Transform:插件遍历和修改 AST(@babel/traverse)
    • Generate:AST → 代码 + Source Map(@babel/generator)
  • @babel/preset-env
    • 根据 browserslist 自动选择插件
    • modules: false 保留 ESM 供 Tree Shaking
  • Polyfill 策略
    • 语法转换 vs API 补充
    • useBuiltIns: false / entry / usage
    • core-js 提供 API polyfill
    • @babel/plugin-transform-runtime:不污染全局
  • vs SWC
    • SWC 用 Rust 实现,快 20-70 倍
    • Babel 插件生态更丰富、自定义更灵活
    • Next.js / Rspack 已经默认使用 SWC
  • 自定义插件
    • 访问者模式(Visitor Pattern)
    • path 对象操作 AST 节点
    • AST Explorer 辅助调试

练习挑战

第一题 ⭐(基础):概念辨析

下面的代码特性中,哪些属于”语法转换”(Babel 直接处理),哪些属于”API 补充”(需要 polyfill)?

  1. 箭头函数 () => {}
  2. Promise.resolve(42)
  3. 可选链 obj?.prop
  4. Array.from([1, 2, 3])
  5. 模板字符串 `hello ${name}`
  6. Object.assign({}, obj)
点击查看答案与解析
特性分类原因
箭头函数语法转换可以在编译时转成普通函数
Promise.resolveAPI 补充需要运行时 polyfill
可选链 ?.语法转换可以在编译时转成三元表达式
Array.fromAPI 补充需要运行时 polyfill
模板字符串语法转换可以在编译时转成字符串拼接
Object.assignAPI 补充需要运行时 polyfill

规律: 如果是新的语法结构(写法上的变化),Babel 可以在编译时转换。如果是新的API(全局方法或原型方法),需要运行时 polyfill。

第二题 ⭐⭐(进阶):分析 preset-env 行为

假设你的 browserslist 配置是 Chrome >= 80。以下代码经过 @babel/preset-env 处理后,哪些部分会被转换,哪些部分保持不变?

const greet = (name) => `Hello, ${name}`;
const arr = [1, 2, 3];
const [first, ...rest] = arr;
const result = arr?.map(x => x * 2) ?? [];
点击查看答案与解析

Chrome 80 已经支持以下所有特性:

  • 箭头函数(Chrome 45+)
  • 模板字符串(Chrome 41+)
  • 解构赋值(Chrome 49+)
  • Rest 参数(Chrome 47+)
  • 可选链 ?.(Chrome 80+)
  • 空值合并 ??(Chrome 80+)

所以所有代码都会保持不变,@babel/preset-env 不会转换任何东西。

这正是 @babel/preset-env 的价值——它只转换目标浏览器不支持的语法。如果目标浏览器已经支持,就不做多余的转换,产物更小、更高效。

如果把目标改成 IE 11,那所有这些语法都需要转换(IE 11 几乎不支持任何 ES6+ 语法)。

第三题 ⭐⭐⭐(综合):写一个 Babel 插件

请写一个 Babel 插件,把所有的 console.log(...) 调用替换为 logger.info(...)

要求:

  1. 只替换 console.log,不影响 console.warnconsole.error
  2. 保留原来的参数不变
  3. 假设 logger 是已经存在的全局变量
点击查看答案与解析
const { types: t } = require('@babel/core');

module.exports = function () {
  return {
    visitor: {
      CallExpression(path) {
        const { callee } = path.node;

        // 检查是否是 console.log 调用
        if (
          t.isMemberExpression(callee) &&
          t.isIdentifier(callee.object, { name: 'console' }) &&
          t.isIdentifier(callee.property, { name: 'log' })
        ) {
          // 替换为 logger.info,保留参数
          path.replaceWith(
            t.callExpression(
              t.memberExpression(
                t.identifier('logger'),
                t.identifier('info')
              ),
              path.node.arguments  // 保留原来的参数
            )
          );
        }
      }
    }
  };
};

转换效果:

// 输入
console.log('hello', name);
console.warn('warning');
console.error('error');

// 输出
logger.info('hello', name);   // ← 被替换
console.warn('warning');       // ← 不变
console.error('error');        // ← 不变

要点解析:

  • 使用 t.isMemberExpressiont.isIdentifier 精确匹配 console.log
  • 使用 t.callExpression + t.memberExpression 构造新的 logger.info(...) 调用
  • 保留 path.node.arguments 确保参数不变
  • 只匹配 log 方法,所以 warnerror 不受影响

自我检测

  • 能描述 Babel 的三阶段工作流程(Parse → Transform → Generate)
  • 能解释 AST 是什么,以及 Babel 为什么需要它
  • 能说出 @babel/preset-env 如何与 browserslist 配合,自动确定转换范围
  • 能区分”语法转换”和”API 补充”,举出各自的例子
  • 能比较 useBuiltIns 的三种值(false / entry / usage)的差异和适用场景
  • 能解释 @babel/plugin-transform-runtime 的作用和使用场景
  • 能从速度、生态、灵活性三个维度对比 Babel 和 SWC
  • 能写出一个简单的 Babel 插件(理解 Visitor Pattern 和 path API)

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90