本文首发于个人博客(点此查看),欢迎关注。javascript
source map 是开发时调试代码的利器之一。现代的构建工具如 webpack 早已对 source map 有了完备的支持,对照文档就能很容易在打包时顺手生成而后在现代浏览器如 Chrome/Firefox 中使用。关于相关配置的介绍使用已经有不少文章,这里就再也不赘述。本文想探究的是 source map 在编译器中的实现原理。html
首先对于 source map 还不是特别清楚其原理及使用方式的同窗能够先看一下阮一峰老师对其的介绍。一句话总结就是 source map 是一种存储了源代码和编译后代码映射关系的信息文件。当你的编译后代码出现问题时,根据 source map 就能精准定位到源代码对应的位置。不然,直接在天书通常的编译后(加上可能压缩后)代码中进行调试,难度不小。java
source map 揭示了源代码和处理后代码之间的映射关系,而从源码处处理后代码的过程天然离不开编译。一个典型的编译过程以下:node
AST,即抽象语法树,是源代码语法结构的一种抽象表示。其以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构(来自维基百科解释)。感兴趣的同窗可访问 astexplorer.net/ 查看代码经 parser 处理为 AST 的过程。在 AST 中,每一个节点都会保存本身的来源信息,以下所示:webpack
interface Node {
type: string;
loc: SourceLocation | null;
}
interface SourceLocation {
source: string | null;
start: Position;
end: Position;
}
interface Position {
line: uint32 >= 1;
column: uint32 >= 0;
}
复制代码
每一个 Node 的 loc 属性(视 parser 不一样可能解析为其余名称,如 traceur 将其解析为 location)包含其 start 和 end 的行列位置信息。在 generate 环节,start 位置信息就是生成 source map 的关键。而一般 generator 不会本身去作映射关系的 VLQ 编码(source map 的位置信息存储方式),而是交由专业的库来处理,好比 Mozilla 出品的 source-map。git
source-map 库封装了底层的映射关系计算的逻辑,在生成 source map 时向开发者提供了两种类型的 API,一种是低级 API,其单纯地经过向结果中插入源代码和编译后代码的行列对应关系来生成 source map,官方示例以下:github
var map = new SourceMapGenerator({
file: "source-mapped.js"
});
map.addMapping({
generated: {
line: 10,
column: 35
},
source: "foo.js",
original: {
line: 33,
column: 2
},
name: "christopher"
});
console.log(map.toString());
// '{"version":3,"file":"source-mapped.js","sources":["foo.js"],"names":["christopher"],"mappings":";;;;;;;;;mCAgCEA"}'
复制代码
另外一种高级 API 则直接侵入了编译过程。在 generate 步骤,source-map 提供了 SourceNode
用于在保留原有节点信息的同时添加该节点对应源代码的行列信息。最后再借助 SourceNode
提供的 toStringWithSourceMap
方法同时输出代码和 source map。官方示例以下:web
function compile(ast) {
switch (ast.type) {
case 'BinaryExpression':
return new SourceNode(
ast.location.line,
ast.location.column,
ast.location.source,
[compile(ast.left), " + ", compile(ast.right)]
);
case 'Literal':
return new SourceNode(
ast.location.line,
ast.location.column,
ast.location.source,
String(ast.value)
);
// ...
default:
throw new Error("Bad AST");
}
}
var ast = parse("40 + 2", "add.js");
console.log(compile(ast).toStringWithSourceMap({
file: 'add.js'
}));
// { code: '40 + 2',
// map: [object SourceMapGenerator] }
复制代码
显然,高级 API 对于 source-map 的依赖和耦合性比较高。不过笔者在探究各个 generator 对于 source map 的支持时发现两种 API 均有使用。好比 @babel/generator 使用了低级 API,而 escodegen 则使用了高级 API。npm
生成 source map 的原理并不复杂,使用 source-map 的低级 API 时, generator 的生成代码是一个遍历 AST node 而后根据其类型将对应的语句逐个拼装的过程,这其中会维护生成代码的行列信息,而在 node 中则保存有源代码的位置信息,如此即可调用 source-map 的低级 API 去生成 source-map。而使用高级 API 的原理则更简单,generator 处理好各个 node 对应生成的代码语句,拿到 node 中的源位置信息,而后调用 new SourceNode()
和toStringWithSourceMap
交给 source-map 去处理和生成代码和 srouce map 便可。编程
最后,回到 source-map 库的实现上来。在其代码库的 lib/source-node.js
中咱们能够看到,SourceNode 实例的 toStringWithSourceMap
方法本质上作的工做也无非就是将生成好的代码片断拼接起来并同时调用低级 API 来生成 source map。至于 VLQ 编码的方式,源码里也有,读者有兴趣可结合原理自行查看。