现在很多打包工具压缩一个js都会生成一个source map。source map可以帮助我们调试线上的代码,因为压缩后的代码往往是一行,利用source map可以将代码还原,那么source map是如何工作的?

位置映射

source map中有一个mapping(映射),它记录了一个文件处理前后的所有内容的位置映射关系,这是它可以还原内容的核心,现在看看咋映射的:

假设现在有a.js,内容为feel the force,处理后为b.js,内容为the force feel,那么mapping应该是多少呢?

background Layer 1 f e e l t h e f o r r c e 输入(a.js) t h e f o r r c e f e e l 输出(b.js) 以字符h为例,它在输入中的位置为(0,6)在输出中的位置为(0,1),那么映射关系为: 0 | 1 | a.js | 0 | 6 输出row(行) 输出column(列) 输入文件名 输入row(行) 输入column(列) 所有的字符映射(mapping)为: 输入位置 映射 (0,0) 输出位置 (0,10) 0 | 10 | a.js | 0 | 0 字符 f (0,1) (0,11) 0 | 11 | a.js | 0 | 1 e (0,2) (0,12) 0 | 12 | a.js | 0 | 2 e (0,3) (0,13) 0 | 13 | a.js | 0 | 3 l (0,5) (0,0) 0 | 0 | a.js | 0 | 5 t (0,6) (0,1) 0 | 1 | a.js | 0 | 6 h (0,7) (0,2) 0 | 2 | a.js | 0 | 7 e (0,9) (0,4) 0 | 4 | a.js | 0 | 9 f (0,10) (0,5) 0 | 5 | a.js | 0 | 10 o (0,11) (0,6) 0 | 6 | a.js | 0 | 11 r (0,12) (0,7) 0 | 7 | a.js | 0 | 12 c (0,13) (0,8) 0 | 8 | a.js | 0 | 13 e

上图可以看到,所谓映射,就是指一个字符从一个位置移动到了另一个位置,然后我们将这个位置的变换记录下来。就好比我们在家里打扫卫生,我们要把家具发生移动,同时我们要记住每个家具之前在什么位置,这样等我们打扫完了,就可以还原了。

我们把每个字符的位置移动都写成一种固定的格式,里面包含了之前的位置(输入位置)和移动之后的位置(输出位置),同时还包含输入文件名,为啥要包含输入文件名?因为我们可能把很多文件进行处理输出,如果不写文件名,可能不知道输入位置来自哪个文件。

字符串提取

对于字符来说,例如f,e,e,l四个字符,其实在处理的时候,是将它们作为一个整体移动的,因为处理是不会改变它们内部的顺序,因此我们可以把相关的字符组成组合进行存储:

background Layer 1 f e e l t h e f o r r c e 输入(a.js) t h e f o r r c e f e e l 输出(b.js) 以the为例,它在输入中的位置是(0,5),输出中的位置是(0,0) 0 | 0 | a.js | 0 | 5 | the 输出row(行) 输出column(列) 输入文件名 输入row(行) 输入column(列) 所有的字符映射(mapping)为: 输入位置 映射 (0,0) 输出位置 (0,10) 0 | 10 | a.js | 0 | 0 | feel 字符组合 feel (0,5) (0,0) 0 | 0 | a.js | 0 | 5 | the the (0,9) (0,4) 0 | 4 | a.js | 0 | 9 | force force 字符组合

看看我们现在的存储结构,可以发现有a.js和the这种字符,我们可以把它们抽离出来放在数组里,然后用下标表示它们,这样可以减少mapping的大小:

background Layer 1 以the为例,它在输入中的位置是(0,5),输出中的位置是(0,0) 0 | 0 | a.js | 0 | 5 | the sources:['a.js'] names:['feel','the','force'] 0 | 0 | 0 | 0 | 5 | 1 sources[0],也就是a.js names[1],也就是the

sources中存储的是所有的输入文件名,names是所有提取的字符组合。需要表示的时候,用下标即可。

省去输出行号

很多时候,我们输出的文件都是一行,这样输出的行号就可以省略,因为都是0,没必要写出来,我们可以把我们的存储单元再缩短一点:

background Layer 1 以the为例,它在输入中的位置是(0,5),输出中的位置是(0,0) sources:['a.js'] names:['feel','the','force'] 0 | 0 | 0 | 0 | 5 | 1 0 | 0 | 0 | 5 | 1 省去了输出行 names[1] 输入行 输入列 sources[0] 输出列

使用相对位置

mapping中的位置记录我们一直用的都是绝对位置,就是这个组合/字符在文件的第几行,第几列,如果文件特别大的话,那么行列就会很大,因此我们可以用相对位置记录行列信息:

background Layer 1 f e e l t h e f o r r c e 输入(a.js) t h e f o r r c e f e e l 输出(b.js) 所有的字符映射(mapping)为: 输入位置 映射 输出位置 (0,10)[绝对] 10 | 0 | 0 | 0 | 0 字符组合 feel (0,5)[相对] (0,-10)[相对] -10 | 0 | 0 | 5 | 1 the (0,4)[相对] (0,4)[相对] 4 | 0 | 0 | 4 | 2 force sources:['a.js'] names:['feel','the','force'] (0,0)[绝对]

第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the的输出位置为(0,-10),因为the在feel的左边数10下才能到the的位置。

到现在为止,我们得到了一个简单的mappings:

但是我们看看真正的一个source map:

我们发现很多AABB的,和我们竖线分割不一样啊, 这是咋回事呢?其实这是VLQ编码,专门用来解决竖线分割数字问题的,毕竟竖线看起来又low又浪费空间。

vlq编码

VLQ 是 Variable-length quantity ,是一种可变长度的编码。

我们之前用竖线分割数字,是为了用一个字符串可以存储多个数字,例如:1|23|456|7。但是这样每个|会占用一个字符,vlq的思路则是对连续的数字做上某种标记:

background Layer 1 1 2 3 4 5 6 7 数字1没有被标记,是一个完整的数字1 数字2被标记了,继续读取下一个,3没被标记,最终是23 数字4被标记了,继续读取下一个,5也被标记,下一个,6没标记,最终456 数字7没有标记,是一个完整的数字7

我们可以发现,这种标记只在数字不是结尾的部分才有,如果是123,那么1,2都有标记,最后的3没有标记,没有标记也就意味着完结。

那么这种标记法的具体实现是什么呢?vlq利用6位进行存储,其中第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了。

我们来看几个用vlq表示的数字就明白了:

background Layer 1 数字 7 二进制 111 vlq表示 001110 内容 是否连续(1连续,0不连续) 正负数(0正数,1负数) 1200 10010110000 100000 内容1 连续标识位 101011 内容2 正数 连续标识位 内容1 内容2 000010 内容3 结束标识位 内容3 -17 10001 (取的是17的二进制) 100011 内容1 连续标识位 000001 内容2 负数 结束标识位 内容1 内容2

上面就是利用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:

下面是压缩前后代码和source map

我们用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位则少了最后的字符组合,一般用来矫正位置。

其他文章

0
我要评论

评论

返回
×

我要评论

回复:

昵称:(昵称不超过20个字)

图片:

提交
还可以输入500个字