深刻理解SourceMap

一 Source Map是什么?

Source Map,顾名思义,是保存源代码映射关系的文件,相信用过webpack的开发者对它应该不会陌生。在项目开发完进行打包后,在打包的文件夹里一般除了js,css,图片字体等资源文件外,你们必定还见过xxx.js.map的文件。这种带map后缀的文件就是Source Map文件——它保存了源代码和转换以后代码(一般通过压缩混淆和其余转换)的关系。 下图展现了部分打包以后生成的Source Map文件:css

WX20190614-151320.png

下面是一个典型的Source Map文件的格式:前端

{
  "version": 3,
  "sources": [
    "log.js",
    "main.js"
  ],
  "names": [
    "sayHello",
    "name",
    "length",
    "substr",
    "console",
    "log"
  ],
  "mappings": "AAAA,SAASA,SAAUC,MACjB,GAAIA,KAAKC,OAAS,EAAG,CACnBD,KAAOA,KAAKE,OAAO,EAAG,GAAK,MAE7BC,QAAQC,IAAI,QAASJ,MCJvBD,SAAS"
}
复制代码

二 为何使用Source Map?

明白了什么是Source Map以后,你们确定有个疑问,咱们为何须要Source Map? 因为现代前端项目的发展,前端代码变得愈来愈庞大和复杂。大部分源码都须要通过转换,才能投入到生产环境中使用。 常见的转换过程包括但不限于:webpack

  • 压缩混淆(UglifyJS)
  • 编译(TypeScript, CoffeeScript)
  • 转译(Babel)
  • 合并多个文件,减小带宽请求。

通过上述转换过程的代码,每每都会变得面目全非,就像下面这样:web

WX20190614-154341.png
这样虽然对带宽很友好,可是调试起来就不是那么轻松了。咱们在代码出错的时候,确定最但愿能定位其在源码中的位置。 好比下面这两个错误提示:
WX20190614-160302.png
WX20190614-160413.png
对于大多数开发者来讲,但愿看到的应该是第二种提示方式,而这就是Source Map可以输出的能力。

三 Source Map是怎么实现映射的?

在探索这个问题以前,能够先想一想真实世界里对这种语言转换是怎么作的?数组

I AM CHRIS ——> Map ——> CHRIS I AM
复制代码

如今咱们要从CHRIS I AM还原到I AM CHRIS,Map里应该存储哪些信息呢?bash

3.1 最简单粗暴的方法

将输出文件中每一个字符位置对应在输入文件名中的原位置保存起来,并一一进行映射。上面的这个映射关系应该获得下面的表格:app

字符 输出位置 在输入中的位置 输入的文件名
c 行1,列1 行1,列6 输入文件1.txt
h 行1,列2 行1,列7 输入文件1.txt
r 行1,列3 行1,列8 输入文件1.txt
i 行1,列4 行1,列9 输入文件1.txt
s 行1,列5 行1,列10 输入文件1.txt
i 行1,列7 行1,列1 输入文件1.txt
a 行1,列9 行1,列3 输入文件1.txt
m 行1,列10 行1,列4 输入文件1.txt

备注: 因为输入信息可能来自多个文件,因此这里也同时记录输入文件的信息。函数

将上面表格整理成映射表的话,看起来就像这样(使用"|"符号分割字符)字体

mappings: "1|1|输入文件1.txt|1|6,1|2输入文件1.txt|1|7,1|3|输入文件1.txt|1|8,1|4|输入文件1.txt|1|9,1|5|输入文件1.txt|1|10,1|7|输入文件1.txt|1|1,1|9|输入文件1.txt|1|3,1|10|输入文件1.txt|1|4"(长度:144复制代码

这种方法确实能将处理后的内容还原成处理前的内容,可是随着内容的增长,转换规则的复杂,这个编码表的记录将飞速增加。目前仅仅10个字符,映射表的长度已经达到了144个字符。如何进一步优化这个映射表呢?优化

备注: mappings: "输出文件行位置|输出文件列位置|输入文件名|输入文件行号|输入文件列号,....."

3.2 优化手段1:不要输出文件中的行号

在经历过压缩和混淆以后,代码基本上不会有多少行(特别是JS,一般只有1到2行)。这样的话,就能够在上节的基础上移除输出位置的行数,使用";"号来标识新行。 那么映射信息就变成了下面这样

mappings: "1|输入文件1.txt|1|6,2|输入文件1.txt|1|7,3|输入文件1.txt|1|8,4|输入文件1.txt|1|9,5|输入文件1.txt|1|10,7|输入文件1.txt|1|1,9|输入文件1.txt|1|3,10|输入文件1.txt|1|4; 若是有第二行的话"(长度:129复制代码

备注: mappings: "输出文件列位置|输入文件名|输入文件行号|输入文件列号,....."

3.3 优化手段2:提取输入文件名

因为可能存在多个输入文件,且描述输入文件的信息比较长,因此能够将输入文件的信息存储到一个数组里,记录文件信息时,只记录它在数组里的索引值就行了。 通过这步操做后,映射信息以下所示:

sources: ['输入文件1.txt'],
mappings: "1|0|1|6,2|0|1|7,3|0|1|8,4|0|1|9,5|0|1|10,7|0|1|1,9|0|1|3,10|0|1|4;" (长度:65)
复制代码

通过转换后mappings字符数从129降低到了65。

备注: mappings: "输出文件列位置|输入文件名索引|输入文件行号|输入文件列号,....."

3.4 优化手段3: 可符号化字符的提取

通过上一步的优化,mappings字符数有了很大的降低,可见提取信息是一个颇有用的简化手段,那么还有什么信息是可以提取的么? 固然。已输出文件中的Chris字符为例,当咱们找到了它的首字符C在源文件中的位置(行1,列6)时,就不须要再去找剩下的hris的位置了,由于Chris能够做为一个总体来看待。想一想源码里的变量名,函数名,都是做为一个总体存在的。 如今能够把做为总体的字符提取并存储到一个数组里,而后和文件名同样,在mapping里只记录它们的索引值。这样就避免了每个字符都要记的窘境,大大缩减mappings的长度。

添加一个包含全部可符号化字符的数组:

names: ['I', 'am', 'Chris']
复制代码

那么以前Chris的映射就从

1|0|1|6,2|0|1|7,3|0|1|8,4|0|1|9,5|0|1|10
复制代码

变成了

1|0|1|6|2
复制代码

最终的映射信息变成了:

sources: ['输入文件1.txt'],
names: ['I', 'am', 'Chris'],
mappings: "1|0|1|6|2,7|0|1|1|0,9|0|1|3|1" (长度: 29)
复制代码

备注: 1. “I am Chris"中的"I"抽出来放在数组里,其实意义不大,由于它自己也就只有一个字符。可是为了演示方便,因此拆出来放在数组里。 2. mappings: "输出文件列位置|输入文件名索引|输入文件行号|输入文件列号|字符索引,....."

3.5 优化手段4: 记录相对位置

前面记录位置信息(主要是列)时,记录的都是绝对位置信息,设想一下,当文件内容比较多时,这些数字可能会变的很大,这个问题怎么解决呢? 能够经过只记录相对位置来解决这个问题(除了第一个字符)。 来看一下具体怎么实现的,以以前的mappings编码为例:

mappings: "1(输出列的绝对位置)|0|1|6(输入列的绝对位置)|2,7(输出列的绝对位置)|0|1|1(输入列的绝对位置)|0,9(输出列的绝对位置)|0|1|3(输入列的绝对位置)|1"
复制代码

转换成只记录相对位置

mappings: "1(输出列的绝对位置)|0|1|6(输入列的绝对位置)|2,6(输出列的相对位置)|0|1|-3(输入列的相对位置)|0,2(输出列的相对位置)|0|1|-2(输入列的绝对位置)|1"
复制代码

从上面的例子可能看不太出这个方法的好处,可是当文件慢慢大起来,使用相对位置能够节省不少字符长度,特别是对于记录输出文件列信息的字符来讲。

3.6 优化手段5: VLQ编码

通过上面几步操做以后,如今最应该优化的地方应该就是用来分割数字的"|"号了。 这个优化应该怎么实现呢? 在回答以前,先来看这样一个问题——若是你想顺序的记录4组数字,最简单的就是用"|"号进行分割。

1|2|3|4
复制代码

若是每一个数字只有1位的话,能够直接表示成

1234
复制代码

可是不少时候每一个数字不止有1位,好比

12|3|456|7
复制代码

这个时候,就必定得用符号把各个数字分割开,像咱们上面例子中同样。还有好的方法嘛? 经过VLQ编码的方式,你能够很好的处理这种状况,先来看看VLQ的定义:

3.6.1 VLQ定义

A variable-length quantity (VLQ) is a universal code that uses an arbitrary number of binary octets (eight-bit bytes) to represent an arbitrarily large integer. 翻译一下:VLQ是用任意个2进制字节组去表示一个任意数字的编码形式。

VLQ的编码形式不少,这篇文章中要说明的是下面这种:

WX20190615-165403.png

  • 一个组包含6个二进制位。
  • 在每组中的第一位C用来标识其后面是否会跟着另外一个VLQ字节组,值为0表示其是最后一个VLQ字节组,值为1表示后面还跟着另外一个VLQ字节组。
  • 在第一组中,最后1位用来表示符号,值为0则表示正数,为1表示负数。其余组的最后一位都是表示数字。
  • 其余组都是表示数字。

这种编码方式也称为Base64 VLQ编码,由于每个组对应一个Base64编码。

3.6.2 小例子说明VLQ

如今咱们用这套VLQ编码规则对12|3|456|7进行编码,先将这些数字转换为二进制数。

12  ——> 1100
3   ——> 11
456 ——> 111001000
7   ——> 111
复制代码
  • 对12进行编码

12须要1位表示符号,1位表示是否延续,剩下的4位表示数字

B5(C) B4 B3 B2 B1 B0
0 1 1 0 0 0
  • 对3进行编码
B5(C) B4 B3 B2 B1 B0
0 0 0 1 1 0
  • 对456进行编码

从转换关系中可以看到,456对应的二进制已经超过了6位,用1组来表示确定是不行的,这里须要用两组字节组来表示。先拆除最后4个数(1000)放入第一个字节组,剩下的放在跟随字节组中。

B5(C) B4 B3 B2 B1 B0 B5(c) B4 B3 B2 B1 B0
1 1 0 0 0 0 0 1 1 1 0 0
  • 对7进行编码
B5(C) B4 B3 B2 B1 B0
0 0 1 1 1 0

最后获得下列VLQ编码:

011000 000110 110000 011100 001110
复制代码

经过Base64进行转换以后:

bg2013012202 (1).png
最终获得下列结果:

YGwcO
复制代码

3.6.3 转换以前的例子

经过上面这套VLQ的转换流程转换以前的例子,先来编码1|0|1|6|2. 转换成VLQ为:

1 ——> 1(二进制) ——> 000010(VLQ)
0 ——> 0(二进制) ——> 000000(VLQ)
1 ——> 1(二进制) ——> 000010(VLQ)
6 ——> 110(二进制) ——> 001100(VLQ)
2 ——> 10(二进制) ——> 000100(VLQ)
复制代码

合并后编码为

000010 000000 000010 001100 000100
复制代码

转换成Base64

BABME
复制代码

其余也是按这种方式编码,最后获得的mapping文件以下:

sources: ['输入文件1.txt'],
names: ['I', 'am', 'Chris'],
mappings: "BABME,OABBA,SABGB" (长度: 17)
复制代码

和第一节的mappings文件对比同样,是否是同样呢?

3.6.4 one more thing

在真实场景中,咱们在mappings中常常能够看到不是5位的字符。大于5位好理解,可能表示的数字太大。好比123456789转换成Base64 VLQ编码就是qxmvrH。而少于5位的状况在于mappings的编码片断中可能不须要那么多信息就能进行映射,好比不须要Names属性,这样只经过4位信息也就能获取到映射关系了。一个编码片断可能会有如下几种长度:

  • 5 - 包含所有五个部分:输出文件中的列号,输入文件索引,输入文件中的行号,输入文件中的列号,符号索引
  • 4 - 输出文件中的列号,输入文件索引,输入文件中的行号,输入文件中的列号
  • 1 - 输出文件中的列号

经过上面的讲解,相信你们必定对Source Map是如何映射源码转换后代码之间的位置关系有所了解。在了解了Source Map原理以后,往后再去使用它确定可以驾轻就熟。

相关文章
相关标签/搜索