美文网首页
【转】深入SourceMap原理

【转】深入SourceMap原理

作者: TingsLee | 来源:发表于2019-11-13 11:09 被阅读0次

    原文链接:深入SourceMap原理

    一 Source Map是什么?

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

    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?
    由于现代前端项目的发展,前端代码变得越来越庞大和复杂。大部分源码都需要经过转换,才能投入到生产环境中使用。
    常见的转换过程包括但不限于:

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

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

    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里应该存储哪些信息呢?

    3.1 最简单粗暴的方法

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

    字符 输出位置 在输入中的位置 输入的文件名
    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|0
    
    

    最终的映射信息变成了:

    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© B4 B3 B2 B1 B0
    0 1 1 0 0 0
    • 对3进行编码
    B5© B4 B3 B2 B1 B0
    0 0 0 1 1 0
    • 对456进行编码

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

    B5© B4 B3 B2 B1 B0 B5© B4 B3 B2 B1 B0
    1 1 0 0 0 0 0 1 1 1 0 0
    • 对7进行编码
    B5© 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原理之后,日后再去使用它肯定能够得心应手。

    相关文章

      网友评论

          本文标题:【转】深入SourceMap原理

          本文链接:https://www.haomeiwen.com/subject/cylhictx.html