我们在上文中已经了解到babel的第一步,将代码解析成AST,接下来的两步就比较好理解了,转换与生成。我再看最开始的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const { parse } = require("./packages/babel-parser"); const traverse = require("./packages/babel-traverse").default; const generate = require("./packages/babel-generator").default;
const code = "const square = n => n * n";
const ast = parse(code);
console.log(`originAST: ${JSON.stringify(ast)}`);
traverse(ast, { enter(path) { if (path.node.type !== "ArrowFunctionExpression") return;
path.arrowFunctionToExpression({ allowInsertArrow: false, noNewArrows: true, specCompliant: false, }); }, });
console.log(`newAST: ${JSON.stringify(ast)}`)
const output = generate(ast, code); console.log(output.code);
|
转换这里的代码其实是babel-plugin-transform-arrow-functions的源码,作用是将箭头函数转换成普通函数,我们分别将转换前后的AST打印出来,比较看一下这个插件对AST做了什么。

转后的AST结构发生了变化,这就是转换阶段babel的作用,将源代码的AST按照标准规范转换成目标代码的AST。看到这儿,我们其实已经可以手写一个简单的插件来玩玩了。
我们新建一个文件replaceN2M.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = function () { return { visitor: { Identifier(path) { if (path.isIdentifier({ name: "n" })) { path.node.name = "m"; } }, BinaryExpression(path) { if (path.node.operator === "*") { path.node.operator = "-"; } }, }, }; };
|
一个最简单的插件就完成了,它做了两件事,将所有变量n转换成m,将所有二元表达式的运算符*转换成-。
babel会深度优先遍历AST,期间会调用插件中的visitor.method,visitor里存放对AST的操作回调,Identifier(){}是指当前AST节点的type === 'Identifier'时,进入此函数,此时path为当前位置,path.node为此节点,那么我们就可以对node进行操作。
我们新建一个文件test2.js,看下我们的插件是否起作用
1
| const square = n => n * n
|
自定义的方法有两种方法进行调用,一种是直接使用babel编译命令,npx babel --plugins ./replaceN2M.js test2.js。第二种是,将我们的插件放进babel的配置文件里,在babel.config.js的plugin中添加"./replaceN2M",现在整个plugin长这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| plugins: [ ["@babel/proposal-object-rest-spread", { useBuiltIns: true }],
convertESM ? "@babel/proposal-export-namespace-from" : null, convertESM ? pluginImportMetaUrl : null,
pluginPackageJsonMacro,
process.env.STRIP_BABEL_8_FLAG && [ pluginToggleBabel8Breaking, { breaking: bool(process.env.BABEL_8_BREAKING) }, ], needsPolyfillsForOldNode && pluginPolyfillsOldNode, "./replaceN2M", ].filter(Boolean)
|
然后直接运行 npx babel test2.js即可,我们可以看到控制台输出了我们想要的结果,或者,让babel直接编译成文件npx babel test2.js --out-dir ./ ,再打开test2.js看到文件内容已经是编译后的内容。
写一个插件最大的难点在于对babelAPI的不熟悉以及对ECMA规范的不了解,如果要写一个复杂的插件,需要多看看类似插件的实现,强烈推荐babel插件开发手册,基本涵盖了开发插件所需要的所有知识。
到这里,我的好奇心基本都已经打消了,转换后的AST怎么重新生成代码,这里就不深入研究了,感兴趣可以直接去看源码。
References
Babel插件开发入门指南
Step-by-step guide for writing a custom babel transformation
Babel 插件开发手册