sourcemap的认知与运用

sourcemap的认知与运用
最新回答
秋桜

2026-01-09 04:58:41

在讨论sourcemap之前,我们首先需要了解编程语言的编译过程。

编译是将编程语言转换为可执行代码的过程。然而,可执行代码和源代码是完全不同的两种格式。如果在执行过程中出现错误,很难定位源代码编写过程中出现的问题。因此,现代编译器基本上都支持一种标准的可执行代码与源代码之间的映射,以特定格式将这种映射关系持久化存储到文件中。当程序执行出现问题时,根据堆栈信息使用映射文件逆向还原到源代码,可以清晰地、语义化地查找编写过程中的错误。

在常见的C Family languages中,这种可执行代码和源码之间映射的格式已经是一种标准化的东西了,称为DWARF。尽管在每个厂商应用的平台上产生的文件格式不尽相同,但它们内里遵循的映射格式都遵循了DWARF调试标准。

回到Web前端领域讨论这个问题,类似地,我们有sourcemap这个东西。或者我们应该称它为sourcemap调试格式标准。因为目前我们对sourcemap的格式都遵循了一定的标准,但在它的用途上,却出现了千差万别。

本文中讨论sourcemap的格式组成是没有意义的,sourcemap的格式应当是一种持续发展的标准,每当标准更新,自然会有健壮的工具帮助解读。理解sourcemap的格式对它的应用不会有太明显的帮助。

按照我的理解,最早sourcemap是为了协助JavaScript代码混淆(或者压缩)而产生的。框架的作者为了缩小代码体积、避免生产环境的JavaScript代码能被轻松地阅读,使用压缩、混淆工具对源码进行了处理。如果产生了问题,有sourcemap还能轻松地还原代码。但当时的sourcemap并不是web标准之一,这为今日的web前端工程化体系埋下了隐患。

在上一节中提到,sourcemap是一种“可执行代码”和源码之间的映射关系,那么只要是产生编译的地方,就应该会有sourcemap产生。

首先想到了Babel这个工具,它是应用最广泛的JavaScript编译器,接下来我们尝试一个示例:

接着,我们安装babel,指定sourcemap参数为inline,并对其进行转换,得到:

可以看到,转换后的代码和源码产生了很大的区别,并且sourcemap以注释的形式紧跟在可执行代码尾部,它的链接指向了一个data URL。

这么看就很容易了解到,sourcemap是一个基于之前的web标准拓展的功能,它只需要以固定的注释格式跟随在一个脚本文件的末尾即可。那么不同的浏览器厂商想要兼容这个标准,只需要加以识别这个格式即可。

但上文可以看出,data URL加在注释中,任何外部人员都可以轻易获得。因此在做代码转换时,可以将sourcemap生成到一个单独的文件中,一般是文件名加.map后缀,那么对应的sourcemap的地址也就变成:

sourcemap的这种设计方法并不是独一份的。例如在C Family Language的构建交付体系中,为了调试方便,对于测试环境交付的库、可执行代码,并不会将它们的"sourcemap"从可执行代码中分离出来。它们的执行环境和大部分JavaScript引擎一样,在调试断点或者Exception时,都能自动地载入"sourcemap"并且给出对应的源码栈信息。而在生产交付时,都会在构建时将"sourcemap"从库和可执行文件中提取出来单独保存。这样既减小了交付的产物体积,还能保证源码信息不被外部人员知晓。

既然sourcemap在逐渐地成为一个标准,那么自然就有配套的标准工具对这种格式进行解析。除了Chrome devtools自带的以外,还可以使用常见的库,接下来我们使用mozilla的source-map库,对刚才的代码进行一些还原的操作。

这是断点调试常见经常会碰到的场景,比如某个exception发生在了我们刚才代码的line 9; column 2; 即console.log的位置。那么我们可以这么做:

通过载入map文件,然后查找line 9 column 2对应的源代码位置可得:

然后我们去index.ts中的3行2列,正好是ts文件中的console.log

可以看到,sourcemap不仅记录了代码行列数,还记录了源文件的位置,那么不用翻sourcemap的manual也能说明,sourcemap也像其他编译系统一样,支持多个文件、库的代码映射的集成合并。这对于大型工程的构建十分有用,大型工程往往由数以千计万计的库和代码文件组成,在这种复杂的构建中,我们往往只想要获得有限几个产物。除了可执行代码,sourcemap的统一产出也有利于我们方便地追溯生产环境的问题。

mozilla的source-map标准库不止有查找映射位置的这个作用,更能直接还原代码。我们可以直接载入一个sourcemap文件,然后查看它对应了那些源代码文件,然后根据这些文件进行逐个还原内容:

由于我们只有一个,得到的输出是

在实际开发场景中,我们碰到的sourcemap运用比上面提到的情况复杂得多,但是问题又没有办法一一枚举,所以这里讨论一些常见的例子

前面就见过两种形式:dataURL放在JavaScript文件结尾的、和独立写入文件的。他们的形式是显而易见的,并且特点也能清楚地看出来。前者内联,增大了体积,但是降低了使用的门槛。后者独立,可以隐藏保存起来,非必要时不使用。

但在webpack这种打包构建工具中,对sourcemap做了更多更详细的定义,其基本分类的依据也遵循以下几个维度:

具体的规则可以翻阅webpack对应的文档,光靠脑子记是没办法的。

对库、预编译/转换的sourcemap处理

日常开发中,使用库或者预编译的组件是很常见的事,例如:

预编译文件的处理

像这些场景对工程的构建提出了比较多的要求。这里就针对常用的webpack做一个处理的展示,还是以前面的工程为例,我们的Typescript库导出了函数a,然后我们直接引用dist/index.js(而非src/index.ts)

然后我们通过webpack打包并且导出它的sourcemap

最后我们得到了./dist/demo.dist.js.map(名字有点诡异哈)

然后我们再通过刚才写的sourcemap工具进行还原看看:

可以看到,webpack默认情况下可以识别预编译的文件它的sourcemap,并且进行合理的目录转换,保证指向正确

那么我们如果删除预编译文件的sourcemap呢?我对a函数的库重新编译了一遍,并且不导出任何sourcemap,结果对引入的a函数库的文件还原就变成了

库的处理

如果a函数是从npm库引用的呢?我尝试了下把函数a放到了库中,然后将源码引用改成了

结果虽然node_modules中带了sourcemap,但是还原后并不是原本的ts代码

这时候需要对webpack进行配置,增加source-map-loader作为预处理,且不能排除node_modules目录:

然后再进行打包编译出来的map我们对其进行还原发现:

webpack成功帮我们提取了node_modules库中的sourcemap

本文主要阐述了sourcemap的基本知识,包含了几个方面: