手写一个 Babel 插件,展示这些包是怎么配合的
用一个最经典的例子把箭头函数ArrowFunctionExpression转成普通函数FunctionExpression比如把const add (a, b) a b转成const add function (a, b) { return a b }这个例子能把 Babel 里几个核心包的配合过程讲清楚babel/parser把代码转成 ASTbabel/traverse遍历 ASTbabel/types创建 / 判断 / 修改节点babel/generator把 AST 转回代码babel/core统一调度插件执行一、先看完整示例1. 安装依赖npm i babel/core babel/parser babel/traverse babel/types babel/generator2. 手写插件const t require(babel/types) function arrowFunctionPlugin() { return { visitor: { ArrowFunctionExpression(path) { const { node } path let body node.body // 如果箭头函数是简写a a 1 // 需要补成 BlockStatement return if (!t.isBlockStatement(body)) { body t.blockStatement([ t.returnStatement(body) ]) } const func t.functionExpression( null, node.params, body, false, false ) path.replaceWith(func) } } } } module.exports arrowFunctionPlugin3. 调用 Babel 转换const babel require(babel/core) const arrowFunctionPlugin require(./arrowFunctionPlugin) const sourceCode const add (a, b) a b const square n { return n * n } const result babel.transformSync(sourceCode, { plugins: [arrowFunctionPlugin] }) console.log(result.code)输出结果const add function (a, b) { return a b; }; const square function (n) { return n * n; };二、这个过程里每个包是怎么配合的1.babel/core总调度器const result babel.transformSync(sourceCode, { plugins: [arrowFunctionPlugin] })这一句就是babel/core在工作。它负责接收源码加载插件调用 parser 解析调用 traverse 遍历在遍历中执行插件 visitor最后调用 generator 输出代码所以可以理解成core是整个 Babel 流程的入口和中枢。2.babel/parser源码转 AST虽然上面没手动调用parser但babel/core内部会帮你调用。源码const add (a, b) a b会先被解析成 AST大概会包含这些节点ProgramVariableDeclarationVariableDeclaratorIdentifierArrowFunctionExpressionBinaryExpression也就是说Babel 不会直接对字符串做替换而是先把代码变成“语法树结构”。3.babel/traverse遍历 AST插件里这段visitor: { ArrowFunctionExpression(path) { ... } }本质上就是在告诉 Babel当遍历 AST 时如果遇到ArrowFunctionExpression节点就执行这里的逻辑。这一步就是babel/traverse在做的事情。它会深度优先遍历整棵 AST然后在命中目标节点时调用 visitor。4.babel/types操作 AST 节点插件里这部分就是types的使用const t require(babel/types)比如判断节点是不是块语句t.isBlockStatement(body)创建return语句t.returnStatement(body)创建代码块t.blockStatement([ t.returnStatement(body) ])创建函数表达式t.functionExpression( null, node.params, body, false, false )所以types的职责是提供 AST 的判断、创建、组装能力。5.path.replaceWith(...)替换节点path.replaceWith(func)这句表示把当前遍历到的箭头函数节点替换成新创建的普通函数节点。这里的path是 Babel 在遍历时传给 visitor 的“路径对象”。它不仅包含当前节点还提供很多操作方法比如replaceWithremoveinsertBeforeinsertAfterfindParent6.babel/generatorAST 转回代码最后 Babel 会把修改后的 AST 再生成代码const add function (a, b) { return a b; };这一步就是babel/generator做的。三、如果想更直观看配合过程可以拆开手动写上面是通过babel/core一把梭完成的。如果你想给面试官展示“这些包到底怎么协作”可以手动拆开写一次。手动拆解版const parser require(babel/parser) const traverse require(babel/traverse).default const t require(babel/types) const generator require(babel/generator).default const code const add (a, b) a b // 1. parser源码 - AST const ast parser.parse(code, { sourceType: module }) // 2. traverse遍历 AST traverse(ast, { ArrowFunctionExpression(path) { const { node } path let body node.body if (!t.isBlockStatement(body)) { body t.blockStatement([ t.returnStatement(body) ]) } const func t.functionExpression( null, node.params, body, false, false ) // 3. 替换节点 path.replaceWith(func) } }) // 4. generatorAST - 新代码 const output generator(ast, {}, code) console.log(output.code)输出const add function (a, b) { return a b; };四、这段代码能体现哪些协作关系你可以直接这么讲原始代码 ↓ parser.parse ↓ 得到 AST ↓ traverse 遍历 AST ↓ 命中 ArrowFunctionExpression ↓ types 创建 FunctionExpression / ReturnStatement / BlockStatement ↓ path.replaceWith 替换原节点 ↓ generator 生成新代码这就是 Babel 的核心链路解析 AST → 遍历节点 → 修改节点 → 重新生成代码五、插件为什么要补return注意这个箭头函数const add (a, b) a b它的body不是代码块而是一个表达式a b但普通函数写法需要function (a, b) { return a b }所以插件里要做这个判断if (!t.isBlockStatement(body)) { body t.blockStatement([ t.returnStatement(body) ]) }这一步很关键面试官一般会觉得你不是只会机械替换。六、这个插件还有什么局限如果面试官继续追问你可以主动补一句这个插件只是演示 AST 转换流程不是完整的箭头函数转换。因为真正的箭头函数和普通函数还有一个重要区别this绑定不同箭头函数没有自己的this它的this取决于外层作用域普通函数有自己的this所以如果只是简单替换成functionExpression在某些场景下语义会变。例如const obj { name: Tom, say: () { console.log(this.name) } }直接转成普通函数后this语义可能就变了。真正 Babel 官方插件在处理箭头函数时还会处理thisargumentssupernew.target这些上下文语义。七、再给你一个更简单的插件例子修改变量名如果你想面试时展示更简单、几秒能讲清的插件可以用这个。把所有foo改成bar。插件代码const t require(babel/types) function renameFooPlugin() { return { visitor: { Identifier(path) { if (path.node.name foo) { path.node.name bar } } } } } module.exports renameFooPlugin使用const babel require(babel/core) const renameFooPlugin require(./renameFooPlugin) const code const foo 1 console.log(foo) const result babel.transformSync(code, { plugins: [renameFooPlugin] }) console.log(result.code)输出const bar 1; console.log(bar);八、面试里怎么回答“手写一个 Babel 插件展示这些包怎么配合”你可以直接这样答我可以举一个把箭头函数转普通函数的 Babel 插件例子。先由babel/core作为入口读取源码和插件配置然后babel/parser把源码解析成 AST接着babel/traverse遍历 AST当遍历到ArrowFunctionExpression节点时执行插件中的 visitor在 visitor 里借助babel/types判断函数体是不是块语句如果不是就补一个return然后创建新的FunctionExpression节点并通过path.replaceWith替换原节点最后由babel/generator把修改后的 AST 生成为新的代码。整个过程就是core 调度 - parser 解析 - traverse 遍历 - plugin types 修改 - generator 输出。九、标准可背代码如果你想背一个最适合面试展示的版本可以记这个const babel require(babel/core) const t require(babel/types) const arrowFunctionPlugin () ({ visitor: { ArrowFunctionExpression(path) { const { node } path let body node.body if (!t.isBlockStatement(body)) { body t.blockStatement([ t.returnStatement(body) ]) } path.replaceWith( t.functionExpression(null, node.params, body) ) } } }) const sourceCode const add (a, b) a b const result babel.transformSync(sourceCode, { plugins: [arrowFunctionPlugin] }) console.log(result.code)十、一句话总结手写 Babel 插件的本质就是在traverse遍历过程中借助types找到并修改 AST 节点再由generator生成新代码而这一切由core统一调度。