babel 插件开发原理理解
本文算是一个整理,借鉴了很多社区的文章,也加上了一些自己的理解。由于 babel 开发相关资料分散在各个文章里,这里做一个整理。这是业余项目 多端开发原理理解 的其中一篇文章。
了解 AST
参考资料: AST 抽象语法树
抽象语法树类似 DOM 树,通常有以下字段:
type: String
: 类型start: Number
: 开始位置end: Number
: 结束位置loc: Object
: 位置的具体信息- 其他字段随 AST 节点的不同而不同
编译器的工作原理
参考资料: 懂编译真的可以为所欲为|不同前端框架下的代码转换
看下图:
具体到 babel 的编译,就是以下过程:
写法 1
代码字符串解析为 AST @babel/parser
const parse = require('@babel/parser').parse; const ast = parse('const a = 1');
parse(code, options = {})
有第二个参数 options
,默认值如下:
const defaultOptions = { sourceType: "script", sourceFilename: undefined, startLine: 1, allowAwaitOutsideFunction: false, allowReturnOutsideFunction: false, allowImportExportEverywhere: false, allowSuperOutsideMethod: false, plugins: [], strictMode: null, ranges: false, tokens: false, createParenthesizedExpressions: false };
其中, sourceType
的值为:
script
: (默认): 声明体(Statement)module
: 模块声明体(ModuleDeclaration)
除了常规的语法,Babel 可以转译的语法是有限的,扩展方式通过 options.plugins
配置:
estree
jsx
flow
doExpressions
objectRestSpread
decorators
classProperties
exportExtensions
asyncGenerators
functionBind
functionSent
dynamicImport
遍历并处理 AST 节点 @babel/traverse
+ @babel/types
遍历 AST 的工具:
const traverse = require('@babel/traverse').default;
处理 AST 的工具:
const t = require('@babel/types');
从 AST 还原为代码字符串 @babel/generator
const generator = require('@babel/generator').default; const code = generator(ast).code;
写法 2
const result = babel.transform(code, { plugins: [ [ { visitor: { ImportDeclaration(path, { opts }) { console.log(opts); // output: { test: 1 } } } }, { test: 1 } ] ] }); console.log(result.code);
专题:熟悉所有 AST 节点
请准备一个 test.js
,初始化内容如下,并安装依赖:
const parse = require('@babel/parser').parse; const traverse = require('@babel/traverse').default; const code = `xxx`; const ast = parse(code); // 加 sourceType 用于编译 es6 模块 // const ast = parse(code, { sourceType: 'module' }); traverse(ast, {});
打开文档: https://github.com/babel/babylon/blob/master/ast/spec.md
该文档介绍了所有的 AST 节点。上面代码中的 traverse
的第二个参数 key 值即为文档中的节点,如:
const code = ` function test() { const a = 1; return a; } `; const ast = parse(code); traverse(ast, { Identifier(path) { console.log(path.node.name); } });
输出为:
test a a
请按照上面的写法,结合文档中的具体节点类型,写 demo 查看运行结果。重点观察 path.node
。经测算, @vdian/traverse
遍历 ast 时但并不完全支持 babylon 的文档涉及的所有 ast 节点,应该是支持了 95% 以上的节点,这点请注意。
专题:熟悉 @babel/types
即 t
的 API
参考文档:
- babel 插件开发手册
- 官方文档的信息实在是太少了, 这里 是有人总结的 API 列表
- 这里是所有节点的定义
根据官方文档, t
有以下作用:构造(Builders)、验证(Validators)等。
1、构造(Builders): t.X()
构建器的方法名称就是您想要的节点类型的名称,除了第一个字母小写。
例如,如果您想建立一个 MemberExpression
您可以使用 t.memberExpression(...)
。
这些构建器的参数由节点定义决定。
节点定义如下所示:
defineType("MemberExpression", { builder: ["object", "property", "computed"], visitor: ["object", "property"], aliases: ["Expression", "LVal"], fields: { object: { validate: assertNodeType("Expression") }, property: { validate(node, key, val) { let expectedType = node.computed ? "Expression" : "Identifier"; assertNodeType(expectedType)(node, key, val); } }, computed: { default: false } } });
在这里你可以看到关于这个特定节点类型的所有信息,包括如何构建它,遍历它,并验证它。
通过查看 builder
属性, 可以看到调用生成器方法所需的 3 个参数,每个参数可用的值可以从 fileds
属性中找到。
如:
t.binaryExpression('*', t.identifier('a'), t.identifier('b'));
创建了一个二元表达式,转为 code 如下:
a * b
2、验证(Validators): t.isX
+ t.assertX
如:
t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" }) t.assertBinaryExpression(maybeBinaryExpressionNode);
下面是各种举例:
检查节点类型
如果你想检查节点的类型,最好的方式是:
BinaryExpression(path) { if (t.isIdentifier(path.node.left)) { // ... } }
你同样可以对节点的属性们做浅层检查:
BinaryExpression(path) { if (t.isIdentifier(path.node.left, { name: "n" })) { // ... } }
功能上等价于:
BinaryExpression(path) { if ( path.node.left != null && path.node.left.type === "Identifier" && path.node.left.name === "n" ) { // ... } }
专题:熟悉 path
的 API
path
顾名思义是“路径”,代码中指的是:
traverse(ast, { Identifier(path) { console.log(path.node.name); } });
访问
获取节点: path.node
// the BinaryExpression AST node has properties: `left`, `right`, `operator` BinaryExpression(path) { path.node.left; path.node.right; path.node.operator; }
访问节点属性内部的 path
: path.get
BinaryExpression(path) { path.get('left'); } Program(path) { path.get('body.0'); }
检查路径类型(和 t
相同)一个路径具有相同的方法检查节点的类型:
BinaryExpression(path) { if (path.get('left').isIdentifier({ name: "n" })) { // ... } }
就相当于:
BinaryExpression(path) { if (t.isIdentifier(path.node.left, { name: "n" })) { // ... } }
检查标识符(Identifier)是否被引用
Identifier(path) { if (path.isReferencedIdentifier()) { // ... } }
或者:
Identifier(path) { if (t.isReferenced(path.node, path.parent)) { // ... } }
找到特定的父路径
有时你需要从一个路径向上遍历语法树,直到满足相应的条件。对于每一个父路径调用 callback
并将其 NodePath
当作参数,当 callback
返回真值时,则将其 NodePath
返回。.
path.findParent((path) => path.isObjectExpression());
如果也需要遍历当前节点:
path.find((path) => path.isObjectExpression());
查找最接近的父函数或程序:
path.getFunctionParent();
向上遍历语法树,直到找到在列表中的父节点路径
path.getStatementParent();
停止遍历,如果你的插件需要在某种情况下不运行,最简单的做法是尽早写回。
BinaryExpression(path) { if (path.node.operator !== '**') return; }
如果您在顶级路径中进行子遍历,则可以使用 2 个提供的 API 方法:
path.skip()
path.stop()
处理
用一个节点替换单节点: path.replaceWith(node)
BinaryExpression(path) { path.replaceWith( t.binaryExpression("**", path.node.left, t.numberLiteral(2)) ); }
function square(n) { - return n * n; + return n ** 2; }
用多节点替换单节点: path.replaceWithMultiple([])
ReturnStatement(path) { path.replaceWithMultiple([ t.expressionStatement(t.stringLiteral("Is this the real life?")), t.expressionStatement(t.stringLiteral("Is this just fantasy?")), t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")), ]); }
function square(n) { - return n * n; + "Is this the real life?"; + "Is this just fantasy?"; + "(Enjoy singing the rest of the song in your head)"; }
注意:当用多个节点替换一个表达式时,它们必须是声明。 这是因为 Babel 在更换节点时广泛使用启发式算法,这意味着您可以做一些非常疯狂的转换,否则将会非常冗长。
用字符串源码替换节点
FunctionDeclaration(path) { path.replaceWithSourceString(function add(a, b) { return a + b; }); }
+ "Because I'm easy come, easy go."; function square(n) { return n * n; } + "A little high, little low.";
注意:这里同样应该使用声明或者一个声明数组。 这个使用了在用多个节点替换一个节点中提到的相同的启发式算法。
插入到容器(container)中
ClassMethod(path) { path.get('body').unshiftContainer( 'body', t.expressionStatement(t.stringLiteral('before')) ); path.get('body').pushContainer( 'body', t.expressionStatement(t.stringLiteral('after')) ); }
class A { constructor() { + "before" var a = 'middle'; + "after" } }
删除一个节点: path.remove()
FunctionDeclaration(path) { path.remove(); }
- function square(n) { - return n * n; - }
替换父节点: path.parentPath.replaceWith
Scope(作用域)
检查本地变量是否被绑定
FunctionDeclaration(path) { if (path.scope.hasBinding("n")) { // ... } }
这将遍历范围树并检查特定的绑定。
您也可以检查一个作用域是否有自己的绑定:
FunctionDeclaration(path) { if (path.scope.hasOwnBinding("n")) { // ... } }
创建一个 UID
这将生成一个标识符,不会与任何本地定义的变量相冲突。
FunctionDeclaration(path) { path.scope.generateUidIdentifier("uid"); // Node { type: "Identifier", name: "_uid" } path.scope.generateUidIdentifier("uid"); // Node { type: "Identifier", name: "_uid2" } }
提升变量声明至父级作用域
有时你可能想要推送一个 VariableDeclaration
,这样你就可以分配给它。
FunctionDeclaration(path) { const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id); path.remove(); path.scope.parent.push({ id, init: path.node }); }
- function square(n) { + var _square = function square(n) { return n * n; - } + };
重命名绑定及其引用
FunctionDeclaration(path) { path.scope.rename("n", "x"); }
- function square(n) { - return n * n; + function square(x) { + return x * x; }
或者,您可以将绑定重命名为生成的唯一标识符:
FunctionDeclaration(path) { path.scope.rename("n"); }
- function square(n) { - return n * n; + function square(_n) { + return _n * _n; }
专题:熟悉 state
state
负责接收 plugin 中的传参。
const { opts } = state;
插件配置举例:
{ "plugins": [ ["my-plugin", { "option1": true, "option2": false }] ] }
babel 插件解析如下:
{ visitor: { FunctionDeclaration(path, state) { console.log(state.opts); // { option1: true, option2: false } } } }
参考资料
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论