Skip to content

Babel 与 SWC

编译原理基础

为什么前端需要编译?

现代前端开发中,我们编写的代码和浏览器实际执行的代码往往存在巨大差异。JSX、TypeScript、ES2024+ 语法、CSS Modules——这些都不是浏览器能直接理解的。编译器(Compiler)和转译器(Transpiler)承担了这个桥梁角色,将开发者友好的高级代码转换为浏览器可执行的低级代码。

开发者编写的代码                    浏览器执行的代码
┌──────────────────┐              ┌──────────────────┐
│  JSX             │              │  React.createElement │
│  TypeScript      │   ──────►   │  ES5 / ES6       │
│  ES2024+         │   编译/转译  │  Polyfill 注入    │
│  装饰器语法       │              │  模块打包         │
└──────────────────┘              └──────────────────┘

在这个领域,Babel 统治了前端编译近十年,而 Rust 编写的 SWC 正以惊人的速度崛起。理解它们的原理和差异,是每个前端工程师的必修课。

编译三阶段

无论是 Babel、SWC、还是传统编译器(如 GCC、LLVM),编译过程都遵循经典的三阶段模型:

                        编译三阶段
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   源代码 (Source Code)                                       │
│       │                                                     │
│       ▼                                                     │
│   ┌─────────────────────────────────────────┐               │
│   │  Phase 1: Parse(解析)                   │               │
│   │                                         │               │
│   │  ┌───────────┐    ┌───────────────────┐ │               │
│   │  │ 词法分析    │ →  │ 语法分析           │ │               │
│   │  │ Lexical    │    │ Syntactic         │ │               │
│   │  │ Analysis   │    │ Analysis          │ │               │
│   │  │            │    │                   │ │               │
│   │  │ 源码→Tokens │    │ Tokens→AST        │ │               │
│   │  └───────────┘    └───────────────────┘ │               │
│   └─────────────────────────────────────────┘               │
│       │                                                     │
│       ▼  AST(抽象语法树)                                    │
│                                                             │
│   ┌─────────────────────────────────────────┐               │
│   │  Phase 2: Transform(转换)               │               │
│   │                                         │               │
│   │  遍历 AST 节点,按规则进行增删改           │               │
│   │  ├── 语法降级 (ES2024 → ES5)            │               │
│   │  ├── JSX 转换 (JSX → createElement)     │               │
│   │  ├── TypeScript 类型擦除                 │               │
│   │  └── 自定义插件转换                       │               │
│   └─────────────────────────────────────────┘               │
│       │                                                     │
│       ▼  新 AST                                              │
│                                                             │
│   ┌─────────────────────────────────────────┐               │
│   │  Phase 3: Generate(代码生成)             │               │
│   │                                         │               │
│   │  将转换后的 AST 重新输出为代码字符串        │               │
│   │  同时生成 Source Map 映射关系              │               │
│   └─────────────────────────────────────────┘               │
│       │                                                     │
│       ▼                                                     │
│   目标代码 (Target Code) + Source Map                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Phase 1: Parse(解析)

解析阶段又分为两个子步骤:

词法分析(Lexical Analysis / Tokenization):将源代码字符串拆分为一系列有意义的最小单元——Token(词法单元)。

源代码: const x = 1 + 2;

词法分析后的 Tokens:
┌──────────┬─────────────┬──────────┐
│  Token   │   Type      │  Value   │
├──────────┼─────────────┼──────────┤
│  const   │  Keyword    │  const   │
│  x       │  Identifier │  x       │
│  =       │  Punctuator │  =       │
│  1       │  Numeric    │  1       │
│  +       │  Punctuator │  +       │
│  2       │  Numeric    │  2       │
│  ;       │  Punctuator │  ;       │
└──────────┴─────────────┴──────────┘

语法分析(Syntactic Analysis / Parsing):根据语法规则,将 Token 序列组织成树形结构——AST。

const x = 1 + 2;

语法分析后的 AST 结构:

        Program

    VariableDeclaration (kind: "const")

    VariableDeclarator
        /        \
  Identifier    BinaryExpression (operator: "+")
  (name: "x")      /            \
              NumericLiteral   NumericLiteral
              (value: 1)       (value: 2)

Phase 2: Transform(转换)

转换阶段是编译器的核心。它遍历 AST,对每个节点应用一系列转换规则。这也是 Babel 插件工作的阶段。

转换示例: 箭头函数 → 普通函数

源 AST:                          目标 AST:
ArrowFunctionExpression          FunctionExpression
├── params: [a, b]               ├── params: [a, b]
└── body: BinaryExpression       └── body: BlockStatement
    ├── left: a                      └── ReturnStatement
    ├── operator: +                      └── BinaryExpression
    └── right: b                             ├── left: a
                                             ├── operator: +
                                             └── right: b

Phase 3: Generate(代码生成)

代码生成阶段将转换后的 AST 重新序列化为代码字符串,同时生成 Source Map 以便调试时能映射回源代码。

新 AST → 代码生成器 → "function(a, b) { return a + b; }"
                   └→ Source Map (原始位置 ↔ 生成位置)

AST(抽象语法树)

AST 是编译过程中最核心的数据结构。它以树的形式表达源代码的语法结构,忽略了空格、注释、分号等对语义无关的信息(这就是"抽象"的含义)。

常见 AST 节点类型

以下是 JavaScript AST(遵循 ESTree 规范和 Babel AST 扩展)中最常见的节点类型:

AST 节点类型层级:
┌─ Program                          顶层程序节点

├─ Statement(语句类)
│  ├── VariableDeclaration          变量声明 (const/let/var)
│  ├── FunctionDeclaration          函数声明
│  ├── ExpressionStatement          表达式语句
│  ├── ReturnStatement              返回语句
│  ├── IfStatement                  条件语句
│  ├── ForStatement                 循环语句
│  ├── BlockStatement               代码块 { ... }
│  ├── ImportDeclaration            导入声明
│  └── ExportDeclaration            导出声明

├─ Expression(表达式类)
│  ├── Identifier                   标识符 (变量名/函数名)
│  ├── Literal / NumericLiteral     字面量 (数字/字符串/布尔)
│  ├── BinaryExpression             二元表达式 (a + b)
│  ├── CallExpression               函数调用 (fn())
│  ├── MemberExpression             成员访问 (obj.prop)
│  ├── ArrowFunctionExpression      箭头函数
│  ├── ObjectExpression             对象字面量
│  ├── ArrayExpression              数组字面量
│  ├── ConditionalExpression        三元表达式
│  ├── TemplateLiteral              模板字符串
│  └── JSXElement                   JSX 元素 (Babel 扩展)

├─ Pattern(模式类)
│  ├── ObjectPattern                对象解构 { a, b }
│  └── ArrayPattern                 数组解构 [a, b]

└─ Other
   ├── TemplateElement              模板字符串片段
   ├── SpreadElement                展开运算符
   └── ClassDeclaration             类声明

AST Explorer 工具

AST Explorer 是学习和调试 AST 最强大的工具。它支持多种解析器(@babel/parser、acorn、typescript、swc 等),可以实时查看代码对应的 AST 结构。

使用方式:

1. 打开 https://astexplorer.net/
2. 左上角选择语言:JavaScript
3. 选择解析器:@babel/parser
4. 左侧输入代码,右侧实时显示 AST
5. 点击右侧 AST 节点,左侧代码对应部分会高亮
6. 顶部可切换到 Transform 模式,测试 Babel 插件

┌──────────────────┬──────────────────┐
│   Source Code     │     AST Tree     │
│                  │                  │
│  const a = 1;   │  Program         │
│                  │   └─ Variable... │
│                  │      └─ Varia... │
├──────────────────┼──────────────────┤
│  Transform Code  │  Transform Output│
│  (插件代码)       │  (转换结果)       │
└──────────────────┴──────────────────┘

一段代码的完整 AST

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

对应的 AST 结构(简化版):

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

Babel 深入

Babel 是什么

Babel 是一个 JavaScript 编译器(更准确地说是转译器——Transpiler),它的核心使命是让开发者能使用最新的 JavaScript 语法,同时保证代码在旧版浏览器中正常运行。

Babel 的定位:
┌───────────────────────────────────────────────────┐
│                                                   │
│  ES2024+ / JSX / TS  ──→  Babel  ──→  ES5 / ES6  │
│                                                   │
│  "Use next generation JavaScript, today."         │
│                                                   │
└───────────────────────────────────────────────────┘

核心包架构

Babel 采用高度模块化的设计,核心功能被拆分为多个独立的包:

Babel 核心包关系:

                    ┌──────────────┐
                    │  @babel/core  │
                    │   编译核心引擎  │
                    └──────┬───────┘

          ┌────────────────┼────────────────┐
          ▼                ▼                ▼
  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
  │ @babel/parser │ │@babel/traverse│ │@babel/generator│
  │   解析器      │ │   遍历器      │ │   代码生成器   │
  │  Code → AST  │ │ AST 遍历/修改 │ │  AST → Code  │
  └──────────────┘ └──────────────┘ └──────────────┘
          │                │
          ▼                ▼
  ┌──────────────┐ ┌──────────────┐
  │ @babel/types  │ │@babel/template│
  │  AST 工具库   │ │ AST 模板构建  │
  │ 创建/校验节点  │ │  快速创建 AST │
  └──────────────┘ └──────────────┘

@babel/core

Babel 的编译引擎,串联整个编译流程。它提供了 transformtransformSynctransformFile 等核心 API:

javascript
const babel = require('@babel/core');

const result = babel.transformSync('const a = () => 1;', {
  presets: ['@babel/preset-env'],
});

console.log(result.code);
console.log(result.map);
console.log(result.ast);

@babel/parser

将源代码字符串解析为 AST。它是 Babylon 的继承者,支持所有 ES2024+ 语法、JSX、TypeScript、Flow 等:

javascript
const parser = require('@babel/parser');

const ast = parser.parse('const x = 1 + 2;', {
  sourceType: 'module',
  plugins: ['jsx', 'typescript', 'decorators-legacy'],
});

常用配置选项:

sourceType:
├── "module"    ─ ESM 模式,支持 import/export
├── "script"    ─ 传统脚本模式
└── "unambiguous" ─ 自动判断(推荐)

plugins:
├── "jsx"               ─ 解析 JSX 语法
├── "typescript"        ─ 解析 TypeScript 语法
├── "decorators-legacy" ─ 解析旧版装饰器
├── "classProperties"   ─ 解析类属性
└── "optionalChaining"  ─ 解析可选链

@babel/traverse

遍历和修改 AST 的核心工具。它基于访问者模式(Visitor Pattern),允许你在遍历过程中对特定类型的节点进行操作:

javascript
const traverse = require('@babel/traverse').default;
const parser = require('@babel/parser');

const ast = parser.parse('const x = 1 + 2;');

traverse(ast, {
  BinaryExpression(path) {
    if (path.node.operator === '+') {
      const left = path.node.left.value;
      const right = path.node.right.value;
      if (typeof left === 'number' && typeof right === 'number') {
        path.replaceWith({ type: 'NumericLiteral', value: left + right });
      }
    }
  },
});

@babel/generator

将 AST 转换回代码字符串,支持 Source Map 生成和格式化选项:

javascript
const generate = require('@babel/generator').default;

const output = generate(ast, {
  sourceMaps: true,
  retainLines: false,
  compact: false,
  minified: false,
});

console.log(output.code);
console.log(output.map);

@babel/types

AST 节点的工具库,提供了创建、校验、判断 AST 节点的方法。它是手写 Babel 插件的必备工具:

javascript
const t = require('@babel/types');

const identifier = t.identifier('myVar');

const numLiteral = t.numericLiteral(42);

const callExpr = t.callExpression(
  t.memberExpression(t.identifier('console'), t.identifier('log')),
  [t.stringLiteral('hello')]
);

t.isIdentifier(identifier);
t.isNumericLiteral(numLiteral);

t.assertIdentifier(identifier);

Preset 机制

Preset(预设)是一组 Plugin 的集合,避免开发者逐个配置数十个插件。

Preset 本质:
┌─────────────────────────────────────────────┐
│  @babel/preset-env                          │
│                                             │
│  = @babel/plugin-transform-arrow-functions  │
│  + @babel/plugin-transform-block-scoping    │
│  + @babel/plugin-transform-classes          │
│  + @babel/plugin-transform-destructuring    │
│  + @babel/plugin-transform-spread           │
│  + @babel/plugin-transform-template-literals│
│  + ... (数十个插件的组合)                     │
│                                             │
│  根据 targets 自动选择需要的插件              │
└─────────────────────────────────────────────┘

@babel/preset-env

最核心的 Preset,负责将 ES2015+ 语法转换为目标环境兼容的代码。它根据你指定的目标环境(targets),只转换目标环境不支持的语法,避免不必要的转换。

json
{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "chrome": "90",
        "firefox": "88",
        "safari": "14",
        "edge": "90"
      },
      "useBuiltIns": "usage",
      "corejs": "3.37",
      "modules": false,
      "debug": true
    }]
  ]
}

关键配置项解析:

targets — 指定目标运行环境:

targets 的多种写法:

1. 指定具体浏览器版本
   "targets": { "chrome": "90", "ie": "11" }

2. 使用 Browserslist 查询语法
   "targets": "> 0.25%, not dead"

3. 指定 Node.js 版本
   "targets": { "node": "14" }

4. 使用 current node 版本
   "targets": { "node": "current" }

targets 的工作流:
┌──────────┐     ┌──────────────┐     ┌───────────────┐
│ targets  │ ──► │ compat-table │ ──► │ 计算需要的     │
│ 配置     │     │ 兼容性数据库   │     │ 转换插件列表   │
└──────────┘     └──────────────┘     └───────────────┘

useBuiltIns — 控制 Polyfill 注入策略(详见 Polyfill 策略章节)。

modules — 控制模块系统的转换:

modules 选项:
├── false     ─ 不转换模块语法(推荐!保留 ESM,交给打包器处理 Tree-Shaking)
├── "auto"    ─ 默认值,根据环境自动判断
├── "commonjs"─ 转换为 CJS
├── "amd"     ─ 转换为 AMD
├── "umd"     ─ 转换为 UMD
└── "systemjs"─ 转换为 SystemJS

@babel/preset-typescript

负责 TypeScript 到 JavaScript 的转换。它的工作原理是类型擦除(Type Erasure)——仅移除类型注解,不做类型检查。

json
{
  "presets": [
    ["@babel/preset-typescript", {
      "isTSX": true,
      "allExtensions": true,
      "allowNamespaces": true,
      "allowDeclareFields": true
    }]
  ]
}
TypeScript 类型擦除过程:

源码:
function add(a: number, b: number): number {
  return a + b;
}

interface User {
  name: string;
  age: number;
}

         ┌──────────────────────────┐
    ──►  │  @babel/preset-typescript │  ──►
         │  移除所有类型注解           │
         │  移除 interface/type       │
         │  转换 enum                 │
         └──────────────────────────┘

输出:
function add(a, b) {
  return a + b;
}

注意事项: Babel 的 TypeScript 处理有以下限制:

Babel 处理 TS 的局限:
┌─────────────────────────────────────────────────────┐
│  ✗ 不做类型检查(需要单独运行 tsc --noEmit)           │
│  ✗ 不支持 const enum(会被当作普通 enum)              │
│  ✗ 不支持 namespace 合并                              │
│  ✗ 不支持 export = / import = 语法                    │
│  ✗ 对 decorator metadata 的支持不完整                  │
│  ✓ 支持 enum(转换为 IIFE)                            │
│  ✓ 支持 type / interface 擦除                         │
│  ✓ 支持 as / satisfies / ! 断言                       │
└─────────────────────────────────────────────────────┘

@babel/preset-react

处理 React JSX 语法的转换。支持经典模式和自动模式(React 17+ 新 JSX Transform):

json
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic",
      "importSource": "react",
      "development": true,
      "throwIfNamespace": true
    }]
  ]
}
两种 JSX 转换模式:

经典模式 (runtime: "classic"):
<div className="app">
  <h1>Hello</h1>
</div>

React.createElement("div", { className: "app" },
  React.createElement("h1", null, "Hello")
);

自动模式 (runtime: "automatic",React 17+):
<div className="app">
  <h1>Hello</h1>
</div>

import { jsx as _jsx } from "react/jsx-runtime";
_jsx("div", { className: "app",
  children: _jsx("h1", { children: "Hello" })
});

自动模式的优势:
├── 不需要手动 import React
├── 产物体积略小
└── 未来性能优化空间更大

Plugin 机制

插件执行顺序

Babel 的插件和 Preset 有严格的执行顺序规则:

Babel 执行顺序规则:

1. Plugins 先于 Presets 执行
2. Plugins 从左到右(从上到下)执行
3. Presets 从右到左(从下到上)执行(反序!)

配置示例:
{
  "plugins": ["A", "B", "C"],
  "presets": ["D", "E", "F"]
}

实际执行顺序:
A → B → C → F → E → D
         ▲           ▲
    plugins 正序   presets 反序

为什么 Presets 反序?
这是为了保证配置的直觉性。通常我们这样写:
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}

实际执行:typescript → react → jsx → env
这样 TypeScript 先被擦除,然后 JSX 被转换,最后 ES 语法降级

插件的本质

一个 Babel 插件本质上是一个返回 Visitor 对象的函数:

javascript
module.exports = function myPlugin(api, options, dirname) {
  return {
    name: 'my-plugin',
    visitor: {
      Identifier(path, state) {
        if (path.node.name === 'foo') {
          path.node.name = 'bar';
        }
      },
    },
  };
};
插件结构解析:

module.exports = function(api, options, dirname) {
                           │      │        │
                           │      │        └── 当前配置文件所在目录
                           │      └── 用户传入的插件选项
                           └── Babel API 对象 (api.types, api.template 等)
  return {
    name: 'plugin-name',    ← 插件名(调试用)
    pre(state) {},          ← 遍历 AST 前的钩子
    visitor: {              ← 访问者对象(核心)
      NodeType(path, state) {}
    },
    post(state) {},         ← 遍历 AST 后的钩子
  };
};

Polyfill 策略

语法转换只是 Babel 工作的一半。对于新的 API(如 PromiseArray.fromObject.assignString.prototype.includes),仅靠语法转换是不够的——需要注入 Polyfill。

语法 vs API 的区别:

语法(Syntax)—— Babel 可以转换
├── 箭头函数       () => {}
├── 解构赋值       const { a } = obj
├── 可选链         obj?.a?.b
├── 空值合并       a ?? b
└── class 语法     class Foo {}

API —— 需要 Polyfill
├── Promise
├── Map / Set / WeakMap / WeakSet
├── Array.from / Array.of
├── Object.assign / Object.entries
├── String.prototype.includes
├── Array.prototype.flat
├── globalThis
└── Symbol / Iterator

useBuiltIns 三种模式

┌────────────────────────────────────────────────────────────┐
│  useBuiltIns: false (默认)                                │
│                                                            │
│  Babel 完全不处理 Polyfill,需要开发者自行引入              │
│  适合:自己控制 Polyfill 策略的场景                         │
│                                                            │
│  产物中不会有任何 core-js 引入                              │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│  useBuiltIns: 'entry'                                      │
│                                                            │
│  将入口处的 import "core-js" 替换为                         │
│  目标环境需要的所有 Polyfill 模块                           │
│                                                            │
│  源码入口需要手动写:                                       │
│  import "core-js/stable";                                  │
│  import "regenerator-runtime/runtime";                     │
│                                                            │
│  Babel 会根据 targets 将其展开为:                          │
│  import "core-js/modules/es.promise";                      │
│  import "core-js/modules/es.array.from";                   │
│  import "core-js/modules/es.string.includes";              │
│  ... (所有 targets 不支持的 Polyfill)                       │
│                                                            │
│  优点:确保完整 Polyfill                                    │
│  缺点:引入了项目中未使用的 Polyfill                        │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│  useBuiltIns: 'usage'(推荐)                               │
│                                                            │
│  自动检测代码中实际使用的 API,按需注入 Polyfill             │
│  不需要手动 import "core-js"                                │
│                                                            │
│  源码:                                                     │
│  const p = new Promise(() => {});                          │
│  [1, 2, 3].includes(1);                                   │
│                                                            │
│  Babel 自动在文件顶部注入:                                 │
│  import "core-js/modules/es.promise.js";                   │
│  import "core-js/modules/es.array.includes.js";            │
│                                                            │
│  优点:最小化 Polyfill 体积                                 │
│  缺点:无法检测第三方库中使用的 API                          │
└────────────────────────────────────────────────────────────┘

@babel/plugin-transform-runtime

useBuiltIns 方案有一个核心问题:全局污染。它直接修改全局对象和原型(如 Array.prototype.includes),这在库开发场景中是不可接受的。

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

全局污染 vs 无污染对比:

useBuiltIns: 'usage' 的输出(全局污染):
import "core-js/modules/es.promise.js";
var p = new Promise(function () {});

@babel/plugin-transform-runtime 的输出(无污染):
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
var p = new _Promise(function () {});
json
{
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs": 3,
      "helpers": true,
      "regenerator": true
    }]
  ]
}
两种 Polyfill 方案对比:

┌─────────────────┬───────────────────┬────────────────────────┐
│                 │  useBuiltIns      │  transform-runtime     │
├─────────────────┼───────────────────┼────────────────────────┤
│  全局污染       │  ✓ 污染全局        │  ✗ 不污染全局           │
│  实例方法       │  ✓ 支持           │  ✗ 不支持               │
│  (如 includes)  │                   │  (corejs3 部分支持)     │
│  适用场景       │  应用项目          │  库/工具包开发           │
│  包体积         │  较小(按需)        │  略大(辅助函数复用)      │
│  配置复杂度     │  简单              │  中等                   │
│  依赖          │  core-js           │  @babel/runtime-corejs3│
└─────────────────┴───────────────────┴────────────────────────┘

选择建议:
├── 应用项目 → useBuiltIns: 'usage' + corejs: 3
└── 库开发   → @babel/plugin-transform-runtime + corejs: 3

babel.config.json vs .babelrc

Babel 支持两种配置文件格式,它们在作用域和行为上有重要区别:

┌────────────────────┬───────────────────────┬────────────────────────┐
│                    │  babel.config.json     │  .babelrc (.babelrc.json)│
├────────────────────┼───────────────────────┼────────────────────────┤
│  作用范围          │  项目级(Project-wide) │  目录级(Relative)      │
│  查找方式          │  从 root 目录加载       │  向上查找到最近的配置     │
│  是否影响          │  ✓ 影响所有文件         │  ✗ 只影响当前包内的文件   │
│  node_modules      │  包括 node_modules     │  不包括 node_modules    │
│  Monorepo 支持     │  ✓ 天然支持            │  ✗ 需要额外配置          │
│  动态配置          │  ✓ 支持 .js/.cjs/.mjs  │  仅静态 JSON            │
└────────────────────┴───────────────────────┴────────────────────────┘

在 Monorepo 中的典型用法:

Monorepo 项目结构:

my-monorepo/
├── babel.config.json        ← 项目级配置(所有包共享)
├── packages/
│   ├── app/
│   │   ├── .babelrc.json    ← 包级配置(覆盖/扩展项目配置)
│   │   └── src/
│   ├── ui-lib/
│   │   ├── .babelrc.json    ← ui-lib 专属配置
│   │   └── src/
│   └── utils/
│       └── src/             ← 使用项目级配置
└── node_modules/

配置合并策略:
app 内的文件 → .babelrc.json + babel.config.json(合并)
utils 内的文件 → babel.config.json(仅项目级)

babel.config.json 示例:
{
  "presets": ["@babel/preset-env"],
  "overrides": [
    {
      "test": "packages/ui-lib/**",
      "presets": [["@babel/preset-react", { "runtime": "automatic" }]]
    }
  ]
}

手写 Babel 插件

访问者模式(Visitor Pattern)

访问者模式是 Babel 插件系统的核心设计模式。它将数据结构操作分离——AST 是数据结构,插件是操作。

访问者模式工作流:

AST 遍历过程(深度优先):

        Program
        ┌──┴──┐
        │     │
      enter  exit
        ↓     ↑
  VariableDeclaration
    ┌───┴───┐
    │       │
  enter    exit
    ↓       ↑
  VariableDeclarator
    ┌───┴───┐
    │       │
  enter    exit
    ↓       ↑
 Identifier
  ┌──┴──┐
  │     │
enter  exit

每个节点都有两次被访问的机会:
├── enter:进入节点时(向下遍历)
└── exit:离开节点时(向上回溯)
javascript
const visitor = {
  Identifier: {
    enter(path) {
      console.log('Entering:', path.node.name);
    },
    exit(path) {
      console.log('Exiting:', path.node.name);
    },
  },

  FunctionDeclaration(path) {
    console.log('Found function:', path.node.id.name);
  },
};

path API

path 是 Babel 中操作 AST 的核心对象。它不仅包含当前节点的信息,还提供了丰富的操作 API:

path 对象结构:

path
├── node          当前 AST 节点
├── parent        父节点
├── parentPath    父节点的 path
├── scope         作用域信息
├── type          节点类型
├── key           在父节点中的属性名
├── listKey       在父节点数组中的键名
├── container      所在容器
├── inList        是否在数组中

├── 查询方法
│   ├── get(key)             获取子路径
│   ├── find(callback)       向上查找满足条件的 path
│   ├── findParent(callback) 向上查找父 path
│   ├── getSibling(key)      获取兄弟路径
│   ├── getNextSibling()     下一个兄弟
│   ├── getPrevSibling()     上一个兄弟
│   │
├── 判断方法
│   ├── isIdentifier()       是否是标识符
│   ├── isMemberExpression() 是否是成员表达式
│   ├── isReferencedIdentifier() 是否是被引用的标识符
│   ├── isDescendant(path)   是否是某 path 的后代
│   │
├── 修改方法
│   ├── replaceWith(node)        替换为新节点
│   ├── replaceWithMultiple([])  替换为多个节点
│   ├── replaceWithSourceString()替换为源码字符串
│   ├── insertBefore(node)       在前面插入
│   ├── insertAfter(node)        在后面插入
│   │
├── 删除方法
│   ├── remove()             删除当前节点
│   │
└── 遍历控制
    ├── skip()               跳过子节点的遍历
    └── stop()               停止整棵树的遍历

state API

state 是插件的状态对象,用于在不同 Visitor 方法之间传递数据:

javascript
module.exports = function (api, options) {
  return {
    visitor: {
      Program: {
        enter(path, state) {
          state.count = 0;
          state.filename = state.file.opts.filename;
        },
        exit(path, state) {
          console.log(`Found ${state.count} matches in ${state.filename}`);
        },
      },
      CallExpression(path, state) {
        state.count++;
      },
    },
  };
};
state 对象结构:

state
├── opts            用户传入的插件选项
├── file            当前文件信息
│   ├── opts        文件编译选项
│   │   ├── filename    文件名
│   │   ├── sourceRoot  源文件根目录
│   │   └── ...
│   └── path        文件根 path
├── filename        文件路径(快捷访问)
├── cwd             当前工作目录
└── [自定义属性]     可以在 state 上挂载任意数据

实战:手写移除 console.log 的插件

这是最经典的 Babel 插件入门案例。在生产构建中,我们通常需要移除所有 console.log 调用以减少不必要的输出。

javascript
module.exports = function ({ types: t }) {
  return {
    name: 'remove-console-log',
    visitor: {
      CallExpression(path, state) {
        const { node } = path;
        const { callee } = node;

        if (
          t.isMemberExpression(callee) &&
          t.isIdentifier(callee.object, { name: 'console' }) &&
          t.isIdentifier(callee.property, { name: 'log' })
        ) {
          path.remove();
        }
      },
    },
  };
};

进阶版本——支持配置移除的方法和排除文件:

javascript
module.exports = function ({ types: t }) {
  return {
    name: 'remove-console',
    visitor: {
      CallExpression(path, state) {
        const { node } = path;
        const { callee } = node;
        const { opts = {} } = state;
        const methods = opts.exclude || ['log', 'debug', 'info'];

        if (!t.isMemberExpression(callee)) return;
        if (!t.isIdentifier(callee.object, { name: 'console' })) return;
        if (!t.isIdentifier(callee.property)) return;
        if (!methods.includes(callee.property.name)) return;

        if (opts.preserveFiles) {
          const filename = state.filename || '';
          if (opts.preserveFiles.some((f) => filename.includes(f))) {
            return;
          }
        }

        const { leadingComments } = node;
        if (leadingComments) {
          const hasKeep = leadingComments.some((comment) =>
            comment.value.includes('@keep')
          );
          if (hasKeep) return;
        }

        path.remove();
      },
    },
  };
};

使用配置:

json
{
  "plugins": [
    ["./plugins/remove-console", {
      "exclude": ["log", "debug", "info", "warn"],
      "preserveFiles": ["debug-utils.js"]
    }]
  ]
}
转换效果:

源码:
console.log('debug info');
console.error('critical error');
console.warn('some warning');
const result = calculate();
console.debug('result:', result);

转换后 (exclude: ['log', 'debug', 'warn']):
console.error('critical error');
const result = calculate();

实战:手写自动埋点插件

自动埋点是 Babel 插件在生产中最常见的应用之一。它可以自动在函数入口处注入追踪代码,无需开发者手动埋点。

javascript
const template = require('@babel/template').default;

const buildTracker = template(`
  __tracker(FUNCTION_NAME, FILENAME, LINE, COLUMN)
`);

module.exports = function ({ types: t }) {
  return {
    name: 'auto-tracker',
    visitor: {
      'FunctionDeclaration|ArrowFunctionExpression|FunctionExpression'(
        path,
        state
      ) {
        if (path.node._tracked) return;
        path.node._tracked = true;

        const { filename = 'unknown' } = state;
        const { line, column } = path.node.loc?.start || {
          line: 0,
          column: 0,
        };

        let funcName = 'anonymous';
        if (path.isFunctionDeclaration() && path.node.id) {
          funcName = path.node.id.name;
        } else if (
          path.parentPath.isVariableDeclarator() &&
          t.isIdentifier(path.parentPath.node.id)
        ) {
          funcName = path.parentPath.node.id.name;
        } else if (
          path.parentPath.isObjectProperty() &&
          t.isIdentifier(path.parentPath.node.key)
        ) {
          funcName = path.parentPath.node.key.name;
        }

        const trackerAST = buildTracker({
          FUNCTION_NAME: t.stringLiteral(funcName),
          FILENAME: t.stringLiteral(filename),
          LINE: t.numericLiteral(line),
          COLUMN: t.numericLiteral(column),
        });

        if (path.isArrowFunctionExpression() && !t.isBlockStatement(path.node.body)) {
          const returnStatement = t.returnStatement(path.node.body);
          const blockBody = t.blockStatement([trackerAST, returnStatement]);
          path.node.body = blockBody;
        } else {
          const body = path.get('body');
          if (body.isBlockStatement()) {
            body.unshiftContainer('body', trackerAST);
          }
        }
      },
    },
  };
};
转换效果:

源码:
function handleClick(e) {
  doSomething();
}

const fetchData = async () => {
  return await api.get('/data');
};

转换后:
function handleClick(e) {
  __tracker("handleClick", "src/app.js", 1, 0);
  doSomething();
}

const fetchData = async () => {
  __tracker("fetchData", "src/app.js", 5, 22);
  return await api.get('/data');
};

更完善的埋点插件还可以支持以下功能:

高级埋点功能扩展思路:

┌──────────────────────────────────────────────┐
│  1. 按注释跳过埋点                             │
│     /* @no-track */                           │
│     function internal() { ... }               │
│                                               │
│  2. 配置只对特定目录/文件埋点                   │
│     { include: ['src/pages/**'] }             │
│                                               │
│  3. 注入执行耗时统计                           │
│     const __start = Date.now()                │
│     ... 函数原始逻辑 ...                       │
│     __tracker(name, Date.now() - __start)     │
│                                               │
│  4. 自动导入 tracker SDK                       │
│     import { __tracker } from '@my/tracker';  │
│                                               │
│  5. 结合 Source Map 还原线上错误位置            │
└──────────────────────────────────────────────┘

SWC

SWC 简介

SWC(Speedy Web Compiler)是一个用 Rust 编写的超高速 JavaScript/TypeScript 编译器。由 Donny(韩国开发者,GitHub: @kdy1)于 2019 年创建,现已被 Vercel 收购并深度集成到 Next.js 中。

SWC 核心特点:

┌──────────────────────────────────────────────┐
│  🦀 Rust 编写                                 │
│     └── 零成本抽象、内存安全、无 GC 开销        │
│                                               │
│  ⚡ 极致性能                                   │
│     └── 单线程比 Babel 快 20 倍                │
│     └── 多线程比 Babel 快 70 倍                │
│                                               │
│  📦 多功能一体                                 │
│     ├── 编译(Transpilation)                  │
│     ├── 压缩(Minification)                   │
│     ├── 打包(Bundling,实验性)                │
│     └── 类型擦除(TypeScript)                 │
│                                               │
│  🔌 可通过 WASM 在 Node.js 中使用              │
│     └── @swc/core, @swc/cli                   │
│                                               │
│  🤝 高度兼容 Babel 生态                        │
│     └── 支持大部分 Babel 配置项                 │
└──────────────────────────────────────────────┘

为什么 SWC 这么快?

性能差异的根本原因:

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Babel (JavaScript)                                         │
│  ├── V8 JIT 编译 → 仍是解释型语言,有运行时开销               │
│  ├── 垃圾回收(GC)→ 频繁的内存分配和回收暂停                 │
│  ├── 动态类型 → 运行时类型检查开销                            │
│  ├── 单线程 → 无法利用多核 CPU                               │
│  └── AST 以 JSON-like 对象表示 → 大量堆分配                  │
│                                                             │
│  SWC (Rust)                                                 │
│  ├── AOT 编译为原生机器码 → 零运行时开销                      │
│  ├── 无 GC → 编译时确定内存生命周期(所有权系统)              │
│  ├── 静态类型 → 零运行时类型检查开销                          │
│  ├── 原生多线程 → 充分利用多核 CPU                           │
│  ├── AST 使用 Arena 分配 → 内存局部性好,缓存友好             │
│  └── SIMD 指令优化 → 字符串处理极快                          │
│                                                             │
│  编译 10000 个模块的对比:                                    │
│  Babel:  ~45s                                               │
│  SWC:    ~0.8s (单线程) / ~0.2s (4线程)                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

.swcrc 配置

SWC 的配置文件为 .swcrc,支持 JSON 格式:

json
{
  "$schema": "https://swc.rs/schema.json",
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true,
      "decorators": true,
      "dynamicImport": true
    },
    "transform": {
      "react": {
        "runtime": "automatic",
        "importSource": "react",
        "development": false,
        "refresh": false
      },
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "target": "es2020",
    "loose": false,
    "externalHelpers": true,
    "keepClassNames": true,
    "minify": {
      "compress": {
        "dead_code": true,
        "drop_console": true,
        "passes": 2
      },
      "mangle": true
    }
  },
  "module": {
    "type": "es6",
    "strict": true,
    "noInterop": false
  },
  "minify": true,
  "sourceMaps": true,
  "env": {
    "targets": {
      "chrome": "90"
    },
    "mode": "usage",
    "coreJs": "3.37"
  }
}
.swcrc 关键配置解析:

jsc
├── parser
│   ├── syntax        ─ "ecmascript" | "typescript"
│   ├── tsx/jsx       ─ 是否启用 JSX/TSX
│   └── decorators    ─ 是否启用装饰器

├── transform
│   ├── react
│   │   ├── runtime   ─ "automatic" | "classic"
│   │   └── refresh   ─ React Fast Refresh
│   └── legacyDecorator ─ 旧版装饰器

├── target            ─ 输出目标 ("es5" ~ "es2022")
├── loose             ─ 宽松模式(类似 Babel loose)
├── externalHelpers   ─ 复用辅助函数(类似 @babel/runtime)
└── minify            ─ 内置压缩配置

module
├── type              ─ "es6" | "commonjs" | "amd" | "umd"
└── strict            ─ 严格模式

env                   ─ 等同于 @babel/preset-env
├── targets           ─ 目标环境
├── mode              ─ "usage" | "entry"
└── coreJs            ─ core-js 版本

SWC 的 Node.js API

javascript
const swc = require('@swc/core');

const result = await swc.transform('const x = (a) => a + 1;', {
  jsc: {
    parser: { syntax: 'ecmascript' },
    target: 'es5',
  },
  sourceMaps: true,
});

console.log(result.code);
console.log(result.map);
javascript
const swc = require('@swc/core');
const path = require('path');

const result = await swc.transformFile(path.resolve('./src/app.tsx'), {
  jsc: {
    parser: {
      syntax: 'typescript',
      tsx: true,
    },
    transform: {
      react: { runtime: 'automatic' },
    },
    target: 'es2020',
  },
});

SWC 与 Babel 的兼容性差异

SWC 对 Babel 的兼容性:

┌──────────────────────────┬──────────┬──────────────────────┐
│  功能                    │  支持状态 │  说明                 │
├──────────────────────────┼──────────┼──────────────────────┤
│  ES2015+ 语法转换         │  ✓ 完全  │  与 Babel 基本一致    │
│  JSX 转换                │  ✓ 完全  │  支持 automatic/classic│
│  TypeScript 擦除         │  ✓ 完全  │  与 Babel 行为一致    │
│  装饰器                  │  ✓ 完全  │  legacy + 新版        │
│  useBuiltIns/core-js     │  ✓ 完全  │  env.mode 配置       │
│  React Fast Refresh      │  ✓ 完全  │  内置支持             │
│  Emotion/Styled-comp     │  ✓ 内置  │  无需额外插件         │
│  自定义 JS 插件           │  △ 有限  │  仅通过 WASM 插件     │
│  Babel 插件生态           │  ✗ 不兼容│  无法直接使用 Babel 插件│
│  babel-plugin-macros     │  ✗ 不支持│  无替代方案           │
│  Flow 类型              │  ✗ 不支持│  仅支持 TypeScript    │
└──────────────────────────┴──────────┴──────────────────────┘
SWC 插件系统(实验性):

Babel 插件:JavaScript 编写,同步执行

SWC 插件:Rust 编写 → 编译为 WASM → 在 SWC 中加载执行

SWC WASM 插件示例目录结构:
my-swc-plugin/
├── Cargo.toml
├── src/
│   └── lib.rs
└── target/
    └── wasm32-wasi/
        └── my_swc_plugin.wasm

局限性:
├── 必须用 Rust 编写
├── 生态远不如 Babel 丰富
├── 调试比 JS 插件困难
└── WASM 插件有性能开销(但仍比 Babel 快)

SWC 在现代框架中的应用

Next.js

Next.js 12+ 默认使用 SWC 替代 Babel。只有当项目根目录存在 .babelrcbabel.config.js 时,才会回退到 Babel。

javascript
const nextConfig = {
  compiler: {
    styledComponents: true,
    removeConsole: {
      exclude: ['error'],
    },
    reactRemoveProperties: true,
    emotion: true,
  },
  experimental: {
    swcPlugins: [['@swc/plugin-styled-components', {}]],
  },
};

module.exports = nextConfig;
Next.js SWC 编译链路:

                  ┌───────────────────────┐
  .tsx/.ts/.jsx   │                       │  .js (ES2020)
  ──────────────► │  SWC (内置在 Next.js)  │ ──────────────►
                  │                       │
                  └───────────────────────┘

              ┌────────────┼────────────┐
              ▼            ▼            ▼
        TypeScript      JSX 转换    语法降级
        类型擦除     (automatic)   (targets)

  有 .babelrc 时的回退:
  ──────────────► Babel ──────────────►
  (性能下降约 17 倍)

Vite

Vite 通过 @vitejs/plugin-react-swc 使用 SWC 替代默认的 Babel:

javascript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';

export default defineConfig({
  plugins: [
    react({
      tsDecorators: true,
    }),
  ],
});
Vite 中的编译器选择:

@vitejs/plugin-react (默认)
└── 使用 Babel 编译 JSX
└── 支持所有 Babel 插件
└── HMR: React Fast Refresh (via Babel)

@vitejs/plugin-react-swc (推荐)
└── 使用 SWC 编译 JSX
└── 不支持 Babel 插件
└── HMR: React Fast Refresh (via SWC)
└── 开发启动速度提升明显

性能对比(大型项目 HMR):
plugin-react:      ~300ms
plugin-react-swc:  ~30ms

Rspack / Rsbuild

字节跳动开源的 Rspack(Rust 版 Webpack)内置了 SWC 作为默认编译器:

javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'typescript',
                tsx: true,
              },
              transform: {
                react: {
                  runtime: 'automatic',
                },
              },
            },
          },
        },
      },
    ],
  },
};

Babel vs SWC vs esbuild vs TypeScript Compiler

全面对比

┌──────────────┬─────────────┬──────────────┬──────────────┬──────────────┐
│              │   Babel     │    SWC       │   esbuild    │   tsc        │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  实现语言     │ JavaScript  │ Rust         │ Go           │ TypeScript   │
│              │             │              │              │  (自举)      │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  编译速度     │ 1x (基准)   │ 20-70x       │ 10-100x      │ 2-5x        │
│              │             │              │              │ (transpileOnly)│
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  类型检查     │ ✗ 不支持    │ ✗ 不支持      │ ✗ 不支持     │ ✓ 完整支持   │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  语法降级     │ ✓ 完整      │ ✓ 完整       │ △ 有限       │ ✓ 完整       │
│  能力        │ ES3+        │ ES5+         │ ES2015+      │ ES3+         │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  JSX 支持    │ ✓           │ ✓            │ ✓            │ ✓            │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  TS 支持     │ ✓ 类型擦除   │ ✓ 类型擦除   │ ✓ 类型擦除    │ ✓ 完整编译   │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  Polyfill    │ ✓ core-js   │ ✓ core-js    │ ✗ 不支持     │ ✗ 需手动     │
│              │ 完整方案     │ 完整方案      │              │              │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  插件系统     │ ✓ 成熟      │ △ WASM插件   │ △ Go插件     │ ✗ 无         │
│              │ JS 编写     │ Rust 编写    │ (有限)       │ (有 Transformer │
│              │ 生态丰富     │ 生态较少     │              │  API,不常用) │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  压缩能力     │ ✗ 需 terser │ ✓ 内置       │ ✓ 内置       │ ✗ 无         │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  打包能力     │ ✗ 无        │ △ 实验性      │ ✓ 内置       │ ✗ 无         │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  Source Map  │ ✓           │ ✓            │ ✓            │ ✓            │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  产物质量     │ ✓ 优秀      │ ✓ 优秀       │ △ 中等       │ ✓ 优秀       │
│              │ 可读性好     │ 接近 Babel   │ 有时冗余     │ 可读性好     │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  代表性用户   │ CRA, 大多数 │ Next.js,     │ Vite(dev),   │ Angular,     │
│              │ Webpack项目 │ Rspack,Deno  │ tsup,tsx     │ 独立使用     │
├──────────────┼─────────────┼──────────────┼──────────────┼──────────────┤
│  维护方       │ 社区        │ Vercel       │ Evan Wallace │ Microsoft    │
│              │ (Sebastian) │ (Donny)      │ (Figma CTO)  │              │
└──────────────┴─────────────┴──────────────┴──────────────┴──────────────┘

编译速度基准测试

编译 1000 个 React 组件文件(含 TypeScript + JSX)的耗时对比:

          Babel        SWC (1线程)   SWC (4线程)   esbuild
           │              │              │            │
  10s ─── ████████████   │              │            │
           │  8.2s        │              │            │
   5s ─── │              │              │            │
           │              │              │            │
   1s ─── │             ██              │            │
           │              │  0.41s       │            │
 0.5s ─── │              │              │            │
           │              │             ██           ██
 0.1s ─── │              │              │  0.12s     │ 0.09s
           │              │              │            │
   0s ─── └──────────────┴──────────────┴────────────┘

选型建议

不同场景下的选型建议:

┌────────────────────────────────────────────────────────┐
│  场景 1: 新建 React 应用                                │
│  推荐:SWC (via Next.js 或 Vite + plugin-react-swc)   │
│  理由:开箱即用,性能极佳,覆盖 99% 需求               │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│  场景 2: 需要自定义编译逻辑(如自动埋点、国际化替换)     │
│  推荐:Babel                                           │
│  理由:插件系统成熟,JS 编写门槛低,社区插件丰富         │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│  场景 3: 纯 TypeScript 库开发                           │
│  推荐:tsc(类型检查)+ SWC 或 esbuild(编译产物)      │
│  理由:兼顾类型安全和编译速度                           │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│  场景 4: 开发 CLI 工具 / 快速打包小型库                  │
│  推荐:esbuild (via tsup)                              │
│  理由:编译+打包一体化,配置最简                        │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│  场景 5: 历史 Webpack 项目升级                          │
│  推荐:swc-loader 替换 babel-loader                    │
│  理由:最小改动获得最大性能提升                          │
│                                                        │
│  // webpack.config.js                                  │
│  // 替换前                                              │
│  { loader: 'babel-loader', options: {...} }            │
│  // 替换后                                              │
│  { loader: 'swc-loader', options: {...} }              │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│  场景 6: Monorepo 大型项目                              │
│  推荐:SWC + tsc --noEmit                              │
│  理由:SWC 处理编译(快),tsc 只做类型检查(准)        │
│                                                        │
│  // package.json                                       │
│  "scripts": {                                          │
│    "build": "swc src -d dist",                         │
│    "typecheck": "tsc --noEmit"                         │
│  }                                                     │
└────────────────────────────────────────────────────────┘

esbuild 的特殊定位

esbuild 常被拿来和 SWC 对比,但它们的核心定位不同:

esbuild vs SWC 定位差异:

esbuild:
├── 核心目标:极速打包器 (Bundler)
├── 编译只是打包的一部分
├── 不支持降级到 ES5
├── 不支持 Polyfill
├── 压缩能力优秀但不如 terser
└── 在 Vite 中用于:dev server 的 TS/JSX 编译 + 生产依赖预构建

SWC:
├── 核心目标:极速编译器 (Compiler)
├── 编译功能完整,对标 Babel
├── 支持降级到 ES5
├── 支持 Polyfill (core-js)
├── 内置压缩器
└── 在 Next.js/Rspack 中用于:完整的 JS/TS 编译

面试高频问题

Q1: Babel 的编译流程是什么?请详细说明每个阶段做了什么

回答思路:

三阶段模型:Parse → Transform → Generate。

Parse 阶段分为词法分析和语法分析。词法分析将源代码拆分为 Token 序列(关键字、标识符、运算符、字面量等);语法分析根据语法规则将 Token 组织成 AST(抽象语法树)。@babel/parser 负责此阶段。

Transform 阶段遍历 AST,使用访问者模式(Visitor Pattern)对节点进行增删改操作。每个 Babel 插件本质上就是定义了一组 Visitor,针对特定节点类型进行转换。@babel/traverse 负责遍历,@babel/types 辅助节点操作。

Generate 阶段将修改后的 AST 序列化为目标代码字符串,同时生成 Source Map。@babel/generator 负责此阶段。

追问:为什么 Babel 选择 AST 作为中间表示?直接用正则替换不行吗?

正则替换无法理解代码的语义结构。比如要把所有 var 替换为 let,正则会误替换字符串 "var" 中的内容或变量名 variable 中的子串。AST 提供了语义级别的精确操作能力——你可以准确定位到 VariableDeclaration 节点并修改其 kind 属性。

Q2: useBuiltIns 的三种模式有什么区别?项目中应该怎么选?

回答思路:

false:Babel 不处理 Polyfill,需要开发者手动引入。适合有专门 Polyfill 管理方案的项目。

entry:需要在入口文件手动 import "core-js/stable",Babel 根据 targets 将其替换为目标环境缺失的所有 Polyfill 模块。优点是 Polyfill 覆盖全面,缺点是会引入项目中未实际使用的 Polyfill。

usage:Babel 自动分析代码中使用的 API,按需注入 Polyfill。不需要手动引入。这是推荐模式,产物体积最小。但注意它无法检测第三方库中使用的新 API。

对于应用项目,推荐 usage + corejs: 3;对于库开发,推荐 @babel/plugin-transform-runtime + corejs: 3 以避免全局污染。

追问:为什么库开发不能用 useBuiltIns?

因为 useBuiltIns 会直接修改全局对象(如 globalThis.Promise)和原型(如 Array.prototype.includes)。如果你的库这样做,会影响使用你的库的宿主项目——可能导致 Polyfill 版本冲突或意外的全局副作用。transform-runtime 通过引入局部变量(如 import _Promise from "@babel/runtime-corejs3/core-js-stable/promise")来避免全局污染。

Q3: Babel 插件和 Preset 的执行顺序是什么?为什么这样设计?

回答思路:

Plugins 先于 Presets 执行。Plugins 按声明顺序从左到右(从上到下)执行。Presets 按声明顺序反向执行,即从右到左(从下到上)。

Presets 反向执行的设计是为了配置的直觉性。我们通常把最基础的 Preset(如 @babel/preset-env)放在第一位,把更具体的(如 @babel/preset-typescript)放在后面。反向执行保证了 TypeScript 先被处理(类型擦除),然后再进行 ES 语法降级,这符合逻辑上的处理顺序。

Q4: SWC 为什么比 Babel 快这么多?

回答思路:

根本原因在于语言层面的差异。Babel 用 JavaScript 编写,运行在 V8 引擎上,虽然 V8 的 JIT 编译已经很优秀,但仍有 GC 暂停、动态类型检查、无法真正多线程等固有开销。SWC 用 Rust 编写,编译为原生机器码执行,零 GC 开销(通过所有权系统管理内存),静态类型(编译时确定内存布局),且可以利用多核 CPU 并行编译。

具体到 AST 操作层面,Babel 的 AST 是 JS 对象(大量堆分配),而 SWC 使用 Arena 分配器(内存局部性好,对 CPU 缓存友好)。SWC 还利用了 Rust 的零成本抽象特性和 SIMD 指令优化字符串处理。

单线程下 SWC 约快 20 倍,开启多线程后可达 70 倍。

Q5: 在 Webpack 项目中如何从 Babel 迁移到 SWC?有哪些注意事项?

回答思路:

最直接的方式是将 babel-loader 替换为 swc-loader,并将 Babel 配置转换为 SWC 配置。

注意事项:

  1. 检查项目中是否有自定义 Babel 插件。如果有,需要评估是否有 SWC 替代方案,或者考虑只对特定文件保留 Babel。
  2. const enum 处理不同:Babel 直接删除 const enum,SWC 则会将其转换为普通对象。
  3. 装饰器行为可能有细微差异,需要测试。
  4. 部分 Babel 社区插件(如 babel-plugin-importbabel-plugin-macros)没有 SWC 对应,需要寻找替代方案或保留 Babel。
  5. 建议先在 CI 中并行运行两套编译,对比产物差异后再完全切换。

Q6: babel.config.json 和 .babelrc 有什么区别?Monorepo 中应该怎么配?

回答思路:

babel.config.json 是项目级配置(Project-wide),从项目根目录加载,影响项目中所有文件(包括 node_modules)。.babelrc 是相对配置(Relative),只影响当前目录及其子目录中的文件,且不会跨越包边界。

在 Monorepo 中,推荐在根目录使用 babel.config.json(或 babel.config.js)定义所有子包共享的基础配置,然后在需要特殊配置的子包中使用 .babelrc.json 进行覆盖或扩展。babel.config.json 支持 overrides 字段,可以针对不同路径模式应用不同的配置,这在 Monorepo 中非常实用。

需要注意的是,如果子包中有 .babelrc,需要确保 babel.config.json 中没有设置 configFile: false,否则 .babelrc 会被忽略。

Q7: 如何手写一个 Babel 插件?请说明核心概念和步骤

回答思路:

核心概念有三个:访问者模式(Visitor Pattern)、path 对象、state 对象。

  1. 一个 Babel 插件是一个函数,接收 api(包含 typestemplate 等工具)和 options(用户配置),返回一个包含 visitor 对象的对象。
  2. visitor 的 key 是 AST 节点类型名,value 是处理函数。当 @babel/traverse 遍历 AST 时,遇到匹配的节点类型就会调用对应的处理函数。
  3. 处理函数接收 pathstatepath 提供了当前节点的上下文信息和操作 API(replaceWithremoveinsertBefore 等)。state 用于跨 Visitor 传递状态。

开发步骤:先在 AST Explorer 中分析目标代码的 AST 结构,确定需要操作的节点类型;然后编写 Visitor 函数,使用 @babel/types 进行节点判断和创建;最后通过 path API 进行节点的增删改。

追问:访问者模式为什么适合 AST 操作?

AST 节点类型繁多(几十种),操作也多样(语法降级、代码注入、类型擦除等)。如果在节点类上直接定义操作方法,每新增一种操作就要修改所有节点类。访问者模式将数据结构(AST 节点)和操作(插件逻辑)解耦——新增操作只需添加新的 Visitor,不影响现有节点类。这就是经典的"开放封闭原则"。

Q8: 现代前端项目中,Babel、SWC、esbuild、tsc 应该如何组合使用?

回答思路:

现代前端项目通常采用分层策略:

类型检查层:始终使用 tsc --noEmit。无论用什么编译器,TypeScript 的类型检查都不可替代。通常在 CI/CD 中运行,或通过 IDE 实时检查。

编译层:根据项目情况选择 SWC 或 Babel。新项目首选 SWC(或使用 SWC 的框架如 Next.js),有复杂自定义编译需求的项目用 Babel。

打包层:Webpack/Rspack/Vite/Rollup 等,它们各自集成了不同的编译器。Vite dev server 用 esbuild 做 TS/JSX 转换,生产构建用 Rollup + SWC/Babel。

压缩层:esbuild 或 SWC 的内置压缩器正在逐步替代 terser,速度快得多。

典型组合:tsc --noEmit(类型检查)+ SWC(编译)+ Rspack/Vite(打包)+ SWC minify(压缩)。


延伸阅读

用心学习,用代码说话 💻