为啥用 sourceMap
这几天在搞前端错误日志,做过线上发布的都知道,我们发布到生产环境的代码,一般都有如下步骤:
- 压缩混淆,减小体积
- 多个文件合并,减少
HTTP
请求数 - 通过编译或者转译,将其他语言编译成
JavaScript
这三个步骤,都使得实际运行的代码不同于开发代码,不管是 debug 还是捕获线上的报错,都会变得困难重重。
解决这个问题的方法,就是使用sourceMap
。
啥是 sourceMap
简单说,sourceMap
就是一个文件,里面储存着位置信息。
仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。
有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码。
sourceMap 长啥样
通过 webpack
等工具,我们可以使用 sourceMap
,这里不细说配置方法,可以看这里
sourceMap
是一个 map
文件,与源码在同一个目录下。
在压缩代码的最后一行,会有这样的一个引用:
//# sourceMappingURL=app.js.map
指向的就是我们的 map
文件。
sourceMap
的格式如下:
{
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", // 转换后的代码
sourceRoot : "" // 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
}
其他的都很好解释,我们详细说一下 mappings
属性。
mappings
以 "AACvB,gBAAgB,EAAE;AAClB;"
为例:
- 每个分号对应转换后源码的一行;
- 每个逗号对应转换后源码的一个位置;
-
AACvB
代表该位置转换前的源码位置,以VLQ
编码表示;
位置对应的原理
位置关系的保存经历了诸多步骤和优化,这个不详细说了,想看的可以看这里,我们只说最后的结果。
在每个位置中:
- 第一位,表示这个位置在【转换后代码】的第几列。
- 第二位,表示这个位置属于【sources 属性】中的哪一个文件。
- 第三位,表示这个位置属于【转换前代码】的第几行。
- 第四位,表示这个位置属于【转换前代码】的第几列。
- 第五位,表示这个位置属于【names 属性】的哪一个变量。
举例
假设现在有a.js
,内容为feel the force
,处理后为b.js
,内容为the force feel
以 the
为例,它在输出中的位置是 (0,0),a.js
是sources
的第 1 个(这里只是举例),输入中的位置是 (0,5),the
是names
的第 2 个(这里只是举例)。
那么映射关系为:
0 1 0 5 2
最后将 01052 表示为 Base64 VLQ 即可。
说明:
- 所有的值都是以 0 作为基数
- 第五位不是必需的,如果该位置没有对应
names
属性中的变量,可以省略第五位 - 每一位都采用
VLQ
编码表示,由于VLQ
编码是可变长的,所以每一位可以由多个字符构成 - 为什么不保存转换后代码的行号,因为我们输出的文件总是一行,这样输出的行号就可以省略,因为都是 0,没必要写出来
- 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述
相对位置是啥呢,看示意图:
第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如 the
的输出位置为 (0,-10), 因为the
在feel
的左边数 10 下才能到这个位置。
VLQ 编码
VLQ
是 Variable-length quantity
的缩写,是一种通用的、使用 任意位数的二进制 来表示一个 任意大的数字 的一种编码方式。这种编码最早用于 MIDI 文件,后来被多种格式采用,它的特点就是可以非常精简地表示很大的数值,用来节省空间。
这种编码需要用最高位表示连续性,如果是 1,代表这组字节后面的一组字节也属于同一个数;如果是 0,表示该数值到这就结束了。
这样干巴巴说不太容易懂,还是举个栗子说明一下吧。
如何对数值 137 进行 VLQ 编码:
步骤 | 结果 |
---|---|
将 137 改写成二进制形式 | 10001001 |
七位一组做分组,不足的补 0 | 0000001 0001001 |
最后一组开头补 0,其余补 1 | 10000001 00001001 |
所以,137 的 VLQ 编码形式为10000001 00001001
Base64 VLQ
与一般的 VLQ
的区别:
- 一个
Base64
字符只能表示6bit(2^6)
的数据 -
Base64 VLQ
需要能够表示负数,于是用最后一位来作为符号标志位。 - 由于只能用 6 位进行存储,而第一位表示是否连续的标志,最后一位表示正数 / 负数。中间只有 4 位,因此一个单元表示的范围为
[-15,15]
,如果超过了就要用连续标识位了。
表示正负的方式:
- 如果这组数是某个数值的
VLQ
编码的第一组字节,那它的最后一位代表 ” 符号 ”,0 为正,1 为负; - 如果不是,这个位没有特殊含义,被算作数值的一部分。
我们再来举个栗子说明下使用方法。
如何对数值 137 进行 Base64 VLQ
编码:
步骤 | 结果 |
---|---|
将 137 改写成二进制形式 | 10001001 |
127 是正数,末位补 0 | 100010010 |
五位一组做分组,不足的补 0 | 01000 10010 |
将组倒序排序 | 10010 01000 |
最后一组开头补 0,其余补 1 | 110010 001000 |
转 64 进制 | y 和 I |
所以 137 通过 Base64 VLQ
表示为yl
可以看出:
- 在
Base64 VLQ
中,编码顺序是从 低位到高位 - 而在
VLQ
中,编码顺序是从 高位到低位
参考文章
- JavaScript Source Map 详解
- BASE64 VLQ 编码规则
- source map 原理分析 &vlq