Skip to content

BabelJs - Babel 插件

BabelJS 是一个强大的 JavaScript 编译器。其主要作用是将现代 JavaScript 代码(ES2015+ 及更新版本)转换为向后兼容的版本,以便在旧的浏览器或环境中运行。Babel 编译过程通常包含三个主要阶段:

  • 解析 (Parsing):Babel 读取你的 JavaScript 代码,并将其转换为抽象语法树(Abstract Syntax Tree, AST),这是代码的结构化表示。
  • 转换 (Transforming):Babel 遍历 AST,并根据你配置的 plugins 和 presets 应用转换。这就是将新语法转换为旧语法的地方。
  • 打印(代码生成 - Printing/Code Generation):Babel 接收转换后的 AST,并从中生成新的 JavaScript 代码。

Babel 本身默认不改变你的代码语法。它依赖于 plugins 来完成实际的转换工作。Plugin 是一小段 JavaScript 代码,它告诉 Babel 如何转换特定的新语言特性。为了方便起见,Babel 还提供了 presets,它们是预定义的 plugin 集合。例如,@babel/preset-env 是一个智能 preset,它根据你的目标环境,自动包含针对在 TC39 流程中达到 Stage 4(完成)并成为 ECMAScript 标准特性的 plugins。

虽然 @babel/preset-env 处理了大多数常见的转换,但有时你可能需要特定的 plugins 用于:

  • 实验性的 JavaScript 特性(尚未标准化的提案)。
  • 通用 presets 未涵盖的特定优化或转换。
  • 非标准的 JavaScript 语法扩展(例如 React 中使用的 JSX)。

让我们探究一些常见的 JavaScript 特性如何通过 Babel 处理,这些特性通常通过 @babel/preset-env 中捆绑的 plugins 或可单独获取的 plugins 来实现,以便进行更精细的控制:

  • Class Properties (字段):例如 @babel/plugin-proposal-class-properties(注意:公共和私有 class 字段现在是标准的 ES2022 特性,通常由 @babel/preset-env 处理)。
  • 幂运算符 (**):例如 @babel/plugin-transform-exponentiation-operator(ES2016 标准,由 @babel/preset-env 处理)。
  • For…of 循环:例如 @babel/plugin-transform-for-of(ES2015 标准,由 @babel/preset-env 处理)。
  • 对象 Rest 和 Spread 属性 (...):例如 @babel/plugin-proposal-object-rest-spread(ES2018 标准,由 @babel/preset-env 处理)。
  • Async/Await:例如 @babel/plugin-transform-async-to-generator(ES2017 标准,由 @babel/preset-env 处理)。

为了说明这一点,让我们搭建一个现代 Babel 项目,并看看一些 plugins(通过 @babel/preset-env 隐式包含)的实际作用。

首先,初始化一个新的 Node.js 项目:

npm init -y

接下来,安装必需的 Babel 包。我们将使用 Babel 的核心、命令行接口(CLI)以及必要的 @babel/preset-env:

npm install --save-dev @babel/core @babel/cli @babel/preset-env

现在,创建一个 Babel 配置文件。你可以在项目根目录下使用 babel.config.json(或 .js, .cjs, .mjs):

babel.config.json 文件内容:

{
"presets": ["@babel/preset-env"]
}

这告诉 Babel 使用 @babel/preset-env 进行转换。默认情况下,除非通过指定目标环境进行配置,@babel/preset-env 会将代码转译为与 ES5 兼容的版本。

创建一个 JavaScript 文件,例如 main.js,用于编写你的现代 JS 代码。

示例 1:Class Properties(公共类字段)

Section titled “示例 1:Class Properties(公共类字段)”

公共类字段是现代 JavaScript 的一个特性(在 ES2022 中标准化),它允许你在类的 constructor 外部直接定义属性。

class Person {
// 公共类字段
name = "Alex Johnson";
age = 30;
// 作为类方法的箭头函数(正确绑定 'this')
greet = () => {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
const person = new Person();
console.log(person.greet());

如果你尝试在不支持 class 字段的旧浏览器中直接运行此代码,将会导致错误。让我们使用 Babel 转译它。

npx babel main.js --out-file main_compiled.js

该 @babel/preset-env(包含 @babel/plugin-proposal-class-properties 或其处理此特性的现代等价插件)将转换代码。

转译后的代码 (main_compiled.js - 简化示例)

Section titled “转译后的代码 (main_compiled.js - 简化示例)”
"use strict";
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
class Person {
constructor() {
_defineProperty(this, "name", "Alex Johnson");
_defineProperty(this, "age", 30);
_defineProperty(this, "greet", () => {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
});
}
}
const person = new Person();
console.log(person.greet());

请注意,class 字段 name、age 和箭头函数 greet 如何被移动到 constructor 中,并使用 _defineProperty(Babel 可能会注入的辅助函数)或类似机制定义。这使得代码与理解 ES5 class 的旧 JavaScript 环境兼容(ES5 class 本身是 constructor 函数和 prototype 的语法糖)。

运行 node main_compiled.js 将输出:

Hello, my name is Alex Johnson and I am 30 years old.

幂运算符 (**) 是一个用于计算幂的 ES2016 特性。

let squared = 3 ** 2; // 3 的 2 次方
let cubed = 2 ** 3; // 2 的 3 次方
console.log(`3 squared is ${squared}`);
console.log(`2 cubed is ${cubed}`);

@babel/preset-env 将包含必要的转换(例如 @babel/plugin-transform-exponentiation-operator)。

npx babel exponentiation.js --out-file exponentiation_compiled.js

转译后的代码 (exponentiation_compiled.js)

Section titled “转译后的代码 (exponentiation_compiled.js)”
"use strict";
let squared = Math.pow(3, 2); // 3 的 2 次方
let cubed = Math.pow(2, 3); // 2 的 3 次方
console.log(`3 squared is ${squared}`);
console.log(`2 cubed is ${cubed}`);

Babel 将 ** 转换为 Math.pow(),这是普遍支持的。

输出:

3 squared is 9
2 cubed is 8

for...of 循环(ES2015)用于迭代可迭代对象(如 Arrays、Strings、Maps、Sets)。

const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
console.log(fruit);
}
npx babel forof_example.js --out-file forof_example_compiled.js

转译后的代码 (forof_example_compiled.js - 简化示例)

Section titled “转译后的代码 (forof_example_compiled.js - 简化示例)”
"use strict";
const fruits = ["apple", "banana", "cherry"];
for (let _i = 0, _fruits = fruits; _i < _fruits.length; _i++) {
const fruit = _fruits[_i];
console.log(fruit);
}
// 注意:`for...of` 的实际转换可能更复杂,
// 特别是对于泛型可迭代对象,可能涉及 try/catch 和 iterators。
// 对于简单的数组,它可能会优化为标准的 for 循环。

输出:

apple
banana
cherry

常见学习难点:for...of 依赖于 Symbol.iterator 协议。如果你的目标环境非常旧,不支持 Symbol 或内置类型的 iterators,你还需要为 Symbol 和相关的 iterator 方法提供 polyfill(例如 core-js)。@babel/preset-env 可以通过其 useBuiltIns 选项来帮助管理这一点。更多详情请参阅关于 Polyfills 的章节。

对象 rest/spread(ES2018)允许轻松地将剩余属性收集到一个新对象中(rest)或合并对象(spread)。

const originalObject = { x: 1, y: 2, a: 3, b: 4 };
// 对象 Rest
const { x, y, ...rest } = originalObject;
console.log(x); // 1
console.log(y); // 2
console.log(rest); // { a: 3, b: 4 }
// 对象 Spread
const anotherObject = { c: 5, ...rest };
console.log(anotherObject); // { c: 5, a: 3, b: 4 }
npx babel object_rest_spread.js --out-file object_rest_spread_compiled.js

转译后的代码 (object_rest_spread_compiled.js - 简化示例)

Section titled “转译后的代码 (object_rest_spread_compiled.js - 简化示例)”
"use strict";
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
const originalObject = { x: 1, y: 2, a: 3, b: 4 };
// 对象 Rest
const { x, y } = originalObject,
rest = _objectWithoutProperties(originalObject, ["x", "y"]);
console.log(x); // 1
console.log(y); // 2
console.log(rest); // { a: 3, b: 4 }
// 对象 Spread
const anotherObject = _objectSpread({ c: 5 }, rest);
console.log(anotherObject); // { c: 5, a: 3, b: 4 }

Babel 使用辅助函数(rest 使用 _objectWithoutProperties,spread 使用 _objectSpread 或 Object.assign)来模拟其功能。

Async/await(ES2017)为处理 Promises 提供了语法糖,使异步代码看起来更像同步代码。

const timer = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function run() {
console.log("Starting...");
await timer(2000); // 在这里暂停 2 秒
console.log("Finished after 2 seconds!");
return "Done";
}
run().then(result => console.log(result));

Async 函数被 Babel 转换为 generator 函数,通常使用像 @babel/plugin-transform-async-to-generator 这样的 plugin(包含在 @babel/preset-env 中)。

npx babel async_await.js --out-file async_await_compiled.js

转译后的代码 (async_await_compiled.js - 简化示例)

Section titled “转译后的代码 (async_await_compiled.js - 简化示例)”
"use strict";
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function () {
var self = this, args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
const timer = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function run() {
return _run.apply(this, arguments);
}
function _run() {
_run = _asyncToGenerator(function* () {
console.log("Starting...");
yield timer(2000); // 在这里暂停 2 秒
console.log("Finished after 2 seconds!");
return "Done";
});
return _run.apply(this, arguments);
}
run().then(result => console.log(result));

常见学习难点:Async/await 依赖于 Promises。如果你的目标环境不原生支持 Promises(例如非常旧的浏览器),你需要一个 Promise polyfill。此外,转换为 generator 函数可能需要 regenerator-runtime。使用 core-js 的 @babel/preset-env 可以管理这些 polyfills。请参阅关于 Polyfills 的章节。

实际应用:在使用像 React 这样使用 JSX(一种需要 @babel/preset-react 的语法扩展)的框架时,或者在需要保持广泛浏览器兼容性的项目中采用前沿 JavaScript 特性时,Plugins 至关重要。

更多资源:有关 Babel 官方 plugins 的完整列表,请访问 Babel 文档网站。