Source Map 原理
Source Map 是一种 .map
结尾的文件类型,主要的作用是记录和源码有关的位置信息。JavaScript 从最开始的简单变得越来越复杂,大部分源码都要经过转换才能放到生产环境。一般情况下在 压缩、文件合并 和 语言转义 方面可以看到实际的运行代码不同于源码,这时候如果要调试就会毫无头绪,Source Map 就是来解决这个问题的。
格式
Source Map 的文件大概长这个样子:
整个文件是一个 json,其中:
version: Source Map 的版本,目前为 3,这是 Source Map 3 的提案。
file:转换后的文件名。
sourceRoot:转换前文件所在目录。如果和转换前的文件在同一个目录,该项为空。
sources:转换前的文件。是一个数组,表示可多文件合并。
names:转换前的变量名和属性名。
mappings:记录位置信息的字符串。
mapping
Source Map 的核心是:如何把两个文件内的位置一一对应。mapping 字段就是来解决这个问题的,它是一个很长的字符串,分为三层:
行对应:以分号(;)表示,每个分号对应转换后源码的一行,一个分号前的内容就对应源码的一行。
位置对应:以逗号(,)表示,每个逗号对应转换后源码的一个位置,一个逗号前的内容对应源码的一个位置。
位置转换:以 VLQ 编码 表示,代表该位置对应的转换前的源码位置。
位置对应原理
每个位置占五位,表示五个字段:
第一位:表示这个位置在第几列(转换后的代码)。
第二位:表示这个位置属于
sources
属性中的哪个文件。第三位:表示这个位置属于第几行(转换前代码)。
第四位:表示这个位置属于第几列(转换前代码)。
第五位:表示这个位置属于
names
属性的哪一个变量。
注意,所有的值都是以 0 为基数的。其次,第五位不是必须的,如果没有 names 属性,就可以忽略第五位。每一位都是用 VLQ 编码表示的,由于 VLQ 是可以变长的,所以每一位可以由多个字符构成。
举例,一个位置是 AAAAA 的,在 VLQ 编码中的 A 是 0,所以这个位置的五个位都是 0,代表的意思也就是:该位置在转换后代码的第 0 列,对应 sources 属性中第 0 个文件,属于转换前代码的第 0 列第 0 行,对应 names 属性中的第 0 个变量。
假设有个文件 a.js
有一行代码:Hello World
,最终打包输出的文件为 bundle.js
,内容为: Awesome JavaScript
,映射关系如下:
以 World 为例,它原始的位置为(0,6),输出后是 Awesome,位置为(0,0),那么可以这样来表示一下:
这样就可以写成一种固定的格式,包含了输出前后的位置信息、文件名和具体的单词,此时的映射关系为:
可以再优化一下,把 a.js
和最后面的单词放到数组里,用 sources
来记录所有的原始文件名,names
来记录所有的单词,并用下标表示它们,以 World
为例,就变成了:
很多时候输出的文件其实只有一行,所以可以暂且把输出文件的行号省略掉,就变成了:
再考虑一点,如果文件很大的话,行列的数值就会特别的大,所以可以考虑用 相对位置 代替 绝对位置 来表示,只用绝对位置表示第一个单词的位置,后面都使用相对前一个单词的位置:
现在可以得到一个初步的 map
文件了:
但是这个 mappings 很难看,而且需要使用 “|” 来分隔, 此时就需要用 VLQ 编码来解决分隔的问题,它的理念是在连续的数字上做标记。
VLQ 编码
VLQ(Variable-length quantity)是一种通用的,使用任意位数的二进制来表示任意一个大的数字的编码方式,最开始用于 MIDI 文件,后来被多种格式采用。用来节省空间。上面的例子用 “|” 做标记,是为了用一个字符串存储多个数字(像 1 | 23 | 456 | 7 这样),但是每个 “|” 都要占一个字符,下面看下 VLQ 如何对连续的数字做标记:
可以发现,标记只在数字不是结尾的部分才有,也就是说,没有标记意味着数字的结束。它的具体实现是这样的,VLQ 利用 6 位进行存储,其中:
第一位,表示是否连续
最后一位,表示正数/负数
中间四位,范围为[-15, 15],因为二进制的四位数全都是 1 的十进制就是 15,超过了就要用连续标识位了。
来看几个用 VLQ 表示数字的例子:
从上图可以看出:
如果数字在 [-15, 15] 内,一个单元就可以,例如 7,只需要把 7 的二进制放到中间四位就好。
如果超过 [-15, 15],就要用多个单元表示,需要对数字按照 **“..5554” **的规则划分,把最右边的 4 位 放进第一个单元格中,然后每 5 个放入右边新的单元格,而第一个单元只放 4 个是因为它的最后一位表示正负标识,其他单元的最后一位就没必要表示正负了。
如果是负数,求它的正数的二进制,继续按照之前的规则放,只把第一个单元格的最后一位改为 1 即可。
最后把划分好的 6 位变成 Base64 编码,因为 Base64 也是 6 位一单元(下图就是 Base64 编码字符表)。
了解了 VLQ 编码 的实现,现在就可以对 “Hello World” 的例子进行编码了,上面知道 mappings 为:
[ 8 | 0 | 0 | 0 | 0, -8 | 0 | 0 | 6 | 0 ]
按照上面 VLQ 规则编码后的每一部分为:
[ 010000 | 000000 | 000000 | 000000 | 000000,010001 | 000000 | 000000 | 001100 | 000000 ]
每一部分按照二进制转十进制,得出结果为:
[ 16 | 0 | 0 | 0 | 0, 17 | 0 | 0 | 12 | 0 ]
最后查 Base64 表,得出 VLQ 编码为:
[ QAAAA, RAAMA ]
可以在 这个网站 验证一下结果:
🎉 这样就得到了一个 mapping 映射关系 !
项目中的 Source Map
在 Webpack 中,使用了一个 source-map 包来实现该功能,大概的意思就是通过自定义的转换关系对 sourcemap 部分的内容进行生产和消费,而 Webpack 也是在其基础上添加了一点的属性和封装。一般情况下要想在项目里的 a.js
生成一个对应的 a.js.map
,就需要在 a.js
的最后添加一句:
像 jquery 的 在线cdn,也在文件结尾加了下面这句注释,而它的在线 map 文件是 这样 的:
Webpack5 里的 Source Map 正则判定规则是:^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
可以看出来是各种前缀拼接 source-map
字符串来定义是哪种 sourcemap 模式:
eval
eval 的 api 是动态执行的,浏览器针对 eval 有个特殊的处理:
加了两句特殊的语句,在浏览器的源代码里也可以找到这个文件:
注意,这里的文件是可以打断点的,也就是说 Webpack 利用了 eval 的该特性来优化 sourcemap 的生成,所以在配置的时候加上 devtool
:
一般情况下生成的 sourcemap 很大,也很耗时,所以就出了上面说的各种 sourcemap 模式的 api。
cheap
sourcemap 速度慢是因为映射慢,映射量大,很多时候的调试不需要精确到行+列,只需要精确到行就可以了,这时候就用 cheap 的 sourcemap。
module
在有多个 loader
的情况下,每次转换都会有 sourcemap,默认能拿到的 sourcemap 的最终结果只关联了最后一个 loader,这个时候要想调试最初的源码就需要把每次的 loader
的 sourcemap 关联起来,module
就是配置它的。
nosources
sourcemap 是有 sourceContent 内容的,也就是源码本身的复制份,如果不想生成 sourceContent,就使用 nosources-source-map
模式的 devtool
。
相关资料
原文转载个人博客
版权声明: 本文为 InfoQ 作者【道道里】的原创文章。
原文链接:【http://xie.infoq.cn/article/073bbbbbabf80451b95858782】。文章转载请联系作者。
评论