Source map
源映射指的是将转换之后的代码映射回原来的代码,比如在我们进行项目的生产构建时,常常会将代码进行压缩以提升运行效率。
源码转换通常存在于以下几种情况:
- 压缩代码从而减小体积。
- 合并多个文件以减少
http
请求数。 - 编译其他预处理语言。
但打包之后的代码常常晦涩难懂,并且开发代码不用于运行代码,不好调试。
当我们直接运行没经过处理的代码,出现了问题我们可以第一时间定位到错误代码的位置。但对于处理过的,比如压缩后的代码,你根本看不出它所对应的源码位置在哪里。
这就是压缩过后的代码,讲真,🐶 都不看。
在我们使用打包工具如 Webpack,Rollup 等,配置了 SourceMap
后(开发环境下会自动开启该功能)。
Webpack 的 Source map
在 Webpack 中(这里特指 Webpack5),配置 devtool
属性可以开启源码映射功能,Webpack 有几十种 Source map 配置,因此这里不多讲,详情请看:Devtool。
对于开发环境,官方认为最佳的配置是 eval-source-map
,这表示每个模块使用 eval()
执行,并且 Source map 转换为 DataUrl 后添加到 eval()
中。初始化 Source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 Source map。
许多项目使用 eval
配置 devtool
,这样会更快速地构建项目,由于会映射到转换后的代码,而不是映射到原始代码,所以不能正确的显示行数。每个被处理的文件最后一行都会有类似的注释:
//@ sourceURL=...
表示该文件对应的源映射文件的位置(在老版本的 Webpack 中是使用 base64 格式的路径)。
Notice
并不是所有浏览器都支持 Source map,一些浏览器只会将文件最下面的特殊注释看成普通注释。
如何实现映射
说了这么多,只知道 Source map 是什么,那么它是如何实现的呢?
下面内容大多数参考:JavaScript Source Map 详解。
一个 .map
文件通常包含以下内容:
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AACvB,gBAAgB,EAAE;AAClB;"
}
version
:源映射版本。file
:转换后文件名。sourceRoot
:转换前文件所在目录。如果与转换前的文件在同一目录,该项为空。sources
:转换前的文件。该项是一个数组,表示可能存在多个文件合并。names
:转换前的所有变量名和属性名。mappings
:记录位置信息的字符串。
mappings
是整个文件的关键,该属性值是一个很长的字符串,分为三层:
- 行对应:每一行之间用分号
;
相隔开,第一个分号前的内容就是第一行,以此类推。 - 位置对应:每个逗号
,
对应源码中的一个位置,第一个分号前的内容就是该行源码的第一个位置,以此类推。 - 位置转换:以 VLQ 编码表示,代表该位置对应的转换前的源码位置。
我们可以发现,每一个位置都对应着多个 VLQ 编码,每个编码作用不同:
- 第一位,表示这个位置在转换后代码的第几列。
- 第二位,表示这个位置属于sources 属性中的哪一个文件。
- 第三位,表示这个位置属于转换前代码的第几行。
- 第四位,表示这个位置属于转换前代码的第几列。
- 第五位和第六位,表示这个位置属于names 属性的哪一个变量。
所有的值都是以 0 作为基数的。其次,第五位和第六位不是必需的,如果该位置没有对应 names
属性中的变量,可以省略第五位和第六位。再次,每一位都采用 VLQ 编码表示;由于 VLQ 编码是变长的,所以每一位可以由多个字符构成。
VLQ
VLQ(Variable-length quantity) 是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式。这种编码最早用于 MIDI 文件,后来被多种格式采用,它的特点就是可以非常精简地表示很大的数值,用来节省空间。
上图就是 base64 位字符对应的编码表。
举个例子:MACdC
,以十进制数字可表示为 12 0 2 29 2
。我们知道数字在计算机中是以二进制存储和计算的,64 进制数可以表示为 6 位 2 进制数,所以又可以转换为:
12 --> 001100
0 --> 000000
2 --> 000010
29 --> 011101
2 --> 000010
在这六位二进制数中:
- 第一位:是否连续,代表这6个位后面的 6 个位也属于同一个数。如果是 0,表示该数值到这 6 个位结束。
- 中间四位:数值位。
- 末位:符号位,如果是 0,代表该数值是一个整数。如果是 1 代表该数值是一个负数。
中间 4 位的二进制数,算上符号位,可以表示 [-15, 15]
区间内的 31 位数。
因此,算数值位和符号位,MACdC
真正代表的其实是:6 0 1 -14 1
。相对应的是:
- 转换后代码的第 7 列。
- 代码属于
sources
中的第一个文件。 - 属于转换前代码的第 2 行。
- 属于转换前代码的相对位置的 -13 列。
- 属于
names
中的第 2 个变量。
我们来看下这个例子(来自于source map 原理分析&vlq):
{
"sources":["test.js"],
"names":["sayHello","name","console","log"],
"mappings":"AAAA,SAASA,SAASC,MACdC,QAAQC,IAAI,SAAUF"
}
我们来分析一下 mappings
里面的 VLQ 编码,按照编码表可以转换成:
base64编码 十进制 转成二进制取出数值和符号位
AAAA --> 0 0 0 0 --> 0 0 0 0
SAASA --> 18 0 0 18 0 --> 9 0 0 9 0
SAASC --> 18 0 0 18 2 --> 9 0 0 9 1
MACdC --> 12 0 2 29 2 --> 6 0 1 -14 1
QAAQC --> 16 0 0 16 2 --> 8 0 0 8 1
IAAI --> 8 0 0 8 --> 4 0 0 4
SAAUF --> 18 0 0 20 5 --> 9 0 0 10 -2
得出了最终的数值,在知道每一位所代表的意义之后,我们就能理解 VLQ 编码了。
总结
对于前端开发来说,Source map
是非常必要且好用的 debug 工具,它通过 VLQ 编码来实现源代码到打包后代码的映射。
但请注意:在生产环境下开启 Source map
让我们即使运行打包后的代码也能找出源码中的错误。但这样做会显著的增加项目包的大小,因为项目中会包含许多源映射 .map
文件。