Babel 概念与原理¶
概述¶
Babel
是一个流行的用于将新版本 ES6+ 代码转换为向后兼容版本(ES5)
代码的 JavaScript 编译器。它还为 JSX 语法提供了编译
支持,以及一些其他插件可用于转换特定类型的代码。
Babel 的作用¶
1. 语法转换¶
将 ES6+ 的新语法转换为 ES5,使代码可以在旧版浏览器中运行。
示例:
// 输入(ES6+)
const add = (a, b) => a + b;
class Person {
constructor(name) {
this.name = name;
}
}
// 输出(ES5)
var add = function add(a, b) {
return a + b;
};
var Person = function Person(name) {
this.name = name;
};
2. Polyfill 注入¶
为目标环境中缺少的功能添加 polyfill。
// 输入
const promise = Promise.resolve();
const arr = [1, 2, 3].includes(2);
// Babel 会注入必要的 polyfill
import "core-js/modules/es.promise";
import "core-js/modules/es.array.includes";
3. 源码转换¶
支持 JSX、TypeScript、Flow 等语法的转换。
// JSX 转换
const element = <h1>Hello, world!</h1>;
// 转换后
const element = React.createElement("h1", null, "Hello, world!");
Babel 工作原理¶
Babel 的工作原理可以分为三个主要阶段:解析(Parse)、转换(Transform)、生成(Generate)。
1. 解析(Parse)¶
当 Babel 接收到源代码时,将会调用一个叫做解析器的工具,用于将源代码转换为抽象语法树(AST)。
解析过程分为两个阶段:
词法分析(Lexical Analysis)¶
将代码字符串转换为令牌(tokens)流。
// 输入
const add = (a, b) => a + b;
// Tokens
[
{ type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'add' },
{ type: 'Punctuator', value: '=' },
{ type: 'Punctuator', value: '(' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: ',' },
{ type: 'Identifier', value: 'b' },
{ type: 'Punctuator', value: ')' },
{ type: 'Punctuator', value: '=>' },
// ...
]
语法分析(Syntactic Analysis)¶
将令牌流转换为 AST。
{
type: "VariableDeclaration",
kind: "const",
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" }
}
}
}
]
}
在这个过程中,解析器会识别代码中的语法结构,并将其转换为对应的节点类型。例如,当解析器遇到一个变量声明语句时,它将会创建一个 "VariableDeclaration" 节点,并将该节点的信息存储在 AST 中。
AST 是一个以节点为基础组成的树形结构,每个节点都有相应的类型、属性和子节点等信息。
2. 转换(Transform)¶
一旦 AST 被创建,Babel 将遍历整个树形结构,对每个节点进行转换。这些转换可以是插件、预设或手动创建的。
转换器会检查 AST 中的每个节点,然后对其进行相应的修改或替换,以将新语法转换为旧语法。
转换过程:
- 遍历 AST:使用访问者模式(Visitor Pattern)遍历 AST
- 应用插件:每个插件定义了对特定节点类型的转换规则
- 修改节点:根据转换规则修改、替换或删除节点
示例:箭头函数转换
// Babel 插件示例
module.exports = function({ types: t }) {
return {
visitor: {
ArrowFunctionExpression(path) {
// 将箭头函数转换为普通函数
const functionExpression = t.functionExpression(
null,
path.node.params,
t.blockStatement([
t.returnStatement(path.node.body)
])
);
path.replaceWith(functionExpression);
}
}
};
};
例如,如果 Babel 遇到一个包含箭头函数的节点,而你已经启用了转换插件,该插件将会将箭头函数转换为其等效的普通函数。
代码转换后,Babel 将会生成一个新的 AST。
3. 生成(Generate)¶
最后,Babel 将基于转换后的 AST 生成代码文本。
在这个步骤中,Babel 将遍历转换后的 AST,并创建对应的代码字符串,并将这些字符串组合成一个完整的 JavaScript 文件。如果启用了代码压缩,Babel 还可以将生成的代码进行压缩。
生成过程:
- **深度优先遍历**转换后的 AST
- 生成代码字符串:根据节点类型生成对应的代码
- 生成 Source Map:如果配置了,生成源码映射文件
Babel 配置¶
配置文件¶
Babel 支持多种配置文件格式:
babel.config.js
(推荐,项目范围配置)babel.config.json
.babelrc
(文件相对配置).babelrc.js
package.json
中的babel
字段
基本配置示例¶
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['> 1%', 'last 2 versions', 'not dead']
},
useBuiltIns: 'usage',
corejs: 3
}
],
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
[
'@babel/plugin-transform-runtime',
{
corejs: 3
}
]
]
};
Babel 核心概念¶
1. Presets(预设)¶
Presets 是一组插件的集合,用于支持特定的语法特性。
@babel/preset-env¶
最常用的预设,可以根据目标环境自动确定需要的转换。
{
presets: [
[
'@babel/preset-env',
{
// 目标环境
targets: {
chrome: '58',
ie: '11',
browsers: ['> 1%', 'last 2 versions']
},
// polyfill 策略
useBuiltIns: 'usage', // 'usage' | 'entry' | false
corejs: 3,
// 模块转换
modules: 'auto', // 'auto' | 'amd' | 'umd' | 'systemjs' | 'commonjs' | false
// 调试
debug: false
}
]
]
}
useBuiltIns 选项:
false
:不自动添加 polyfill'entry'
:在入口文件引入全部 polyfill'usage'
:按需引入 polyfill(推荐)
// useBuiltIns: 'entry'
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// useBuiltIns: 'usage'
// Babel 自动按需引入
@babel/preset-react¶
用于转换 JSX 语法。
{
presets: [
[
'@babel/preset-react',
{
runtime: 'automatic', // 'automatic' | 'classic'
development: process.env.NODE_ENV === 'development',
importSource: '@emotion/react' // 自定义 JSX runtime
}
]
]
}
@babel/preset-typescript¶
用于转换 TypeScript。
{
presets: [
[
'@babel/preset-typescript',
{
isTSX: true,
allExtensions: true,
allowNamespaces: true
}
]
]
}
2. Plugins(插件)¶
插件是 Babel 的核心,每个插件负责转换特定的语法特性。
常用插件¶
{
plugins: [
// 类属性
'@babel/plugin-proposal-class-properties',
// 装饰器
['@babel/plugin-proposal-decorators', { legacy: true }],
// 对象展开
'@babel/plugin-proposal-object-rest-spread',
// 可选链
'@babel/plugin-proposal-optional-chaining',
// 空值合并
'@babel/plugin-proposal-nullish-coalescing-operator',
// 动态 import
'@babel/plugin-syntax-dynamic-import'
]
}
@babel/plugin-transform-runtime¶
用于复用 Babel 注入的辅助代码,减小打包体积。
{
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3, // false | 2 | 3
helpers: true, // 是否复用辅助函数
regenerator: true, // 是否使用 regenerator
useESModules: true // 是否使用 ES 模块
}
]
]
}
对比:
// 不使用 transform-runtime
// 每个文件都会注入辅助函数
function _classCallCheck(instance, Constructor) { /* ... */ }
function _defineProperties(target, props) { /* ... */ }
// 使用 transform-runtime
// 从统一的模块引入
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _defineProperties from "@babel/runtime/helpers/defineProperties";
Babel 在不同场景中的使用¶
1. 在 Webpack 中使用¶
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true
}
}
}
]
}
};
2. 在 Vite 中使用¶
Vite 默认使用 esbuild 进行转译,但也可以配置 Babel:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }]
]
}
})
]
});
3. 在 Node.js 中使用¶
或使用 register:
4. 命令行使用¶
# 转换单个文件
npx babel src/app.js --out-file dist/app.js
# 转换整个目录
npx babel src --out-dir dist
# 监听模式
npx babel src --out-dir dist --watch
# 生成 source map
npx babel src --out-dir dist --source-maps
Polyfill 策略¶
1. 使用 @babel/polyfill(已废弃)¶
2. 使用 core-js + regenerator-runtime(推荐)¶
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3
}
]
]
};
3. 使用 @babel/plugin-transform-runtime¶
对比:
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
core-js + useBuiltIns | 按需引入,体积小 | 污染全局 | 应用开发 |
transform-runtime | 不污染全局 | 不支持实例方法 | 库开发 |
最佳实践¶
1. 针对不同环境使用不同配置¶
// babel.config.js
module.exports = function(api) {
const isProduction = api.env('production');
return {
presets: [
[
'@babel/preset-env',
{
targets: isProduction
? { browsers: ['> 1%', 'last 2 versions'] }
: { node: 'current' }
}
]
],
plugins: isProduction
? ['transform-remove-console']
: []
};
};
2. 启用缓存¶
// webpack.config.js
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
}
3. 减小转译范围¶
4. 使用 browserslist¶
在 package.json
中定义:
或创建 .browserslistrc
:
5. 性能优化¶
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
// 只转换需要的特性
targets: { esmodules: true },
// 按需引入 polyfill
useBuiltIns: 'usage',
corejs: 3,
// 不转换模块(让 webpack 处理)
modules: false
}
]
],
// 缓存配置
cacheDirectory: true,
// 缓存标识符
cacheIdentifier: process.env.NODE_ENV
};
常见问题¶
1. async/await 转换问题¶
问题: 使用 async/await 但运行时报错
解决:
或安装 regenerator-runtime
:
2. 装饰器语法问题¶
问题: 装饰器语法报错
解决:
{
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-class-properties'
]
}
注意:装饰器插件必须放在 class-properties 之前。
3. 动态 import 问题¶
问题: 动态 import 语法无法使用
解决:
总结¶
Babel 的原理就是将 JavaScript 源代码转换为抽象语法树(AST),然后对 AST 进行转换,生成与源代码功能相同但向后兼容的代码。Babel 提供了一个强大的生态系统,使得开发者可以轻松扩展并自定义转换器,实现自己的功能需求。