generator 是打印 AST 为目标代码,并生成 sourcemap。
这节我们实现一下 generator。
思路分析
generator 会遍历 AST 进行打印,对于每种 AST 我们是知道如何打印的,比如 while 语句:
先打印 while、再打印空格,再打印 ( ,然后打印 test 部分,之后打印 ),最后打印 block 部分。
那么实现了每种 AST 的打印就可以拼接出目标代码。
而 sourcemap 是记录源码位置和目标代码位置的关联,在打印的记录下当前打印的行列,就是目标代码位置,而源码位置 parse 的时候就有了,这样就生成了一个 mapping。
sourcemap 就是由一个个 mapping 组成的,打印每个 AST 节点的时候添加一下 mapping,最终就生成了 sourcemap。
代码实现
我们定义一个 Printer 类做打印,实现每种 AST 的打印逻辑:
class Printer {
constructor (source, fileName) {
this.buf = '';
this.printLine = 1;
this.printColumn = 0;
}
addMapping(node) {
// 待实现
}
space() {
this.buf += ' ';
this.printColumn ++;
}
nextLine() {
this.buf += '\n';
this.printLine ++;
this.printColumn = 0;
}
Program (node) {
this.addMapping(node);
node.body.forEach(item => {
this[item.type](item) + ';';
this.printColumn ++;
this.nextLine();
});
}
VariableDeclaration(node) {
if(!node.declarations.length) {
return;
}
this.addMapping(node);
this.buf += node.kind;
this.space();
node.declarations.forEach((declaration, index) => {
if (index != 0) {
this.buf += ',';
this.printColumn ++;
}
this[declaration.type](declaration);
});
this.buf += ';';
this.printColumn ++;
}
VariableDeclarator(node) {
this.addMapping(node);
this[node.id.type](node.id);
this.buf += '=';
this.printColumn ++;
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;
}
}
这样递归进行打印就可以生成完整的目标代码,我们把它记录到了 this.buf 属性。
同时,我们在打印的时候记录了 printLine、printColumn 的信息,也就是当前打印到了第几行,这样在 addMapping 里面就可以拿到 AST 在目标代码中的位置,而源码位置是在 parse 的时候记录到 loc 属性的,有了这两个位置就可以生成一个 mapping。
sourcemap 的生成是使用 source-map 包,这个 mozilla 维护的,因为 sourcemap 的标准也是他们提出来的。
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;
}
}
sourcemap 需要指定源文件名,这也是为什么我们要传入 fileName。
之后实现 addMapping 方法:
addMapping(node) {
if (node.loc) {
this.sourceMapGenerator.addMapping({
generated: {
line: this.printLine,
column: this.printColumn
},
source: this.fileName,
original: node.loc && node.loc.start
})
}
}
最后,我们定义 Generator 类,在 generate 方法里面调用 printer 的打印逻辑来生成目标代码,并且调用 this.sourceMapGenerator.toString() 来生成 sourcemap。
class Generator extends Printer{
constructor(source, fileName) {
super(source, fileName);
}
generate(node) {
this[node.type](node);
return {
code: this.buf,
map: this.sourceMapGenerator.toString()
}
}
}
然后暴露出 generate 的 api:
function generate (node, source, fileName) {
return new Generator(source, fileName).generate(node);
}
这样,我们就实现了 babel generator 的功能,也就是打印目标代码和生成 sourcemap。
可以在生成的代码中添加 sourceMappingURL 就可以映射回源码,可以通过打断点或者运行代码 throw error 的方式来测试 测试 sourcemap的功能。
//# sourceMappingURL=./xxx.map.json
总结
generator 是打印 AST 为目标代码,我们知道每种 AST 是如何打印的,那么递归打印 AST,拼接字符串,就可以生成目标代码。
sourcemap 是调试代码和线上报错定位源码必不可少的功能,我们基于 source-map 包来生成,记录一个个 mapping。
具体的 mapping 就是源代码位置和目标代码位置的关联,AST 在源码的位置记录在 loc 属性,而在目标代码的位置位置可以计算出来。这样就可以生成 sourcemap。
(代码在这里,建议 git clone 下来通过 node 跑一下)