source map原理分析
现在很多打包工具压缩一个js都会生成一个source map。source map可以帮助我们调试线上的代码,因为压缩后的代码往往是一行,利用source map可以将代码还原,那么source map是如何工作的?
位置映射
source map中有一个mapping(映射),它记录了一个文件处理前后的所有内容的位置映射关系,这是它可以还原内容的核心,现在看看咋映射的:
假设现在有a.js,内容为feel the force,处理后为b.js,内容为the force feel,那么mapping应该是多少呢?
上图可以看到,所谓映射,就是指一个字符从一个位置移动到了另一个位置,然后我们将这个位置的变换记录下来。就好比我们在家里打扫卫生,我们要把家具发生移动,同时我们要记住每个家具之前在什么位置,这样等我们打扫完了,就可以还原了。
我们把每个字符的位置移动都写成一种固定的格式,里面包含了之前的位置(输入位置)和移动之后的位置(输出位置),同时还包含输入文件名,为啥要包含输入文件名?因为我们可能把很多文件进行处理输出,如果不写文件名,可能不知道输入位置来自哪个文件。
字符串提取
对于字符来说,例如f,e,e,l四个字符,其实在处理的时候,是将它们作为一个整体移动的,因为处理是不会改变它们内部的顺序,因此我们可以把相关的字符组成组合进行存储:
看看我们现在的存储结构,可以发现有a.js和the这种字符,我们可以把它们抽离出来放在数组里,然后用下标表示它们,这样可以减少mapping的大小:
sources中存储的是所有的输入文件名,names是所有提取的字符组合。需要表示的时候,用下标即可。
省去输出行号
很多时候,我们输出的文件都是一行,这样输出的行号就可以省略,因为都是0,没必要写出来,我们可以把我们的存储单元再缩短一点:
使用相对位置
mapping中的位置记录我们一直用的都是绝对位置,就是这个组合/字符在文件的第几行,第几列,如果文件特别大的话,那么行列就会很大,因此我们可以用相对位置记录行列信息:
第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the的输出位置为(0,-10),因为the在feel的左边数10下才能到the的位置。
到现在为止,我们得到了一个简单的mappings:
1. sources:\['a.js'\]
2. names:\['feel','the','force'\]
3. mappings:\[10|0|0|0|0,-10|0|0|5|1,4|0|0|4|2\]
但是我们看看真正的一个source map:
1. "sources":\["test.js"\],
2. "names":\["sayHello","name","console","log"\],
3. "mappings":"AAAA,SAASA,SAASC,MACdC,QAAQC,IAAI,SAAUF"
我们发现很多AABB的,和我们竖线分割不一样啊, 这是咋回事呢?其实这是VLQ编码,专门用来解决竖线分割数字问题的,毕竟竖线看起来又low又浪费空间。
vlq编码
VLQ 是 Variable-length quantity ,是一种可变长度的编码。
我们之前用竖线分割数字,是为了用一个字符串可以存储多个数字,例如:1|23|456|7。但是这样每个|会占用一个字符,vlq的思路则是对连续的数字做上某种标记:
我们可以发现,这种标记只在数字不是结尾的部分才有,如果是123,那么1,2都有标记,最后的3没有标记,没有标记也就意味着完结。
那么这种标记法的具体实现是什么呢?vlq利用6位进行存储,其中第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了。
我们来看几个用vlq表示的数字就明白了:
上面就是利用vlq编码划分的结果,有一些需要注意的点:
1.如果这个数字在[-15,15]内,一个单元就可以表示,例如上面的7,只需要把7的二进制放入中间的四位就好。
2.如果超过[-15,15],就要用多个单元表示,需要对数字的二进制进行划分,按照..5554的规则划分。把最右边的4位放入第一个单元中,然后每5个放入一个新单元的右边。为啥第一个单元只放4位?因为第一个单元的最后一位是表示正负数的,其他单元的最后一位没必要表示正负了。
3.如果是负数,我们求的是它正数的二进制,放还是按照之前的规则放,只是把第一个单元的最后一位改成1就好。
最后把划分号的6位变成Base64编码,因为Base64也是6位一单元,和这里一样。下面有一个demo,将输入的内容变成字符码数组,然后用vlq&base64编码:
上面的demo中有vlq的encode和decode编码实现,想学的朋友可以自行查看。
4位mapping
我们可以自己做一个简单的demo去看看source map生成的mapping。首先安装uglify.js,然后写一个简单的test.js,压缩test.js:
1. npm install uglify\-js \-g
2. uglifyjs test.js \-o output.js \--source\-map "url='output.js.map'"
下面是压缩前后代码和source map
1. /\*test.js\*/
2. function sayHello(name) {
3. console.log('hello,', name)
4. }
5. /\*output.js\*/
6. function sayHello(name){console.log("hello,",name)}
7. //# sourceMappingURL=output.js.map
8. /\*output.js.map\*/
9. {"version":3,"sources":\["test.js"\],"names":\["sayHello","name","console","log"\],"mappings":"AAAA,SAASA,SAASC,MACdC,QAAQC,IAAI,SAAUF"}
我们用vql解析mappings,得到[0|0|0|0 , 9|0|0|9|0 , 9|0|0|9|1 , 6|0|1|-14|1 , 8|0|0|8|1 , 4|0|0|4 , 9|0|0|10|-2]
首先我们看到有些是5位,有些是4位,5位的我们之前已经知道,输出列|输入文件名|输入行|输入列|字符组合,4位则少了最后的字符组合,一般用来矫正位置。
2人已点赞
