写你的第一个 Babel 插件:import 改写实战
本节目标
- 写完并跑通第一个真正的 Babel 插件
- 把
import { Button } from 'antd'自动改成import Button from 'antd/lib/button' - 理解插件的本质:插件 = 一个返回 visitor 的函数
Babel 插件的本质非常简单:
module.exports = function myPlugin() {
return {
visitor: {
// 当遍历到某种节点时,Babel 调用这里的函数
SomeNodeType(path) { /* 用 path 改树 */ }
}
}
}这次我们改 ImportDeclaration( import 声明)节点:当来源是 'antd' 时,把每个具名导入 { Button, Input } 拆成独立的默认导入 import Button from 'antd/lib/button'。这正是 antd 按需加载的经典写法。
动手:完整插件 + 运行
// ===== 文件 1:plugin.cjs(我们的插件)=====
// 运行环境:Node.js 18+,需安装: npm install @babel/core @babel/types
// 说明:插件本身只是“返回 visitor 的函数”,不依赖运行环境
module.exports = function babelPluginImportToLib() {
const t = require('@babel/types')
return {
visitor: {
ImportDeclaration(path) {
const source = path.node.source.value
if (source !== 'antd') return // 只处理来自 antd 的导入
const newNodes = path.node.specifiers
.filter((s) => s.type === 'ImportSpecifier') // 具名导入 { X }
.map((s) => {
const exported = s.imported.name // 导出的名字,如 Button
const local = s.local.name // 本地绑定的名字
return t.importDeclaration(
[t.importDefaultSpecifier(t.identifier(local))],
t.stringLiteral('antd/lib/' + exported.toLowerCase())
)
})
if (newNodes.length > 0) {
// 用多个新节点替换原来的单条导入
path.replaceWithMultiple(newNodes)
}
}
}
}
}// ===== 文件 2:run.cjs(用插件转换一段代码)=====
// 运行环境:Node.js 18+,需安装: npm install @babel/core
// 运行方式:把 plugin.cjs 和 run.cjs 放同一目录,终端执行 node run.cjs
const babel = require('@babel/core')
const myPlugin = require('./plugin.cjs')
const code = "import { Button, Input } from 'antd'"
const result = babel.transformSync(code, { plugins: [myPlugin] })
console.log('转换前:', code)
console.log('转换后:', result.code)
// 转换后:
// import Button from "antd/lib/button";
// import Input from "antd/lib/input";名词解释
插件(Plugin):在 Babel 语境下,插件就是“一个返回 { visitor } 的函数”。Babel 在遍历 AST 时,遇到你在 visitor 里声明的节点类型,就调用对应的处理函数。换句话说,插件 = 你告诉 Babel“遇到某类节点时该做什么”的一段逻辑。
ImportDeclaration(导入声明节点):AST 中表示一条 import ... from ... 语句的节点。它的 source.value 是模块路径(如 'antd'),specifiers 是这条语句导入的具体绑定(默认导入 import x、具名导入 import { x })。改按需加载,本质上就是重写这个节点。
课后练习
练习 1:本节的插件把 import { Button } from 'antd' 转换成了什么?请写出转换后的完整代码。
答案:
import Button from "antd/lib/button";。逻辑是:每个具名导入{ Button }被拆成一条独立的默认导入,模块路径被小写化为antd/lib/button(Button → button)。
练习 2:如果源码是 import React from 'react',本节的插件会改动它吗?为什么?
答案:不会,有两道关卡把它挡住了:①
if (source !== 'antd') return直接跳过非 antd 的导入;② 它是默认导入不是具名导入,specifiers.filter(s => s.type === 'ImportSpecifier')会把它过滤掉,于是newNodes为空,不会触发replaceWithMultiple。
本节小结(观点与完整描述)
本节你写出了第一个“能落地”的 Babel 插件,也戳破了插件神秘感:插件 = 一个返回 { visitor } 的函数,你写的就是“遇到某类节点时做什么”。真正干活的三件套是 path.replaceWith / replaceWithMultiple / remove 配合 t.xxx() 造节点;而 babel.transformSync(code, { plugins: [你的插件] }) 则让你在普通 Node 脚本里离线跑通整条流水线。值得点破的是——你每天用的 babel-plugin-import、React 的 JSX 转换、TypeScript 去类型,底层全是同一套机制,区别只在 visitor 里改了什么。理解了“插件就是 visitor 函数”,你就从“用别人的插件”跨进了“写自己的插件”。