William's Blog

sourcemap学习笔记

sourcemap产生的背景

  前端代码开发已经实现了模块化和工程化。为了减小静态资源体积,一般会选择将对js和css文件进行压缩形成一个或者多个bundle文件。压缩后的代码几乎不具有可毒性。这样会存在一个问题:当压缩后代码出现错误时,我们如何快速地定位到源码中出错的位置?sourcemap文件就是为了解决这种问题而被设计出来的。sourcemap在编译压缩后的bundle与源代码之间建立了一个映射关系,通过sourcemap可以很快地找到bundle中某一行代码在源代码中对应的位置。

sourcemap格式

  sourcemap文件包含一个JSON对象,典型的数据结构如下:

{
    "version" : 3,
    "file": "bundle.js",
    "sourceRoot": "",
    "sources": ["foo.js", "bar.js"],
    "sourcesContent": [null, null],
    "names": ["src", "maps", "are", "fun"],
    "mappings": "A,AAAB;;ABCDE;"
}

  对上述JSON对象的属性介绍如下:

  • version: sourcemap文件的版本号,取值为正整数。
  • file: sourcemap文件对应的bundle文件参数。
  • sourceRoot: sourcemap文件对应的源代码所在根目录。
  • sources: 源文件列表。
  • sourcesContent: 源文件内容列表,顺序与sources元素顺序对应。
  • names: 源代码中所有变量名和属性名列表。
  • mappings: 存储映射关系(Base 64编码)。

sourcemap如何存储映射关系

  sourcemap的mappings属性的值是将位置映射关系采用Base 64 VLQ编码后生成的字符串。mappings属性值包含的信息可以按照下面的方式进行拆分。
  (1)生成的bundle文件中的每一行依次对应于mappings属性值中的一个组,组与组之间使用英文输入法中的分号分隔。上文所示的sourcemap示例中mappings包含三个组:“A,AAAB”“空”“ABCDE”,分别对应于bundle文件的第1、2、3行;
  (2)mappings中每个组可以使用英文逗号分隔为多个段,如(1)中的第1组分为两个段,对应的字符序列分别为“A”“AAAB”
  (3)每个段又可以看做是由5个可变长度域组成,段与可变长度域的关系可由图1表示。

图 1 段与可变长度域的关系

  域是可变长度的字符串的原因在于域值是Base 64 VLQ编码后生成的字符串(VLQ是变长编码)。域包含的信息如下:
  (1)第一个字段表示的是段对应于bundle文件的第几列(从第0列开始计算);
  (2)第二个字段表示的是段对应的源文件在sources属性对应的源文件列表中的索引;
  (3)第三个字段表示的是段对应于源文件的第几行(从第0行开始计算);
  (4)第四个字段表示的是段对应于源文件的第几列(从第0列开始计算);
  (5)第五个字段表示的是段对应的变量名或属性在names属性对应的变量名或属性列表中的索引。

Base 64 VLQ编码

  VLQ编码是一种采用任意长度的二进制位来表示任意大小的整数的通用编码方式。Base 64编码是将二进制序列分成多个由6个bit组成的数据块(不足补0)并将6bit数据映射成常见字符的编码方式。Base 64 VLQ编码结合了VLQ编码和Base 64编码的特点,它的编码特点如下:
  (1)对任意整数编码后生成的结果是一个由Base 64字母表中的字符组成的任意长度的字符串。
  (2)每一个字符对应6bit的二进制位。最高位取1时,表示该6bit后面的6bit和该6bit表示的是同一个整数,最高位取0时,表示整数的编码到该6bit结束。起始6bit的最低位起到标志整数符号的作用,0表示正数,1表示负数,其余的bit组的最低位当做数据位。

sourcemap mappings示例

  假设某一个sourcemap的内容如下:

{
    "version" : 3,
    "file": "index.min.js",
    "sourceRoot": "",
    "sources": ["page1.js", "page2.js"],
    "names": ["a", "b", "c", "d"],
    "mappings": "ABCDE;"
}

  对上述mappings解读如下:
  (1)index.min.js只有一行,只对应于mappings中的一个组 “ABCDE” 。
  (2)组 “ABCDE” 只包含一个段,“ABCDE”是对段中可变长度域的值进行Base 64 VLQ编码后的结果。
  (3)将 “ABCDE” 逐个进行Base 64解码,可以得到如下二进制序列:000000 000010 000010 011010 000100。
  (4)上述解码后得到的5个6bit二进制序列的最高位都为0,表示每一个整数只对应一个6bit单元。每个6bit单元的最低位都是0,表示每一个整数都是正数。上述二进制序列解码后得到的数字序列为0 1 1 13 2。
  (5)根据段中域的含义,mappings展示出的信息如下:index.min.js的第1行第0列对应于page2.js文件的第1行第13列,这个位置对应的变量名为c

如何关联bundle文件和sourcemap文件

  有两种方法可以关联bundle文件和sourcemap文件:
  (1)在bundle文件的http响应头部添加如下字段:SourceMap: <sourcemap url>
  (2)在bundle文件内部添加一行注释://# sourceMappingURL=<sourcemap url>/*# sourceMappingURL=<sourcemap url> */

参考文献

  1. Source Map Revision 3 Proposal
  2. Introduction to JavaScript Source Maps
  3. JavaScript Source Map 详解
  4. Wikipedia: Variable-length code
  5. Wikipedia: Variable-length quantity

William

本博客作者 William 现任职于北京贝壳找房,从事web前端开发相关工作。
您可以通过Email与他取得联系