初识 Source Map
一、背景
出于优化以及安全性,大部分源码都需要经过转换,才会最后在生产环境使用
- 压缩混淆代码,减小体积
- 文件合并
- 其他语言通过机器编译成 Javascript
代码经过各种转换后,与开发代码产生了差异,对于线上代码的查错会显得无可奈何无从下手。因此我们需要有一个桥梁搭建起源代码及压缩后代码的联系。
二、source map 是什么
source map 是从已转换的代码映射到原始源的文件,里面存储着位置信息,能使转换后的每一个位置都能对应转换前的位置,使浏览器能够重构原始源并在调试器中显示重建的原始源。
下面是 source map 的格式:
{
version: 3,
file: "min.js",
names: ["bar", "baz", "n"],
sources: ["one.js", "two.js"],
sourceRoot: "http://example.com/www/js/",
mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA"
}
- version:版本号,目前 source map 标准的版本都是 3
- file:生成 source map 的文件,也就是经过一系列转化最后打包成的文件
- names:转换前的所有变量名和属性名
- sources:源文件
- sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
- mappings:记录位置信息的字符串,表示了源代码及编译后代码的关系(采用 Base64 VLQ)
1、如何使用 source map:
- 生产一个source map
- 加入一个注释在转换后的文件,它指向source map。注释的语法类似:
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map
这里的 sourceMappingURL 可以是本地文件系统,也可以是一个链接。
2、浏览器加载
source map 只有在打开 dev tools 的情况下才会开始下载。但是在浏览器的network里是看不到 sourcemap 的,因为浏览器隐藏了。但是如果使用其他的代理工具,比如 charles 的话,是能看到的。
浏览器 networks:
charles:
三、上手尝试
先让我们自己动手尝试一下,如果是我们来记录原代码和编译后代码对应的位置关系,会怎么来记录。
原代码:
// index.js
const sourceMapDemo = (content) => {
console.log(content);
}
sourceMapDemo('sourcemap');
编译后的代码:
console.log("sourcemap");
//# sourceMappingURL=bundle.js.map
编译后的位置(行/列) | 编译后单词 | 原始文件 | 原始位置(行/列) | 原始单词 |
---|---|---|---|---|
0,0 | console | index.js | 1,2 | console |
0,8 | log | index.js | 1,10 | log |
0,12 | “sourcemap” | index.js | 4,14 | ‘sourcemap’ |
将以上表格组合成一串字符串信息,如下所示:0|0|index.js|1|2|console, 0|8|index.js|1|10|log, 0|12|index.js|4|14|'sourcemap'
编译后的代码一共25个字符, 转换成位置信息却有79个字符,这肯定是需要优化的!
我们采取下面的方式进行精简优化:
- 省去输出文件中的行号,改用 ‘;’ 来标识换行
因为最后编译后的代码往往会压缩到一行,绝大部分情况都可以省去行数。如果有多行的话,用‘;’分割也很方便 - 用索引标识变量名
- 用索引来代替文件名
- 用相对位置来代替绝对位置
// sources
[
"./src/index.js"
]
// names
[
"console",
"log"
]
优化完:0|0|1|2|0, 8|0|0|8|1, 4|0|3|4
四、Base64 VLQ
1、base64
从二进制到字符的过程:6bit 为一个单元
2、VLQ
看一下这两组数字:
1|2|3|4|5|6|7
1|23|456|7
如果是第一组数的话,因为我们知道每一个数就是一位,所以为了省去空间,可以把‘|’ 给省去,变成1234567。
但是第二组数,我们怎么知道第二组数字是23,第三组数字是456呢?
这里就引入了 VLQ 这个概念。
Variable-length quantity——可变长度的编码:精简地表示很大的数值
维基百科上的定义是:
A VLQ is essentially a base-128 representation of an unsigned integer with the addition of the eighth bit to mark continuation of bytes.
按照一个字节有8位计算,第1位是连续性标志位,后7位是放值的地方,这样就可以扩展一个可变长度的字符串。
转化过程:
将二进制按照7位一分组,分别放入字节中,如果放不满前面用0补足。除了最后一组数字之外,前面的每一组的连续性标志位都置为1,最后一组置为0。
127:01111111 => 01111111
128:10000000 => 10000001 00000000
3、Base64 VLQ
将 Base64 和 VLQ 结合在一起就变成了Base64 VLQ。
使用6个比特来记录一个数字(可表示至多64个值),用其中1位来标识它是否未结束(C),不需要引入额外的符号,再用一位标识正负(S),剩下还有四位用来表示数值。
第一个字节组:
B5 | B4 | B3 | B2 | B1 | B0 |
---|---|---|---|---|---|
C | Value | Value | Value | Value | S |
后续字节组:
任意数字中,第一个字节组中已经标明了该数字的正负,所以后续的字节组中无需再标识,于是可以多出一位来作表示值。
B5 | B4 | B3 | B2 | B1 | B0 |
---|---|---|---|---|---|
C | Value | Value | Value | Value | Value |
最后再将每一个字节组转为 Base 64 编码。
例子1:
对于16如何进行 Base 64 VLQ 编码:
- 16 转为二进制形式10000
- 在最右边补充符号位:100000
- 从右边的最低位开始,将整个数每隔5位,进行分段,即变成1和00000两段。如果最高位所在的段不足5位,则前面补0,因此两段变成00001和00000。
- 将两段的顺序倒过来,即00000和00001
- 在每一段的最前面添加一个”连续位”,除了最后一段为0,其他都为1,即变成100000和000001
- 将每一段转成Base 64编码:gB
例子2:
1|23|456|7
数值 | 二进制 |
---|---|
1 | 1 |
23 | 10111 |
456 | 111001000 |
7 | 111 |
最终得到:000010 101110 000001 110000 011100 00111
转为Base 64编码:CuBwcO
五、mappings
细分层级:
- 第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。
- 第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。
- 第三层是位置转换,代表该位置对应的转换前的源码位置。这里由1,4或5个可变长度字符组成(压缩数字内容的编码方式)
位置转换对应:
- 第一位,表示这个位置在(转换后的代码的)的第几列
- 第二位,表示这个位置属于sources属性中的哪一个文件
- 第三位,表示这个位置属于转换前代码的第几行
- 第四位,表示这个位置属于转换前代码的第几列
- 第五位,表示这个位置属于names属性中的哪一个变量
最后将最一开始的例子转为Base64 VLQ0|0|1|2|0, 8|0|0|8|1, 4|0|3|4 => AACEA,QAAQC,IAGI
webpack 的 source map:
{
"version": 3,
"sources": [
"webpack:///./src/index.js"
],
"names": [
"console",
"log"
],
"mappings": "AACEA,QAAQC,IAGI",
"file": "bundle.js",
"sourcesContent": [
"const sourceMapDemo = (content) => {\n console.log(content);\n}\n\nsourceMapDemo('sourcemap');\n"
],
"sourceRoot": ""
}
自己转换的 mappings 和 webpack 打出来的 source map是一致的。
具体的 Base 64 VLQ 转译可以见 Base 64 VLQ 和数组标识的映射关系
六、参考文档
JavaScript Source Map 详解
note/VLQ编码.md at master · D-kylin/note · GitHub
何为SourceMap?