前言
随着前端代码越来越复杂,开发者在部署之前通常会对代码进行打包压缩进而减小代码大小,从而有效提高访问速度。例如,一段压缩后的前端代码为 ajax.googleapis.com/ajax/libs/j… ,滚动到底部我们可以看到最后一行为:
/@ sourceMappingURL=jquery.min.map
这行代码其实指明了压缩后的 JavaScript 文件所对应的 Map 文件,为同一目录下的 ajax.googleapis.com/ajax/libs/j… ,通过这个示例我们可以了解到 Source Map 文件的基本结构。
Source Map 要解决的问题
JavaScript 脚本正变得越来越复杂,大部分源码都需要通过各种转换才能投入生产环境。例如,前端的编译处理包括但不限于
-
转译器/Transpilers (Babel, Traceur)
-
编译器/Compilers (Closure Compiler, TypeScript, CoffeeScript, Dart)
-
压缩/Minifiers (UglifyJS)
经过一系列神奇操作后,发布到线上的代码往往面目全非,对带宽友好了,但当开发者需要debug时,面对一堆转换过后的混乱代码往往毫无头绪。
此时, Source Map 出现了。顾名思义,Source Map 就是源码的映射,可以将压缩后的代码对应回未压缩的源码,使得调试线上产品变得轻松而友好。
Source Map 是如何解决的
简而言之,Source Map 是一个信息文件,里面存储着位置信息。在使用 Source Map 之后,开发者面对报错信息时,调试工具将直接显示原始代码,而不是转换后的代码,这就给开发者带来了巨大方便。
文件结构
JavaScript、CSS 代码的 Source Map 是根据 Source Map Revision 3 Proposal 这个提案来实现的。提案里用一句话描述其特点为” Better bidirectional mapping ”,即“更好的双向映射”。
按照提案,一个 Source Map 文件的内容实际上是一个 JSON 对象:
{
"version" : 3, // 生成当前 Source Map 时所遵从的提案(Source Map Revision)版本号。
"file": "out.js", // 可选,转换后的文件名。
"sourceRoot": "", // 可选,转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
"sources": ["foo.js", "bar.js"], // 源文件列表,后面的 mappings 字段会用到这个列表。该项是一个数组,表示可能存在多个文件合并。
"sourcesContent": [null, null], // 可选,源文件内容,可用于源文件无法托管时。
"names": ["src", "maps", "are", "fun"], // mappings 所用到的符号名称列表,转换前的所有变量名和属性名。
"mappings": "A,AAAB;;ABCDE;" // 字符串,编码后的映射信息数据。
}
映射规则
mappings 属性
两个文件的各个位置能够一一对应的关键,在于 Source Map 文件中的 mappings 属性。它的含义可以分为三层:
-
第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
-
第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
-
第三层是位置转换,以 VLQ编码 表示,代表该位置对应的转换前的源码位置。
举例来说,假定mappings属性的内容如下:
mappings:"AAAAA,BBBBB;CCCCC"
就表示,转换后的源码分成两行,第一行有两个位置,第二行有一个位置。
位置映射的原理
每个位置使用五位,表示五个字段。
从左边算起,
-
第一位,表示这个位置在(转换后的代码的)的第几列。
-
第二位,表示这个位置属于sources属性中的哪一个文件。
-
第三位,表示这个位置属于转换前代码的第几行。
-
第四位,表示这个位置属于转换前代码的第几列。
-
第五位,表示这个位置属于names属性中的哪一个变量。
有几点需要说明。首先,所有的值都是以0作为基数的。其次,第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位。再次,每一位都采用VLQ编码表示;由于VLQ编码是变长的,所以每一位可以由多个字符构成。
如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。
VLQ 编码
这种编码最早用于MIDI文件,后来被多种格式采用。它的特点就是可以非常精简地表示很大的数值。
例如,假设我们想记录如下四个数字:
1|23|456|7
上面的示例中,引入了数字系统外的符号来标识一个数字还未结束。在二进制系统中,我们使用6个比特来记录一个数字(可表示至多64个值),用其中一个字节来标识它是否未结束(正文 C 标识),不需要引入额外的符号,再用一位标识正负(下方 S),剩下还有四位用来表示数值。用这样6个字节组成的一组拼起来就可以表示出我们需要的数字串了。
VLQ(Variable Length Quantities)编码格式
第一个字节组:
后续字节组:
现在我们使用上面的二进制规则来重新编码之前的这个数字序列 1|23|456|7。
先看每个数字对应的真实二进制是多少:
- 对1进行编码:1需要一位来表示,还好对于首个字节组,我们有四位来表示值,所以是够用的。
- 对23进行编码:23的二进制为10111一共需要5位,第一组字节组只能提供4位来记录值,所以用一组字节组不行,需要使用两组字节组。将 10111拆分为两组,后四位0111放入第一个字节组中,剩下一位1放入第二个字节组中。
- 对456进行编码:456的二进制111001000需要占用9个字节,同样,一个字节组放不下,先拆出最后四位(1000)放入一个首位字节组中,剩下的5位(11100)放入跟随的字节组中。
- 对7进行编码:7的二进制为111,首位字节组能够存放得下。
将上面的编码合并得到最终的编码:
000010 101110 000001 110000 011100 001110
结合上面的 Base64 编码表,上面的结果转成对应的 base64 字符为:
CuBwcO