在浏览器中开启Source Map
- Source Map在浏览器中默认是关闭的,这样就不会影响正常用户。当我们开启后,浏览器就根据压缩代码中指定的Source Map地址去请求 map 资源。
Source Map基本结构
{
"version":3,
"sources":["index.js"],
"names":["add","a","b"],
"mappings":"AAAA,SAASA,IAAIC,EAAGC,GACd,OAAOD,EAAIC"
}
Source Map功能
- 输入(js) ⇒ 处理转换(压缩) ⇒ 输出(js)
- Source Map的功能是帮助我们在拿到输出后还原回输入
Source Map映射解析
- 最直观的想法恐怕是,将生成的文件中每个字符位置对应的原位置保存起来,一一映射
“feel the force” ⇒ Yoda_input.txt(压缩输出) ⇒ “the force feel”
- 将上面的的输入与输出列成表格可以得出这个转换后输入与输出的对应关系。
输出位置 | 输入 | 在原输入中的位置 | 字符 |
---|
行 1, 列 0 | Yoda_input.txt | 行 1, 列 5 | t |
行 1, 列 1 | Yoda_input.txt | 行 1, 列 6 | h |
行 1, 列 2 | Yoda_input.txt | 行 1, 列 7 | e |
行 1, 列 4 | Yoda_input.txt | 行 1, 列 9 | f |
行 1, 列 5 | Yoda_input.txt | 行 1, 列 10 | o |
行 1, 列 6 | Yoda_input.txt | 行 1, 列 11 | r |
行 1, 列 7 | Yoda_input.txt | 行 1, 列 12 | c |
行 1, 列 8 | Yoda_input.txt | 行 1, 列 13 | e |
行 1, 列 10 | Yoda_input.txt | 行 1, 列 0 | f |
行 1, 列 11 | Yoda_input.txt | 行 1, 列 1 | e |
行 1, 列 12 | Yoda_input.txt | 行 1, 列 2 | e |
行 1, 列 13 | Yoda_input.txt | 行 1, 列 3 | l |
- 上面可以直观看出,生成文件中 (1,0) 位置的字符对应源文件中 (1,5)位置的字符,…
将上面的表格整理记录成一个映射编码看起来会是这样的
mappings(283 字符):1|0|Yoda_input.txt|1|5, 1|1|Yoda_input.txt|1|6, 1|2|Yoda_input.txt|1|7, 1|4|Yoda_input.txt|1|9, 1|5|Yoda_input.txt|1|10, 1|6|Yoda_input.txt|1|11, 1|7|Yoda_input.txt|1|12, 1|8|Yoda_input.txt|1|13, 1|10|Yoda_input.txt|1|0, 1|11|Yoda_input.txt|1|1, 1|12|Yoda_input.txt|1|2, 1|13|Yoda_input.txt|1|3
省去输出文件中的行号
- 大多数情况下处理后的文件行数都会少于源文件,特别是 js,使用 UglifyJS 压缩后的文件通常只有一行。因此,没必要在每条映射中都带上输出文件的行号,转而在这些映射中插入;来标识换行,可以节省大量空间。
mappings (245 字符): 0|Yoda_input.txt|1|5, 1|Yoda_input.txt|1|6, 2|Yoda_input.txt|1|7, 4|Yoda_input.txt|1|9, 5|Yoda_input.txt|1|10, 6|Yoda_input.txt|1|11, 7|Yoda_input.txt|1|12, 8|Yoda_input.txt|1|13, 10|Yoda_input.txt|1|0, 11|Yoda_input.txt|1|1, 12|Yoda_input.txt|1|2, 13|Yoda_input.txt|1|3;
换元法
- 一共有三个单词,拿输出文件中 the 来说,当我们通过它的第一个字母t(1,0)确定出对应源文件中的位置(1,5),后面的he 其实不用再记录映射了,因为the 可以作为一个整体来看,试想 js 源码中一个变量名,函数名这些都不会被拆开的,所以当我们确定的这个单词首字母的映射关系,那整个单词其实就能还原到原来的位置了
names: ['the','force','feel']
- 在记录时,只需要记录一个索引,还原时通过索引来这个names 数组中找即可。所以上面映射规则中最后一列本来记录了每个字符,现在改为记录一个单词,而单词我们只记录其在抽取出来的符号数组中的索引。
- the 的映射可以简化为:
0|Yoda_input.txt|1|5, 1|Yoda_input.txt|1|6, 2|Yoda_input.txt|1|7
0|Yoda_input.txt|1|5|0
- 同时,考虑到代码经常会有合并打包的情况,即输入文件不止一个,所以可以将输入文件抽取一个数组,记录时,只需要记录一个索引,还原的时候再到这个数组中通过索引取出文件的位置及文件名即可
sources: ['Yoda_input.txt']
0|0|1|5|0
sources: ['Yoda_input.txt']
names: ['the','force','feel']
mappings (31 字符): 0|0|1|5|0, 4|0|1|9|1, 10|0|1|0|2;
记录相对位置
- 当文件内容巨大时,上面精简后的编码也有可能会因为数字位数的增加而变得很长
- 将上面记录的这些位置用相对值来记录。
原输出位置 | 相对输出位置 |
---|
行 1, 列 0 | 行 1, 列 0 |
行 1, 列 4 | 行 1, 列 4(上一值 + 4 = 4) |
行 1, 列 10 | 行 1, 列 6(上一值 + 6 = 10) |
sources: ['Yoda_input.txt']
names: ['the','force','feel']
mappings (31 字符): 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1;
VLQ (Variable Length Quantities)
VLQ 以二进制方式的方式呈现
- 用6个位来记录一个数字(可表示至多64个值),用其中一个位来标识它是否未结束(下方 C 标识),再用一位标识正负(下方 S),剩下还有四位用来表示数值。用这样6个字节组成的一组拼起来就可以表示出我们需要的数字串了。
- C为1表示未结束,S为1表示负数,其他Value存储值,第一个6位的组,只能存储4个value
- 但如果未结束,因为第一个组中已经标明了该数字的正负,所以后续的字节组中无需再标识,于是可以多出一位来作表示值
B5 | B4 | B3 | B2 | B1 | B0 |
---|
C | Value | Value | Value | Value | S |
位 结果
000000 0
000001 -0
000010 1
000011 -1
000100 2
000101 -2
… …
011110 15
011111 -15
100000 未结束的0
100001 未结束的-0
100010 未结束的1
100011 未结束的-1
… …
111110 未结束的15
111111 未结束的-15
数值 二进制
1 1
23 10111
456 111001000
7 111
B5(C) B4 B3 B2 B1 B0(S)
0 0 0 0 1 0
- 对23进行编码
- 23的二进制为10111一共需要5位,第一组字节组只能提供4位来记录值,所以用一组字节组不行,需要使用两组字节组。将 10111拆分为两组,后四位0111放入第一个字节组中,剩下一位1放入第二个字节组中
- 因为第一个组已经表示了正负S,所以第二个组可以有五位来存储value
B5(C) B4 B3 B2 B1 B0(S) B5(C) B4 B3 B2 B1 B0
1 0 1 1 1 0 0 0 0 0 0 1
- 对456进行编码
- 456的二进制111001000需要占用9个字节,同样,一个字节组放不下,先拆出最后四位(1000)放入一个首位字节组中,剩下的5位(11100)放入跟随的字节组中。
B5(C) B4 B3 B2 B1 B0(S) B5(C) B4 B3 B2 B1 B0
1 1 0 0 0 0 0 1 1 1 0 0
B5(C) B4 B3 B2 B1 B0(S)
0 0 1 1 1 0
000010 101110 000001 110000 011100 001110
CuBwcO
利用 Base64 VLQ 编码生成最终的 srouce map
sources: ['Yoda_input.txt']
names: ['the','force','feel']
mappings (31 字符): 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1;
- 现在来编码 0|0|1|5|0。先用二进制对每个数字进行表示,再转成 VLQ 表示:
0-> 0 -> 000000
0-> 0 -> 000000
1-> 1 -> 000010
5-> 101 -> 001010
0-> 0 -> 000000
000000 000000 000001 000101 000000
AACKA
sources: ['Yoda_input.txt']
names: ['the','force','feel']
mappings (18 字符): AACKA, IACIC, MACTC;
{
"version": 3,
"file": "Yoda_output.txt",
"sources": ["Yoda_input.txt"],
"names": ["the", "force", "feel"],
"mappings": "AACKA,IACIC,MACTC;"
}
略去不必要的字段
真实场景中,有些情况下某些字段其实不必要,这时就可以将其省略
- 5 - 包含全部五个部分:输出文件中的列号,输入文件索引,输入文件中的行号,输入文件中的列号,符号索引
- 4 - 输出文件中的列号,输入文件索引,输入文件中的行号,输入文件中的列号
- 1 - 输出文件中的列号