源映射机制用于将程序的源代码映射到基于其生成的脚本。尽管该主题不是新话题,并且已经撰写了许多文章(例如this,this和this),但仍需要澄清某些方面。本文旨在以简短易用的形式简化和系统化此主题上的所有已知信息。
本文讨论了与流行浏览器(例如Google Chrome DevTools)环境中的客户端开发有关的Source Maps,尽管它们的范围并不限于任何特定的语言或环境。当然,源地图的主要来源是标准,尽管它尚未被采用(状态-建议),但仍得到浏览器的广泛支持。
Source Maps的工作始于2000年代后期,为Firebug Closure Inspector插件创建了第一个版本。第二个版本于2010年发布,其中包含减小地图文件大小的更改。第三版是Google和Mozilla合作开发的一部分,于2011年提出(最新修订于2013年)。
当前,在客户端开发环境中,存在这样一种情况:源代码几乎从未直接集成到网页中,而是在此之前经历了各个处理阶段:最小化,优化,级联,此外,源代码本身可以用需要翻译的语言编写... 在这种情况下,出于调试目的,您需要一种机制,使您可以在调试器中准确观察人类可读的源代码。
源地图需要以下文件:
- 实际生成的javascript文件
- 一组带有用于创建它的源代码的文件
- 将它们相互映射的映射文件
地图文件
Source Maps的整个工作都基于一个地图文件,该文件可能看起来像这样:
{
"version":3,
"file":"index.js",
"sourceRoot":"",
"sources":["../src/index.ts"],
"names":[],
"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,SAAS,SAAS;IACd,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;",
"sourcesContent": []
}
通常,映射文件的名称由其所属脚本的名称组成,并带有“ .map”扩展名bundle.js-bundle.js.map。这是具有以下字段的常规json文件:
- “ Version”-源地图版本;
- “文件”-(可选)当前映射文件所属的生成文件的名称;
- “ SourceRoot”-(可选)源文件路径的前缀;
- “ Sources”-源文件路径的列表(以与script标记的src地址相同的方式解析,可以使用file://。);
- “名称”-生成的文件中发生更改的变量和函数的名称的列表;
- “映射”-以Base64 VLQ格式将源文件的变量和函数映射到生成的文件的坐标;
- “ SourcesContent”-(可选)对于独立的映射文件,是一个行列表,其中的每一行都包含来自源文件的源文本;
下载源地图
为了使浏览器加载地图文件,可以使用以下方法之一:
- JavaScript文件带有一个HTTP标头:SourceMap:<url>(以前不推荐使用的X-SourceMap:<url>)
- 生成的JavaScript文件具有特殊注释,例如:
//# sourceMappingURL=<url> ( CSS /*# sourceMappingURL=<url> */)
因此,下载地图文件后,浏览器将从“源”字段中拉出源,并使用“映射”字段中的数据在生成的脚本上显示它们。在DevTools的Sources选项卡中,您将找到两个选项。
文件://伪协议可用于指定路径。另外,<url>可以采用Base64编码包含映射文件的全部内容。在Webpack术语中,像这样的源映射称为内联源映射。
//# sourceMappingURL=data:application/json;charset=utf-8;base64,<source maps Base64 code>
源地图加载错误
, map- -, Network DevTools. , map-, Console DevTools : «DevTools failed to load SourceMap: ...». , : «Could not load content for ...».
独立的地图文件
源文件代码可以直接包含在映射文件的“ sourcesContent”字段中,如果此字段可用,则无需单独下载它们。在这种情况下,“源”中文件的名称不反映其真实地址,并且可以完全是任意的。这就是为什么在DevTools的Sources选项卡中,您会看到如此奇怪的“协议”:webpack://,ng://等。
对应
映射机制的本质是将生成文件中变量和函数名称的坐标(行/列)映射到相应源代码文件中的坐标。为了使显示机制正常工作,需要以下信息:
(#1)生成的文件中的行号;
(#2)生成文件中的列号;
(#3)“来源”中的来源索引;
(#4)源行号;
(#5)源列号;
所有这些数据都在``映射''字段中,其值是一个长字符串,具有特殊的结构和在Base64 VLQ中编码的值。
该行用分号(;)分隔为与生成的文件(#1)中的行相对应的部分。
每个部分都用逗号(,)分隔为段,每个段可以包含1,4或5个值:
- 生成的文件中的列号(#2);
- “源”(#3)中的源索引;
- 源行号(#4);
- 源列号(#5);
- “名称”列表中变量/函数名称的索引;
行号和列号的值是相对的,表示相对于先前坐标的偏移量,并且仅表示文件或节开始处的第一个坐标。
每个值都是一个Base64 VLQ号。 VLQ(可变长度量)是使用任意数量的固定长度的二进制块对任意较大的数字进行编码的原理。
源映射使用六位块,从低到高排序。保留每个块的最高有效第6位(连续位),如果将其置位,则当前块后跟与相同编号相关的下一个块,如果清除,则表示序列已完成。
由于源映射必须有一个符号,因此也为它保留了符号位,但仅在序列的第一个块中。如预期的那样,置位符号位表示负数。
因此,如果一个数字可以在单个块中编码,则它不能以15为模(1111 2),因为在序列的前六位块中保留了两位:连续位将始终被清除,符号位将根据数字的符号进行设置。
六位VLQ块被映射到Base64编码,其中每个六位序列被映射到特定的ASCII字符。
解码号码mE。反转顺序,最后一部分是Em。我们从Base64解码数字:E-000100,m-100110。在第一个中,我们丢弃高连续性位和两个前导零-100。在第二个中,我们丢弃高连续性和低号位(符号位被清除-数字为正)-0011。结果,我们得到100 0011 2,对应于十进制67。
您也可以沿相反方向编码41。其二进制代码为101001 2,我们将其分为两个块:上部为10,最小的部分(始终为4位)为1001。将最高有效的连续位(清除)添加到上部,并将三个前导零加000010。将最高有效的连续位(设置)添加到下部。最低有效符号位(清零-数字为正)-110010。我们在Base64中对数字进行编码:000010-C,110010-y。我们反转顺序,结果得到yC。
同名的库对于使用VLQ非常有用。