欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript Source Map

程序员文章站 2024-02-08 21:52:52
...

问题:线上代码要合并、压缩来减少http请求数和减小体积,并且压缩后的代码还进行了混淆,那么JavaScript的解释器告诉的:第几行第几列代码出错,这样的报错信息对于合并、压缩、混淆后的代码来说,根本不知道它所对应的原始位置。

解决:Source map,有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。

什么事Source map?

  • 简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
  • 是一个独立的map文件,与源码在同一个目录下

JavaScript Source Map

引用Source map:在转换后的代码尾部,加上如下语句

//# sourceMappingURL=vendor_d0bc9c9d6e6b4ba664cb.js.map

Source map的格式

map文件就是一个一个JavaScript对象,可以被解释器读取。

- version:Source map的版本,目前为3。
- file:转换后的文件名。输出文件
- sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
- sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。
- names:转换前的所有变量名和属性名。
- mappings:记录位置信息的字符串,下文详细介绍。

 如下:

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
  }
  
{"version":3,"file":"vendor_d0bc9c9d6e6b4ba664cb.js","sources":[],"mappings":";A","sourceRoot":""} 

 两个文件的各个位置是如何一一对应的?关键就是map文件的mappings属性。

属性mappings

分成三层:

  • 第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
  • 第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
  • 第三层是位置转换,以VLQ编码表示,代表该位置对应的转换前的源码位置。

如:mappings:"AAAAA,BBBBB;CCCCC"=》表示:转换后的源码分成两行,第一行有两个位置,第二行有一个位置。

位置对应的原理:

每个位置使用五位,表示五个字段。从左边算起如下:

- 第一位,表示这个位置在(转换后的代码的)的第几列。
- 第二位,表示这个位置属于sources属性中的哪一个文件。
- 第三位,表示这个位置属于转换前代码的第几行。
- 第四位,表示这个位置属于转换前代码的第几列。
- 第五位,表示这个位置属于names属性中的哪一个变量。

 如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。

注意:

  • 所有的值都是以0作为基数的。
  • 第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位。
  • 每一位都采用VLQ编码表示;由于VLQ编码是变长的,所以每一位可以由多个字符构成。
  • 为什么不保存转换后代码的行号,因为输出的文件总是一行,这样输出的行号就可以省略,因为都是0,没必要写出来
  • 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述

JavaScript Source Map

如上:第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the的输出位置为(0,-10),因为the在feel的左边数10下才能到这个位置。

举例

假设现在有a.js,内容为feel the force,处理后为b.js,内容为the force feel

JavaScript Source Map

以the为例,它在输出中的位置是(0,0),a.js是sources的第1个(这里只是举例),输入中的位置是(0,5),the是names的第2个(这里只是举例)。

那么映射关系为: 0 1 0 5 2

最后将 01052 表示为 Base64 VLQ 即可。

VLQ编码

VLQ是Variable-length quantity 的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式。VLQ的特点就是可以非常精简地表示很大的数值,用来节省空间。

如何用VLQ编码表示数值。

VLQ编码是变长的。如果(整)数值在-15到+15之间(含两个端点),用一个字符表示;超出这个范围,就需要用多个字符表示。它规定,每个字符使用6个两进制位,正好可以借用Base 64编码的字符表。

JavaScript Source Map

 

在这6个位中,左边的第一位(最高位)表示是否"连续"(continuation)。如果是1,代表这6个位后面的6个位也属于同一个数;如果是0,表示该数值到这6个位结束。

  Continuation
  |     Sign
  |     |
  V     V
  101011

 这6个位中的右边最后一位(最低位)的含义,取决于这6个位是否是某个数值的VLQ编码的第一个字符。如果是的,这个位代表"符号"(sign),0为正,1为负(Source map的符号固定为0);如果不是,这个位没有特殊含义,被算作数值的一部分。

VLQ编码:实例

VLQ编码:需要用最高位表示连续性,如果是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为负;
  • 如果不是,这个位没有特殊含义,被算作数值的一部分。

1)如何对数值16进行Base64 VLQ编码。

第一步,将16改写成二进制形式10000。
第二步,在最右边补充符号位。因为16大于0,所以符号位为0,整个数变成100000。
第三步,从右边的最低位开始,将整个数每隔5位,进行分段,即变成1和00000两段。如果最高位所在的段不足5位,则前面补0,因此两段变成00001和00000。
第四步,将两段的顺序倒过来,即00000和00001。
第五步,在每一段的最前面添加一个"连续位",除了最后一段为0,其他都为1,即变成100000和000001。
第六步,将每一段转成Base 64编码。

 查表可知,100000为g,000001为B。因此,数值16的VLQ编码为gB。上面的过程,看上去好像很复杂,做起来其实很简单,具体的实现请看官方的base64-vlq.js文件,里面有详细的注释。

2)如何对数值137进行Base64 VLQ编码:

步骤

结果

将137改写成二进制形式

10001001

127是正数,末位补0

100010010

五位一组做分组,不足的补0

01000 10010

将组倒序排序

10010 01000

最后一组开头补0,其余补1

110010 001000

转64进制

y和I

所以 137 通过Base64 VLQ表示为yl