一、前言
在前面两章, 我们一起学习了parser和traverse, 还差最后一步generator就可以把代码编译的整个流程串联起来。下面我们了解写@babel/generator的用法和原理吧
往期回顾:
二、基础案例
const parser = require('@babel/parser')
const generator = require('@babel/generator')
const ast = parser.parse(`
const a: number = 1
`)
const output = generator.default(ast, {})
最后输出的内容如下, 产生了最终的code和sourcemap
选项
并且generator还支持一些参数
const output = generator.default(ast, {
sourceMaps: true
// ...
})
| 选项 | 说明 |
|---|---|
| auxiliaryCommentAfter | 添加注释到生成代码的最后 |
| auxiliaryCommentBefore | 添加注释到生成代码的最前 |
| comments | 生成的代码是否包含注释 |
| compact | 生成的代码是否包含多于的空格 |
| concise | 设置为true可减少空白(但不如opts.compact) |
| decoratorsBeforeExport | decoratorsBeforeExport为false, export @decorator class Bar {}, decoratorsBeforeExport为true,@decoratorexport class Foo {} |
| filename | 用于警告消息中 |
| minified | 是否压缩 |
| retainFunctionParens | 保留函数表达式周围的参数(可用于更改引擎解析行为) |
| retainLines | 尝试在输出代码中使用与源代码中相同的行号(有助于保留堆栈跟踪) |
| shouldPrintComment | 是一个函数,参数可以接收注释内容,如果函数返回true,就会保留注释,否则删除,类似comments选项 |
| sourceMaps | 是否生成sourceMap |
| sourceRoot | 设置sourcemap url相对根路径 |
三、实现@babel/generator
下面的代码中,定义了各种AST的生成函数,其实就是使用策略模式这种设计模式,对于不同类型的节点使用不同的方法处理。
其中还实现了sourcemap的生成,借助了source-map这个库,分别在不同节点遍历时,创建一个map
const { SourceMapGenerator } = require('source-map');
class Printer {
constructor (source, fileName) {
// 记录生成的代码,不断拼接
this.buf = '';
this.sourceMapGenerator = new SourceMapGenerator({
file: fileName + ".map.json",
});
this.fileName = fileName;
// 设置源码内容
this.sourceMapGenerator.setSourceContent(fileName, source);
this.printLine = 1;
this.printColumn = 0;
}
addMapping(node) {
if (node.loc) {
// 添加一个映射关系
this.sourceMapGenerator.addMapping({
generated: {
line: this.printLine, // 这里是转换后的行数
column: this.printColumn // 这里是转换后的列数
},
source: this.fileName, // 文件名
original: node.loc && node.loc.start // 转换前的起始位置
})
}
}
// 增加一个空格
space() {
this.buf += ' ';
this.printColumn ++;
}
// 换行
nextLine() {
this.buf += '\n';
this.printLine ++;
this.printColumn = 0;
}
// 遍历 Program下的节点,并调用相应的方法拼接
Program (node) {
this.addMapping(node);
node.body.forEach(item => {
// 执行对应节点的拼接方法
this[item.type](item) + ';';
// 行数+1
this.printColumn ++;
// 换行
this.nextLine();
});
}
// 遍历声明语句
VariableDeclaration(node) {
if(!node.declarations.length) {
return;
}
this.addMapping(node);
// let var const
this.buf += node.kind;
// 空格
this.space();
// 遍历变量名
node.declarations.forEach((declaration, index) => {
if (index != 0) {
this.buf += ',';
this.printColumn ++;
}
// 执行到 VariableDeclarator 拼接方法
this[declaration.type](declaration);
});
// 添加分号
this.buf += ';';
// 列数+1
this.printColumn ++;
}
VariableDeclarator(node) {
this.addMapping(node);
// 执行到 Identifier 拼接
this[node.id.type](node.id);
// 添加赋值
this.buf += '=';
// 列数+1
this.printColumn ++;
// 执行到 Identifier 或 NumericLiteral 等
this[node.init.type](node.init);
}
Identifier(node) {
this.addMapping(node);
this.buf += node.name;
}
FunctionDeclaration(node) {
this.addMapping(node);
// 拼接函数
this.buf += 'function ';
this.buf += node.id.name;
this.buf += '(';
this.buf += node.params.map(item => item.name).join(',');
this.buf += '){';
this.nextLine();
this[node.body.type](node.body);
this.buf += '}';
this.nextLine();
}
CallExpression(node) {
this.addMapping(node);
this[node.callee.type](node.callee);
this.buf += '(';
node.arguments.forEach((item, index) => {
if(index > 0 ) this.buf += ', ';
this[item.type](item);
})
this.buf += ')';
}
ExpressionStatement(node) {
this.addMapping(node);
this[node.expression.type](node.expression);
}
ReturnStatement(node) {
this.addMapping(node);
this.buf += 'return ';
this[node.argument.type](node.argument);
}
BinaryExpression(node) {
this.addMapping(node);
this[node.left.type](node.left);
this.buf += node.operator;
this[node.right.type](node.right);
}
BlockStatement(node) {
this.addMapping(node);
node.body.forEach(item => {
this.buf += ' ';
this.printColumn += 4;
this[item.type](item);
this.nextLine();
});
}
NumericLiteral(node) {
this.addMapping(node);
this.buf += node.value;
}
}
总结
@babel/generator实现起来较为简单,核心在于借助了策略模式,各个节点都有其对应的代码生成方法。
另外还使用了source-map这个库,生成代码映射。