主题
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: bPhase 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 的编译引擎,串联整个编译流程。它提供了 transform、transformSync、transformFile 等核心 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(如 Promise、Array.from、Object.assign、String.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 / IteratoruseBuiltIns 三种模式
┌────────────────────────────────────────────────────────────┐
│ 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: 3babel.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。只有当项目根目录存在 .babelrc 或 babel.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: ~30msRspack / 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 配置。
注意事项:
- 检查项目中是否有自定义 Babel 插件。如果有,需要评估是否有 SWC 替代方案,或者考虑只对特定文件保留 Babel。
const enum处理不同:Babel 直接删除const enum,SWC 则会将其转换为普通对象。- 装饰器行为可能有细微差异,需要测试。
- 部分 Babel 社区插件(如
babel-plugin-import、babel-plugin-macros)没有 SWC 对应,需要寻找替代方案或保留 Babel。 - 建议先在 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 对象。
- 一个 Babel 插件是一个函数,接收
api(包含types、template等工具)和options(用户配置),返回一个包含visitor对象的对象。 visitor的 key 是 AST 节点类型名,value 是处理函数。当@babel/traverse遍历 AST 时,遇到匹配的节点类型就会调用对应的处理函数。- 处理函数接收
path和state。path提供了当前节点的上下文信息和操作 API(replaceWith、remove、insertBefore等)。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(压缩)。
延伸阅读
- Babel 官方文档 — 最权威的配置参考和 API 文档
- Babel Plugin Handbook — Jamie Kyle 编写的 Babel 插件开发手册,必读
- SWC 官方文档 — SWC 配置和 API 参考
- AST Explorer — 在线 AST 可视化和插件调试工具
- esbuild 官方文档 — esbuild 设计理念和 API
- the-super-tiny-compiler — 一个 200 行的微型编译器实现,帮助理解编译原理
- Rust 性能之书 — 理解 SWC 为什么快的底层原因
- core-js 作者博客 — 了解 Polyfill 生态的最新动态
- Babel RFC 仓库 — 跟踪 Babel 的设计决策和未来方向
- SWC 插件开发指南 — 用 Rust 编写 SWC WASM 插件