初识 Source Map

一、背景


出于优化以及安全性,大部分源码都需要经过转换,才会最后在生产环境使用

  1. 压缩混淆代码,减小体积
  2. 文件合并
  3. 其他语言通过机器编译成 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:

  1. 生产一个source map
  2. 加入一个注释在转换后的文件,它指向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个字符,这肯定是需要优化的!

我们采取下面的方式进行精简优化:

  1. 省去输出文件中的行号,改用 ‘;’ 来标识换行
    因为最后编译后的代码往往会压缩到一行,绝大部分情况都可以省去行数。如果有多行的话,用‘;’分割也很方便
  2. 用索引标识变量名
  3. 用索引来代替文件名
  4. 用相对位置来代替绝对位置
// 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 编码:

  1. 16 转为二进制形式10000
  2. 在最右边补充符号位:100000
  3. 从右边的最低位开始,将整个数每隔5位,进行分段,即变成1和00000两段。如果最高位所在的段不足5位,则前面补0,因此两段变成00001和00000。
  4. 将两段的顺序倒过来,即00000和00001
  5. 在每一段的最前面添加一个”连续位”,除了最后一段为0,其他都为1,即变成100000和000001
  6. 将每一段转成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 VLQ
0|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?