Babel 与 AST
一、Babel 概述
1.1 Babel 是什么
Babel 是一个 JavaScript 编译器,主要用于:
- 语法转换 (ES6+ → ES5)
- Polyfill (core-js)
- 源码转换 (JSX, TypeScript)
1.2 核心包
| 包名 | 作用 |
|---|---|
@babel/core | 核心编译引擎 |
@babel/cli | 命令行工具 |
@babel/preset-env | 智能预设,按目标环境转换 |
@babel/preset-react | React JSX 转换 |
@babel/preset-typescript | TypeScript 转换 |
二、Babel 编译流程
2.1 三个阶段
源代码 → Parse(解析) → AST → Transform(转换) → AST → Generate(生成) → 代码2.2 详细流程
1. Parse (解析)
├── 词法分析: 代码 → Tokens
└── 语法分析: Tokens → AST
2. Transform (转换)
└── 遍历 AST,应用插件转换
3. Generate (生成)
└── AST → 代码字符串2.3 示例
javascript
// 输入
const add = (a, b) => a + b;
// Parse 后的 AST (简化)
{
"type": "VariableDeclaration",
"declarations": [{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "add" },
"init": {
"type": "ArrowFunctionExpression",
"params": [
{ "type": "Identifier", "name": "a" },
{ "type": "Identifier", "name": "b" }
],
"body": {
"type": "BinaryExpression",
"operator": "+",
"left": { "type": "Identifier", "name": "a" },
"right": { "type": "Identifier", "name": "b" }
}
}
}]
}
// 输出
var add = function(a, b) { return a + b; };三、Babel 配置
3.1 配置文件
javascript
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead',
useBuiltIns: 'usage',
corejs: 3
}],
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-decorators',
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
};3.2 Preset 与 Plugin
- Plugin: 单个转换功能
- Preset: 一组 Plugin 的集合
执行顺序:
- Plugins 先于 Presets
- Plugins 按顺序执行
- Presets 逆序执行
3.3 Polyfill 策略
方式一: useBuiltIns
javascript
// babel.config.js
{
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 按需引入
corejs: 3
}]
]
}
// 缺点: 污染全局方式二: @babel/plugin-transform-runtime (推荐)
javascript
{
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3 // 使用 @babel/runtime-corejs3
}]
]
}
// 优点: 不污染全局,适合库开发四、AST 操作
4.1 AST 节点类型
| 类型 | 说明 | 示例 |
|---|---|---|
Identifier | 标识符 | foo |
Literal | 字面量 | 42, "hello" |
BinaryExpression | 二元表达式 | a + b |
CallExpression | 函数调用 | foo() |
FunctionDeclaration | 函数声明 | function foo() {} |
ArrowFunctionExpression | 箭头函数 | () => {} |
VariableDeclaration | 变量声明 | const a = 1 |
ImportDeclaration | 导入声明 | import x from 'y' |
4.2 AST 操作工具
javascript
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
// 1. 解析
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
});
// 2. 遍历转换
traverse(ast, {
// 访问 Identifier 节点
Identifier(path) {
if (path.node.name === 'foo') {
path.node.name = 'bar';
}
},
// 访问函数调用
CallExpression(path) {
// path.node - 当前节点
// path.parent - 父节点
// path.replaceWith() - 替换节点
// path.remove() - 删除节点
}
});
// 3. 生成代码
const output = generate(ast, {}, code);
console.log(output.code);五、Babel 插件开发
5.1 插件结构
javascript
module.exports = function(babel) {
const { types: t } = babel;
return {
name: 'my-plugin',
visitor: {
// 访问者模式
Identifier(path) {
// 处理 Identifier 节点
},
CallExpression(path) {
// 处理函数调用
}
}
};
};5.2 实战:移除 console.log
javascript
module.exports = function({ types: t }) {
return {
name: 'remove-console',
visitor: {
CallExpression(path) {
const { callee } = path.node;
// 匹配 console.log / console.warn 等
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: 'console' })
) {
path.remove();
}
}
}
};
};5.3 实战:自动导入 React
javascript
module.exports = function({ types: t }) {
return {
name: 'auto-import-react',
visitor: {
Program: {
enter(path) {
// 检查是否已导入 React
const hasReactImport = path.node.body.some(
node => t.isImportDeclaration(node) &&
node.source.value === 'react'
);
if (!hasReactImport) {
// 创建 import React from 'react'
const importDeclaration = t.importDeclaration(
[t.importDefaultSpecifier(t.identifier('React'))],
t.stringLiteral('react')
);
path.unshiftContainer('body', importDeclaration);
}
}
}
}
};
};5.4 实战:埋点插件
javascript
module.exports = function({ types: t }) {
return {
name: 'auto-tracker',
visitor: {
// 为函数添加埋点
FunctionDeclaration(path) {
const functionName = path.node.id.name;
// 创建 tracker 调用
const trackerCall = t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier('tracker'),
t.identifier('log')
),
[t.stringLiteral(functionName)]
)
);
// 插入到函数体开头
path.get('body').unshiftContainer('body', trackerCall);
}
}
};
};
// 转换前
function handleClick() {
doSomething();
}
// 转换后
function handleClick() {
tracker.log('handleClick');
doSomething();
}六、高频面试题
Q1: Babel 的编译流程?
- Parse: 将代码解析为 AST
- Transform: 遍历 AST,应用插件转换
- Generate: 将 AST 生成代码
Q2: Preset 和 Plugin 的执行顺序?
- Plugins 先于 Presets 执行
- Plugins 按配置顺序执行
- Presets 按配置逆序执行
Q3: useBuiltIns 的三个值的区别?
| 值 | 说明 |
|---|---|
false | 不自动注入 polyfill |
entry | 在入口处全量引入 |
usage | 按需引入使用到的 |
Q4: @babel/plugin-transform-runtime 的作用?
- 复用辅助函数,减少代码体积
- 使用沙箱化的 polyfill,不污染全局
- 适合库的开发