一、前言
随着前端代码越开越复杂的情况下,开发者通常会使用webpack、UglifyJS2等工具对代码进行打包变换,这样可以减少代码大小,有效提高访问速度,同时还能够有效的保护源代码不被别人获取。
正常代码hello.js:
function sayHello() {
var name = "Fundebug";
var greeting = "Hello, " + Name;
console.log(greeting);
}
sayHello();
>>>>>>>>>>使用UglifyJS2对源码进行压缩 start>>>>>>>>>>>>
uglifyjs hello.js \
-m toplevel=true \
-c unused=true,collapse_vars=true \
-o hello.min.js
>>>>>>>>>>使用UglifyJS2对源码进行压缩 end>>>>>>>>>>>>
压缩后的代码hello.min.js:
function o(){var o="Hello, "+Name;console.log(o)}o();
然而压缩代码的报错信息是很难Debug的,因为它的行号和列号已经失真。
执行hello.js的报错信息,我们能够直接定位到出错的位置。

而执行压缩后的hello.min.js的报错信息,

对比压缩前后的出错信息,我们会发现,错误行号和列号已经失真,且函数名也经过了变换。而对于真实的前端项目,开发者会将数十个源文件压缩为一个文件,这时,错误的列号可能多达数千,且出错的真实文件名也是很难确定的,这样的话,压缩代码的报错信息是很难Debug的。
而Source Map则可以用于还原真实的出错位置,帮助开发者更快的Debug。
二、什么是SourceMap
简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。
1、生成SourceMap文件
目前各种主流前端任务管理工具,打包工具都支持生成Source Map,具体可以查看生成SourceMap。
使用UglifyJS2时指定source-map选项即可生成Source Map:
>>>>>>>>>>使用UglifyJS2生成SourceMap start>>>>>>>>>>>>
uglifyjs hello.js \
-m toplevel=true \
-c unused=true,collapse_vars=true \
--source-map url='hello.min.js.map' \
-o hello.min.js
>>>>>>>>>>使用UglifyJS2生成SourceMap end>>>>>>>>>>>>
添加了SourceMap的hello.min.js:
function o(){var o="Hello, "+Name;console.log(o)}o();
//# sourceMappingURL=hello.min.js.map
生成的SourceMap文件hello.min.js.map:
{
"version": 3,
"sources": [
"hello.js"
],
"names": [
"sayHello",
"greeting",
"Name",
"console",
"log"
],
"mappings": "AAAA,SAASA,IACL,IACIC,EAAW,UAAYC,KAC3BC,QAAQC,IAAIH,GAEhBD"
}
2、SourceMap文件解析
由hello.min.js.map可知,Source Map是一个JSON文件,而它包含了代码转换前后的位置信息。也就是说,给定一个转换之后的压缩代码的位置,就可以通过Source Map获取转换之前的代码位置,反过来也一样。Source Map各个属性的含义如下:
- version:Source Map的版本号。
- sources:转换前的文件列表。该项是一个数组,表示可能存在多个文件合并。
- names:转换前的所有变量名和属性名。
- mappings:记录位置信息的字符串,经过编码。
- file:(可选)转换后的文件名。
- sourceRoot:(可选)转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
- sourcesContent:(可选)转换前的文件内容列表,与sources列表依次对应。
Source Map真正神奇之处在于mappings属性,它记录了位置是如何对应的。
3、mappings属性
sourcemap的mappings属性的值是将位置映射关系采用Base 64 VLQ编码后生成的字符串。
mappings属性值包含的信息可以按照下面的方式进行拆分:
- 第一层是行对应,以分号(;)标识编译后代码的每一行,即是分号间隔的内容代表编译后代码的一行;
- 第二层是位置对应,以逗号(,)标识编译后代码该行中的每一个映射位置,即是逗号间隔的内容代表一个映射位置;
- 第三层是位置转换,以5组VLQ编码字段标识源码和编译后代码的具体映射信息。从左至右每组表示如下:
- 第1组,表示对应编译后代码的第几列(从第0列开始计算);
- 第2组,表示源码所属文件在sources数组中的索引值;
- 第3组,表示对应源码的第几行(从第0行开始计算);
- 第4组,表示对应源码的第几列(从第0列开始计算);
- 第5组,表示在names数组中的索引值,若没有则可省略。
注意:每组VLQ编码字段有0~N个VLQ编码字符组成,如 qC | A | A | U | H。
例如“AAAAA”表示的就是:该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。
4、VLQ编码
VLQ编码最早用于MIDI文件,后来被多种格式采用。它的特点就是可以非常精简地表示很大的数值。它规定,每个字符使用6个两进制位。
如下图所示:

- 在这6个位中,左边的第一位(最高位)表示是否"连续"(continuation)。如果是1,代表这6个位后面的6个位也属于同一个数;如果是0,表示该数值到这6个位结束。
- 这6个位中的右边最后一位(最低位)的含义,取决于这6个位是否是某个数值的VLQ编码的第一个字符。如果是的,这个位代表"符号"(sign),0为正,1为负(Source map的符号固定为0);如果不是,这个位没有特殊含义,被算作数值的一部分。
上面看的可能比较迷糊,我们看一个例子,如何对数值137进行VLQ编码:
- 先把 137 转换成二进制:10001001
- 由于 137 是正数,所以在最低位补0 变成 100010010
- 按照 5bit 一组的方式分组,变成 01000 10010
- 按照从低位到高位的顺序,以 5bit 一组为单位,依次拿出数据做转换。
- 先拿出第一组 10010 , 因为后面还有数据,所以需要在高位补 1,变成 110010 ,编码成 Base64: y
- 再拿出第二组,也是我们这里的最后一组,01000 , 由于是最后一组,所以在高位补 0 变成 001000,编码成 Base64: I
- 按照从低位到高位的顺序把这些 ASCII 字符拼接起来,变成 yI
注意:转换为二进制码之后,是从低位到高位的进行Base64转换。
下图为base64的映射表:

根据以上对VLQ编码的方式,我们能够掌握了如何将一个数值进行base64 VLQ编码,了解了编码之后,解码也不必在讲解了,倒推的回去就行了。这里推荐一个在线的VLQ编解码网站:Base64VLQ在线编解码网站。
5、mappings位置数值解读
我们了解完了VLQ的编解码之后,我们将开头生成的SourceMap文件的mappings属性分析一下看看是否能够一一对应。
开头生成的SourceMap文件的mappings属性为:
"mappings": "AAAA,SAASA,IACL,IACIC,EAAW,UAAYC,KAC3BC,QAAQC,IAAIH,GAEhBD"
我们使用Base64VLQ在线编解码网站把内容解码一下:
[0,0,0,0], [9,0,0,9,0], [4,0,1,-5], [4,0,1,4,1], [2,0,0,11],
[10,0,0,12,1], [5,0,1,-27,1], [8,0,0,8,1], [4,0,0,4,-3], [3,0,2,-16,-1]
注意:mappings的位置值都是相对位置,每个值都是相对于前一个位置值的。(当文件内容巨大时,精简后的编码也有可能会因为数字位数的增加而变得很长,同时,处理较大数字总是不如处理较小数字容易和方便。于是mappings中的位置记录是记录的这些位置的相对值。)
因此每一个位置值加上前一个位置值可以得到:
[0,0,0,0], [9,0,0,9,0], [13,0,1,4], [17,0,2,8,1], [19,0,2,19],
[29,0,2,31,2], [34,0,3,4,3], [42,0,3,12,4], [46,0,3,16,1], [49,0,5,0,0]
整理一下可以得出如下结果:
(a,b)=>name(m,n)表示生成代码中的a行b列对应原始代码中的m行n列的位置,并且在原始代码中这个位置的变量名是name。
(0,0)=>(0,0)
(0,9)=>sayHello(0,9)
(0,13)=>(1,4)
(0,17)=>greeting(2,8)
(0,19)=>(2,19)
(0,29)=>Name(2,31)
(0,34)=>console(3,4)
(0,42)=>log(3,12)
(0,46)=>greeting(3,16)
(0,49)=>(5,0)