原理-SourceMap
简单的来说, Source Map就是一个信息文件, 里面存储了位置信息. 也就是说, 转换后的代码的每一个位置, 所对应的转换前的位置. 这样一来, 在浏览器中进行断点调试的时候, 就能直接显示原始代码, 而不是转换后的代码.
为什么要用 srouce Map就是一个信息文件
一般来说, 我们发布到生产环境的代码都进行了如下的操作:
- 通过编译和转译, 将其他语言的代码(typescript)编译成javascript
- 多个文件合并, 减少HTTP请求数量
- 压缩混淆, 减小代码体积
对于线上代码报错, 直接通过源码来捕获和调试都是非常困难的.
有了Source Map, 出错的时候我们就可以直接定位到原始代码, 而不是转换后的代码.
启用 source map
source map 主要要求浏览器支持, 目前主流PC端浏览器支持SourceMap的情况如下:

假设我们有个app.js文件, 则会有一个同名的map文件在同一个路径下面:
- app.js
- app.js.map
在app.js的文件末尾, 会有一行指向map文件的引用:
//# sourceMappingURL=app.js.map
其实还可以生成内嵌的sourcemap:
'//# sourceMappingURL=data:application/json;charset=utf8;base64,' + base64Map;
Source Map 文件解析
map文件的格式如下:
{
version : 3, //SourceMap的版本,目前为3
sources: ["foo.js", "bar.js"], //转换前的文件,该项是一个数组,表示可能存在多个文件合并
names: ["src", "maps", "are", "fun"], //转换前的所有变量名和属性名
mappings: "AACvB,gBAAgB,EAAE;AAClB;", //记录位置信息的字符串
file: "out.js", //转换后的文件名
sourcesContent: ["\t// The module cache\n", "xxx"], //转换前的文件内容列表,与sources列表依次对应
sourceRoot : "" //转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
}
mappings
那么, 压缩过的文件和原始文件是如何一一对应的呢?
关键就在于map文件的mappings属性. 这是一个很长的字符串, 它分成三层.
- 第一层: 行对应, 以分号
;表示, 每个分号对应转换后源码的一行. 所以, 第一个分号前的内容, 就对应源码的第一行, 以此类推. - 第二层: 位置对应, 以逗号
,表示, 每个逗号对应转换后源码的一个位置. 所以, 第一个逗号前的内容, 就对应该行源码的第一个位置, 以此类推. - 第三层: 位置转换, 以
VLQ编码表示, 代表该位置对应的转换前的源码位置.
位置关系的对应
每个位置使用5位, 表示5个字段, 从左边开始:
- 第一位: 表示这个位置在转换后的代码的第几列
- 第二位: 表示这个位置属于source属性中的哪一个文件
- 第三位: 表示这个位置属于转换前代码的第几行
- 第四位: 表示这个位置属于转换前代码的第几列
- 第五位: 表示这个位置属于names属性中的哪一个变量
这里有些概念需要说明:
- 所有的值都是以0作为基数的
- 第五位不是必须的, 如果该位置没有对应names属性中的变量, 可以省略第5位.
- 每一位都采用了VLQ编码表示, 由于VLQ编码是变长的, 所以每一位可以由多个字符构成
映射关系实例
假设我们现在有一个a文件, 内容为:
feel the force
处理之后变成了b文件:
the force feel
以字符h为例, 它在输入中的位置为(0, 6), 在输出中的位置为(0, 1), 那么映射关系为:
1 | 0 | 0 | 5 | 1
// 输出列 | 输入文件下标 | 输入行 | 输入列 | 变量下标
- 输入文件可以抽离出来放在数组中, 减少mapping文件的大小, names同理
- 很多时候, 我们输出的文件都是同一行的, 这样输出的行号就可以省略
以这里的例子:
- source:
['a.js'] - names:
['feel', 'the', 'force']
mapping中的位置一直用的是绝对定位, 如果文件特别大的话, 行列就会很大, 因此我们可以用相对位置进行记录.
其中第一次输入的位置和输出位置是绝对的, 往后的输入位置和输出位置都是相对于上一次的位置移动了多少. 例如: the的输出位置为(0, -10), 因为the在feel的左边数10下才能到the的位置, 这样我们就得到了一个简单的mapping:
sources:['a.js']
names:['feel','the','force']
mappings:[10|0|0|0|0,-10|0|0|5|1,4|0|0|4|2]
然后, 对这些竖线分割的数字进行VLQ编码.
VLQ编码
VLQ是Variable-length quantity的缩写, 是一种通用的, 使用任意位数的二进制来表示一个任意大的数字的一种编码方式
这种编码最早用于MIDI文件, 后来被多种格式采用. 特点是可以非常精确的表示很大的数值.
VLQ编码是变长的. 每7位表示一个数字. 开头的第一位表示是否连续(continuation), 如果是1, 表示下面7位也是同一个数, 如果是0, 表示数据到此结束
VLQ 编码实例
比如将一个137进行VLQ编码:
- 将137改写为二进制: 10001001
- 7位一组分组, 不足的补0: 0000001 0001001
- 最后一组开头补0, 其余补1: 10000001 00001001
Base64 VLQ
如果整数值在-15到+15之间(包含端点), 用一个字符表示. 超出这个范围就需要用多个字符表示.
与一般的VLQ的区别:
- 一个Base64字符只能表示6bit(2^6)的数据
- Base64 VLQ需要能够表示负数, 因此最后一位作为符号位, 0表示正数, 1表示负数
- 只能用6位进行存储, 所以一个单元表示的范围为
[-15, 15], 如果超过了就要使用连续标志位
Continuation
| Sign
| |
V V
101011
而6个位中的右边最后一位, 取决于这6个位是否是某个数值的VLQ编码的第一个字符.
- 如果是: 这一个为表示符号(sign), 0为正, 1位负(Source map的符号固定为0);
- 如果不是: 这个位没有特殊含义, 算作数值的一部分;
6位的设计主要是为了可以借用base 64编码的字符表.

下面看一个例子, 如何对数值16进行VLQ编码:
- 将16改写为二进制:
10000 - 在最右边补充符号位, 因为16大于0, 所以符号位为0, 整个数字为:
10000 0 - 从右边的最低位开始, 将整个树每隔5位, 进行分段, 即变为
1和00000两段. 如果最高位所在的段不足5位, 则前面补0, 也就是变成了00001和00000 - 将两段的顺序颠倒, 即
00000,00001 - 在每一段的最前面添加一个'连续位', 除了最后一段为0, 其他都为1, 即
100000和000001 - 将每一段转换为
Base 64编码, 查表可知100000为g,000001为B
最终, 数值16被编码为gB.
小结
可以看出:
- 在
Base64 VLQ中, 编码顺序是从低位到高位 - 在
VLQ中, 编码顺序是从高位到低位