sourcemap知多少

想必你们都对这一段截图耳熟能详: html

image

由于检测并加载了sourcemap,因此能够直接定位到编译前代码的特定位置。因此用一句话解释sourcemap很简单,就是一段维护了先后代码映射关系的json描述文件。但若是细究其内容,能够泡杯茶说一段故事了~html5

历史渊源

在2009年google的一篇 文章 中,在介绍Cloure Compiler(一款js压缩优化工具,可类比于uglify-js)时,google也顺便推出了一款调试工具:firefox插件Closure Inspector,以方便调试编译后代码。这就是sourcemap的最初代啦!webpack

You can use the compiler with Closure Inspector , a Firebug extension that makes debugging the obfuscated code almost as easy as debugging the human-readable source.git

2010年,在第二代即 Closure Compiler Source Map 2.0 中,sourcemap肯定了统一的json格式及其他规范,已几乎具备如今的雏形。最大的差别在于mapping算法,也是sourcemap的关键所在。第二代中的mapping已决定使用base 64编码,可是算法同如今有出入,因此生成的.map相比如今要大不少。es6

2011年,第三代即 Source Map Revision 3.0 出炉了,这也是咱们如今使用的sourcemap版本。从文档的命名看来,此时的sourcemap已脱离Clousre Compiler,演变成了一款独立工具,也获得了浏览器的支持。这一版相较于二代最大的改变是mapping算法的压缩换代,使用VLQ编码生成base64前的mapping,大大缩小了.map文件的体积。github

Sourcemap发展史的有趣之处在于,它做为一款辅助工具被开发出来。最后它辅助的对象日渐式微,而它却成为了技术主体,被写进了浏览器中。web

.map文件

{
    "version": 3,
    "sources": ["test.es6.js"],
    "names": [],
    "mappings": ";;AAAA,IAAM,MAAM,GAAG,SAAT,MAAM,CAAI,CAAC;SAAK,CAAC,GAAG,CAAC;CAAA,CAAC",
    "file": "test.js",
    "sourcesContent": ["const square = (x) => x * x;"]
}
复制代码

这就是一段简单的map文件,其中算法

  • version:sourcemap版本(如今都是v3)
  • sources:源文件列表(若是是打包成bundle.js的,那源文件就有很长一堆了)
  • sourcesContent: 原文件内容(若是是webpack压缩的,能够看到这里显示的代码是经webpack链接后的__webpack_require__那一层)
  • names: 原变量名与属性名(压缩时可能会改变变量名称)
  • mapping:映射json
  • file:编译后文件

接下来,就来详细瞅瞅主要元素mapping把!json

mapping策略全攻略

html5rocks的sourcemap教程 (这也是最经典的一篇sourcemap博文了)所说,数组

sourcemap v1最开始生成的sourcemap文件大概有转换后文件的10倍大。sourcemap v2将之减小了50%,v3又在v2的基础上减小了50%。因此目前133k的文件对应的sourcemap文件大小大概在300k左右。

那么它是如何用最精简的方式维护现文件到源文件的映射的呢?对于整个映射而言,重要的几要素无非是“原文件、原行数、原列数、原变量名”,因此对映射算法的优化,实质也就是对这一段数据的存储优化。

我发现了一篇很是好的文章How do source maps work,做者经过简单的示例解释了map的大体工做原理。接下来我会介(梗)绍(概)下做者的几个要点。

sourcemap示例分析

原代码:

const square = (x) => x * x;
复制代码

Babel编译后代码:

"use strict";

var square = function square(x) {
  return x * x;
};

//# sourceMappingURL=test.js.map
复制代码

Mappings(接下来的分析都会以这份mapping为准喔):

";;AAAA,IAAM,MAAM,GAAG,SAAT,MAAM,CAAI,CAAC;SAAK,CAAC,GAAG,CAAC;CAAA,CAAC"
复制代码

如上,能够看到这段mappings有几个关键元素:分号; 逗号, 及分隔出的一串一眼看不懂的字母。其中:

  • 分号:表明分隔行
  • 逗号:表明分隔列
  • 上文四字字母:通过base64转换事后的VLQ编码,表明了每一个segment的原位置定位,好比var, square, =…

附:【base64 VLQ编码 - 十进制转换表 】(先仅供参考,具体转换规则见下文VLQ编码部分)

image

一段segment中的VLQ编码,其实既能够是4位,也能够是5位。每一位都有特定含义:

  • 第一位[0]:表明在转换后文件的第几列(相对于前一个position的相对位置)
  • 第二位[1]:表明源文件在sources里对应的文件序列// 上述mapping的第二位都是A,由于原文件都是第0位
  • 第三位[2]: 表明在源文件的第几行 // 上述mapping的第三位都是A,由于源文件都在也只有第1行
  • 第四位[3]:表明在转换后文件的第几列(相对于前一个position的相对位置)
  • 第五位[4]:表明属于names里的哪个变量

带着解释,再看如下两个问题

上述mapping为何以;;开头呢?

上述mapping以;;开头,是由于babel编译后新增的use strict及下面空行没有队对应的原文~

上述mapping的1、二个segement [AAAA, IAAM] 是怎么map到原文件位置的?

[AAAA,IAAM] 转换为VLQ编码是[0000, 4006]。第二个segment里的4表明square在转换后文件的第4列,而6表明square在现文件的第6列。

0|1|2|3|4|  
v|a|r| |s|q|u|a|r|e|  
0|1|2|3|4|5|6|  
c|o|n|s|t| |s|q|u|a|r|e|
复制代码

以上,相信再看见一段mappings时,不说能立马知道它表明了哪一段源文件,但至少不会两眼一抹黑了~

其实从以上过程读来,在知道了规则的状况下,反编译一段编码老是容易的。难的实际上是创造和优化规则的过程。好比将源文件名放在一个数组中,经过存储原文件在数组中的index,而不是源文件name自己;好比存储的column index是相对位置而不是绝对位置;好比经过;分隔行经过,分隔列,而不是记录行号和列号;好比为何选择了经base64转换的VLQ编码…正是这些微小的一点点比对出来的细节,才能积少成多,将mapping文件最大化的压缩。

VLQ(Variable Length Quantities)编码

要了解VLQ编码,原本我是拒绝的…我也许永远也不会用到…可是一想来都来了,不如也顺便了解一下叭!就当作陶冶情操呢~

So..了解完成后,感受文本编码和网络技术一般会讲到传递xx包要怎么封装一串数据同样,重点都是差很少的。一是如何分割数字(即表示连续),好比1和17在一块儿组成117的时候,怎么来表示这是1和17而不是一百一十七。二是如何表示正负。

一个VLQ小节有6位(因此恰好适合经过base64转换成字符呀),第一位表明连续,最后一位通常表明正负,因此中间至少有4位能够表示value。

image

如上,能够看到-15~15均可以用一个VLQ小节来表示。当|Number|>15时,则须要用到多个VLQ小节。另外,正负只需用第一小节的最后一位表示。因此多个小节时,第>1个小节的第6位能够不表明正负,也表明内容。

若是本身有须要使用VLQ编码的话,则能够参考市面上现成的库~好比GitHub - Rich-Harris/vlq

番外:webpack的几种sourcemap选项

用webpack的人都知道,它有好多种sourcemap选项Devtool | webpack。在没了解sourcemap的深层本质以前,我对这几个选项都只知其一;不知其二,就死记硬背着cheap-module-eval-source-map是最优解╮( ̄▽ ̄")╭

其实这几个选项,都是关键字cheap, module, eval, inline, hidden + sourcemap的排列组合,只要清楚各自的做用及意义就行了。

  • cheap:一看就是简略版的source-map,cheap不包含两样东西。①不映射列名②不映射loader编译前的代码。若是存在loader,好比babel或者jsx,那么就存在双重sourcemap了。生成后代码 => loader处理后代码 => 源代码。
  • module:映射到loader处理前代码。通常和cheap搭配使用,这样cheap-module属性就仅仅是关闭列映射了。
  • eval:eval自己指经过每个模块主体都被eval()包裹并执行。而eval-source-map则意味着在每一个模块下,sourceMappingUrl都经过DataURI的方式加载。
    image
  • inline:直接放在编译后js底部的sourceMappingUrl也是DataURI形式的。
    image
  • hidden:隐藏属性。编译后js底部不会标明sourceMappingUrl,须要手动加载。可用于错误监控服务器上报错误日志时肯定位置etc。

对我而言,行数必需列数没必要需,映射loader编译前的代码也必需,再加上eval在从新构建时比较快,因此cheap-module-eval-source-map确实是一个不错的选择。

另外,可别把source-map带到线上去了呀,那会给人人带来视奸你代码的机会。

最后,当在开发模式下debug代码时,要记得感谢sourcemap和浏览器的支持。每个小小的细节和好用之处,都离不开前人长久的努力呀。

Refs & Recommend Readings:

相关文章
相关标签/搜索