文章目录
Babel插件的作用
Babel 是一款通过插件转换代码的 JavaScript 编译器。掌握如何编写 Babel 插件,可以构建自定义代码转换、工具链与自动化重构。
- 解析(Parse):源码 → AST(抽象语法树)
- 转换(Transform):AST → 修改后的 AST(通过插件/预设)
- 生成(Generate):修改后的 AST → 输出代码
Babel插件结构
Babel 插件是一个函数:接收 babel 对象,返回包含 visitor 的对象:
// 基本插件结构
module.exports = function (babel) {
// babel 内含:types、template、traverse、generate 等
const { types: t } = babel;
return {
// visitor:键为 AST 节点类型名
visitor: {
// 遍历到 Identifier 节点时调用
Identifier(path, state) {
// 在此编写转换逻辑
},
},
};
};
Babel核心API
types(t)
const { types: t } = babel;
// 类型检查
t.isIdentifier(node); // 判断是否为该节点类型
t.isIdentifier(node, { name: "x" }); // 带属性约束的检查
t.isNumericLiteral(node); // 是否为数字字面量
// 构造不同类型的节点
t.identifier("x"); // x
t.numericLiteral(42); // 42
t.stringLiteral("hello"); // 'hello'
t.binaryExpression("+", left, right); // left + right
t.callExpression(callee, args); // callee(args)
t.memberExpression(obj, prop); // obj.prop
t.arrowFunctionExpression(params, body); // (params) => body
t.functionDeclaration(id, params, body); // function id(params) {}
t.variableDeclaration("const", [declarator]); // const x = y
t.objectExpression([property]); // { key: value }
t.arrayExpression([elements]); // [elements]
t.templateLiteral(quasis, exprs); // `string ${expr}`
// 节点操作
t.cloneNode(node); // 深拷贝节点
t.removeProperties(node); // 移除特殊属性
t.validate(node, key, value); // 校验节点属性
traverse
const traverse = require("@babel/traverse").default;
// 手动遍历 AST
traverse(ast, {
Identifier(path) {
console.log(path.node.name);
// 默认会继续进入子节点
// path.skip(); // 跳过子节点
// path.stop(); // 停止整次遍历
},
FunctionDeclaration(path) {
// 访问父节点
const parent = path.parent;
// 访问兄弟节点所在容器
const siblings = path.parentPath.container;
// 获取作用域
const scope = path.scope;
},
});
template
const template = require("@babel/template").default;
// 简单模板
const buildRequire = template(`
require(SOURCE)
`);
// 用法:buildRequire({ SOURCE: t.stringLiteral('./module') })
// 带占位符的模板
const buildWrapper = template(`
(function() {
BODY;
})()
`);
// 用法:buildWrapper({ BODY: statementNode })
// 需要额外语法插件的导出模板
const buildExport = template(`
module.exports = EXPRESSION
`, { plugins: ["proposal-export-default-from"] });
generate
const generate = require("@babel/generate").default;
const output = generate(
ast,
{
sourceMaps: true, // 生成 source map
comments: true, // 保留注释
compact: false, // 是否压缩输出
},
code
);
// output = { code: '...', map: sourceMap }
path对象
Identifier(path) {
// 节点相关
path.node; // 当前 AST 节点
path.parent; // 父节点
path.parentPath; // 父 path
// 控制遍历
path.skip(); // 不再遍历子节点
path.stop(); // 停止整次遍历
// 替换节点
path.replaceWith(node); // 替换为单个节点
path.replaceWithMultiple(nodes); // 替换为多个节点
path.replaceWithSourceString(code); // 用源码字符串替换
// 删除节点
path.remove(); // 移除当前节点
// 作用域
path.scope; // 当前作用域
// 绑定
path.getBinding('name'); // 获取变量绑定
path.bindings; // 当前作用域内全部绑定
}
state对象
// 插件可接收配置项
module.exports = function (babel) {
return {
visitor: {
Program(path, state) {
// state.opts = { optionName: value },来自插件配置
// state.filename = 当前文件名
// state.file = { ast, code, opts }
},
},
};
};
// 用法:{ plugins: [ ['plugin-name', { optionName: value }] ] }
实现一个依赖注入的插件
// transform-deps.js —— 按使用自动注入依赖
module.exports = function (babel) {
const { types: t, template } = babel;
// import 语句模板
const importTemplate = template(`
import IDENTIFIER from 'MODULE';
`);
return {
visitor: {
// 从 Program 进入
Program: {
enter(path, state) {
// 收集需要注入的标识符
const depsToInject = new Set();
const options = state.opts || {};
const importMap = options.imports || {};
// 扫描全文件使用的标识符
path.traverse({
Identifier(idPath) {
const name = idPath.node.name;
// 若是声明或局部变量则跳过
if (idPath.scope.hasBinding(name)) return;
// 判断是否需要按配置注入
if (importMap[name]) {
depsToInject.add(name);
}
},
});
// 已有 import,避免重复
const existingImports = new Set();
path.traverse({
ImportDeclaration(importPath) {
importPath.node.specifiers.forEach(spec => {
if (t.isImportDefaultSpecifier(spec)) {
existingImports.add(spec.local.name);
}
});
},
});
// 在文件顶部插入缺失的 import
const body = path.node.body;
let insertIndex = 0;
// 若有 #!/usr/bin/env node,跳过第一行
if (t.isStringLiteral(body[0]) && body[0].value.startsWith("#!")) {
insertIndex = 1;
}
// 过滤已有的import模块
const depsArray = Array.from(depsToInject).filter(
dep => !existingImports.has(dep)
);
// 基于导入模板将代码
depsArray.forEach(depName => {
const importNode = importTemplate({
IDENTIFIER: t.identifier(depName),
MODULE: t.stringLiteral(importMap[depName]),
});
body.splice(insertIndex, 0, importNode);
});
},
},
// 转换:console.log → logger.log
CallExpression(path, state) {
const callee = path.node.callee;
const options = state.opts || {};
const transforms = options.transforms || {};
// 转换 console.log
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: "console" }) &&
t.isIdentifier(callee.property) &&
transforms.console
) {
// 替换为 logger.log
path.node.callee = t.memberExpression(
t.identifier("logger"),
callee.property
);
}
},
},
};
};
如何在babel配置文件中使用编写好的插件
// .babelrc 或 babel.config.js
{
"plugins": [
[
"./transform-deps.js",
{
"imports": {
"React": "react",
"lodash": "lodash",
"axios": "axios"
},
"functionDeps": {
"fetchData": [{ "name": "fetch", "source": "./utils/fetch" }]
},
"transforms": {
"console": true,
"promisify": true
}
}
]
]
}