JavaScript Source Maps 简介

Ryan Seddon

您有没有想过在不影响性能的情况下,即使经过合并和缩减,客户端代码仍可保持可读性,更重要的是可调试性呢?现在,您可以轻松探索源映射的魔力。

源映射是一种将合并/缩减文件映射回未构建状态的方法。在针对生产环境构建应用时,您不仅会缩减和合并 JavaScript 文件,还会生成一个源映射,其中包含原始文件的相关信息。在生成的 JavaScript 中查询特定行号和列号时,您可以在源映射中执行查询,该映射会返回原始位置。开发者工具(目前为 WebKit 每夜 build、Google Chrome 或 Firefox 23+)可以自动解析源映射,使其看起来就像正在运行未缩小和未合并的文件一样。

此演示允许您右键点击包含已生成源代码的文本区域中的任意位置。选择“获取原始位置”将通过传入生成的行号和列号查询源映射,并返回原始代码中的位置。请确保您的控制台处于打开状态,这样您才能看到输出内容。

实际运用 Mozilla JavaScript 源代码映射库的示例。

现实世界

在您查看以下源代码映射的实际实现之前,请确保您已在 Chrome Canary 或 WebKit 每夜版中启用了源映射功能,方法是点击开发者工具面板中的设置齿轮,并选中“启用源代码映射”选项。

如何在 WebKit 开发者工具中启用源代码映射。

Firefox 23+ 在内置开发者工具中默认启用源映射。

如何在 Firefox 开发者工具中启用源代码映射。

为什么我应该关注源映射?

目前,源映射只能在未压缩/合并的 JavaScript 与已压缩/未合并的 JavaScript 之间发挥作用,但随着 CoffeeScript 等编译为 JavaScript 的语言的讨论,甚至有可能添加对 CSS 预处理器(例如 SASS 或 LESS)的支持,前景光明。

将来,我们几乎可以轻松地使用任何语言,就像使用源映射的浏览器本身就支持这些语言一样:

  • CoffeeScript
  • ECMAScript 6 及更高版本
  • SASS/LESS 等
  • 编译为 JavaScript 的几乎所有语言

请查看在 Firefox 控制台的实验性版本中正在调试的 CoffeeScript 的抓屏:

Google Web Toolkit (GWT) 最近添加了对 Source Maps 的支持。GWT 团队的 Ray Cromwell 制作了一个精彩的抓屏,展示了源代码映射支持的实际运作方式。

我构建的另一个示例使用 Google 的 Traceur 库,您可以通过该库编写 ES6(ECMAScript 6 或 Next)并将其编译为与 ES3 兼容的代码。Traceur 编译器还会生成源映射。请查看此演示,了解使用时所使用的 ES6 特征和类,这要归功于源映射,它们在浏览器中本身就受支持。

通过演示中的文本区域,您还可以编写 ES6,它会实时编译,并生成源映射和等效的 ES3 代码。

使用源代码映射进行 Traceur ES6 调试。

演示:编写 ES6、进行调试、查看实际应用源映射

源代码映射的工作原理是什么?

目前,唯一支持生成源映射的 JavaScript 编译器/缩减器是 Closure 编译器。(稍后我会说明具体使用方法。)当您合并和缩减 JavaScript 后,旁边会出现一个源映射文件。

目前,Closure 编译器不会在文件末尾添加特殊的注释,以便向浏览器开发者工具表明源映射可用:

//# sourceMappingURL=/path/to/file.js.map

这样,开发者工具便可将调用映射回它们在原始源文件中的位置。之前,注释 pragma 是 //@,但由于该注释和 IE 条件编译注释存在一些问题,因此决定将其更改为 //#。目前,Chrome Canary 版、WebKit Nightly 和 Firefox 24 及更高版本支持新的评论指令。此语法更改也会影响 source网址。

如果您不喜欢这种奇怪的注释的想法,也可以在编译的 JavaScript 文件中设置一个特殊标头:

X-SourceMap: /path/to/file.js.map

像注释一样,这会告知源映射使用方在哪里查找与 JavaScript 文件关联的源映射。此标头还可以解决以不支持单行注释的语言引用源映射的问题。

WebKit Devtools 示例:启用和停用源代码映射。

只有在启用源映射并打开开发者工具的情况下,才会下载源映射文件。您还需要上传原始文件,以便开发者工具可以在必要时引用并显示这些文件。

如何生成源代码映射?

您需要使用 Closure 编译器来缩减、合并和生成 JavaScript 文件的源映射。命令如下所示:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

--create_source_map--source_map_format 是两个重要的命令标志。这是必需的,因为默认版本为 V2,我们只希望使用 V3。

源映射剖析

为了更好地了解源代码映射,我们将举一个由 Closure 编译器生成的源代码映射文件的小例子,并深入探讨“映射”部分的工作原理。以下示例与 V3 规范示例略有不同。

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

如上所示,源映射是一个包含大量实用信息的对象字面量:

  • 源映射所基于的版本号
  • 所生成代码的文件名(您的缩减/合并后的生产文件)
  • sourceRoot 可让您在源前面加上文件夹结构 - 这也是一种节省空间的技术
  • 来源包含合并在一起的所有文件名
  • name 包含出现在您的整个代码中的所有变量/方法的名称。
  • 最后,映射属性就是使用 Base64 VLQ 值的神奇之处。这样就可以节省空间。

Base64 VLQ 并保持源映射较小

最初,源映射规范的所有映射的输出非常详细,导致源映射的大小大约是所生成代码大小的 10 倍。版本 2 将大小减少了约 50%,而版本 3 又减少了 50%,因此对于一个 133 kB 的文件,最终获得的源映射大小为约 300 kB。

那么,他们是如何在保持复杂映射的同时缩减大小的呢?

VLQ(可变长度数量)结合使用可将值编码为 Base64 值。映射属性是一个超大字符串。该字符串中的分号 (;) 表示生成文件中的行号。每行中的英文逗号 (,) 表示该行中的每个线段。在可变长度字段中,每个段为 1、4 或 5。某些部分可能会显示更长,但包含接续位。每个位均在前一个段基础上构建,这有助于减小文件大小,因为每个位都是相对于其前一个段位而言的。

源映射 JSON 文件中的段细分。

如上所述,每段可变长度可为 1、4 或 5。此图被视为可变长度为 4 且带有一个扩展位 (g)。我们将细分此段,并向您展示源映射是如何确定原始位置的。

上面显示的值纯粹是 Base64 解码的值,要获取它们的真实值需要一些额外的处理。每个细分通常要搞定以下五件事:

  • 生成的列
  • 出现过此问题的原始文件
  • 原始行号
  • 原始列
  • 以及原名(如果有)

并非所有分段都有名称、方法名称或参数,因此所有分段都会在四和五个变量长度之间切换。上段图中的 g 值称为接续位,可用于在 Base64 VLQ 解码阶段进一步优化。扩展位可让您基于段值进行构建,以便存储大数字而无需存储大数字,这是一种非常智能的空间节省技术,其根采用 midi 格式。

上图 AAgBC 经过进一步处理后,将返回 0, 0, 32, 16, 1 - 32 是有助于构建以下值 16 的延续位。B 采用 Base64 进行纯解码后是 1。因此使用的重要值为 0、0、16、1。这可让我们知道所生成文件的第 1 行(行数按英文分号保持计数)第 0 列映射到文件 0(文件 0 的数组为 foo.js),第 16 行的第 1 列。

我将引用 Mozilla 的 Source Map JavaScript 库,以便说明这些片段的解码方式。您也可以参考 WebKit 开发者工具源代码映射代码,这些代码也是以 JavaScript 编写的。

为了正确理解我们如何从 B 获取值 16,我们需要对按位运算符有基本的了解,以及规范如何用于源映射。通过使用按位 AND (&) 运算符对数字 (32) 和 VLQ_CONTINUATION_BIT(二进制 100000 或 32)进行比较,将前一个数字 g 标记为延续位。

32 & 32 = 32
// or
100000
|
|
V
100000

此方法将在每个位的位置都返回 1,因为这两个位都显示有该位。因此,33 & 32 的 Base64 解码值将返回 32,因为它们仅共享 32 位位置,如上图所示。然后,针对每个前置延续位,将位移值增加 5。在上例中,它仅偏移 5 次,因此左移 1 (B) 5。

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

然后,通过将数字 (32) 右移一个点,从 VLQ 有符号值进行转换。

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

这就是 1 到 16 的过程。这个过程看起来可能过于复杂,但一旦数字开始变得越来越大,它就会更有意义。

潜在的 XSSI 问题

该规范提及了可能因使用源代码映射而导致的跨网站脚本收录问题。为了缓解此问题,建议您在源代码映射的第一行前面加上“)]}”,以便故意使 JavaScript 失效,从而引发语法错误。WebKit 开发者工具已经能够处理这一问题。

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

如上所示,对前三个字符进行划分,以检查它们是否与规范中的语法错误匹配,如果匹配,则移除第一个换行符实体 (\n) 之前的所有字符。

sourceURLdisplayName 的实际运用:评估和匿名函数

虽然不是源映射规范的一部分,但以下两个惯例可以让您在使用评估和匿名函数时大大简化开发工作。

第一个帮助程序与 //# sourceMappingURL 属性非常相似,并且实际上在源代码映射 V3 规范中有所提及。通过在代码中(将要求值)添加以下特殊注释,您可以为评估命名,使其在开发者工具中显示为更符合逻辑的名称。查看使用 CoffeeScript 编译器的简单演示:

演示:查看 eval() 通过 source网址 显示为脚本的代码

//# sourceURL=sqrt.coffee
source网址 特殊注释在开发者工具中是什么样的

另一个帮助程序允许您使用匿名函数当前上下文提供的 displayName 属性来命名匿名函数。分析以下演示以查看 displayName 属性的实际效果。

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
显示实际运用的 displayName 属性。

在开发者工具中对代码进行性能分析时,系统将显示 displayName 属性,而不是 (anonymous) 之类的属性。但是,displayName 几乎已经完全停用,而无法在 Chrome 中实现。但所有希望并没有丢失,我们提出了一个更好的方案,名为 debugName

在编写时,评估命名仅适用于 Firefox 和 WebKit 浏览器。displayName 属性仅适用于 WebKit 夜间模式。

让我们团结起来

目前,有关向 CoffeeScript 添加源映射支持的讨论很长。请查看相应问题,并添加对将源映射生成添加到 CoffeeScript 编译器的支持。对于 CoffeeScript 及其忠实粉丝来说,这将是一个巨大的胜利。

UglifyJS 还存在一个你应该考虑的源映射问题

很多tools都会生成源代码映射,包括 Coffeescript 编译器。现在,我认为这个问题没有实际意义。

可用于生成源代码映射的工具越多,对我们的效果就越好,因此请尽情获取,为您最喜爱的开源项目寻求或添加源代码映射支持。

有待完善

目前,源映射未能满足的一项内容是监视表达式。问题在于,在当前执行上下文中尝试检查参数或变量名称时,不会返回任何内容,因为实际并不存在。这需要进行某种反向映射,以查找您要检查的参数/变量的真实名称,并与已编译的 JavaScript 中的实际参数/变量名称进行比较。

这当然是一个可以解决的问题,如果对源映射有更多的关注,我们就可以开始看到一些令人惊叹的功能和更好的稳定性。

问题

最近,jQuery 1.9 增加了对由官方 CDN 提供的源映射的支持。此外,它还指出了一个特殊 bug:在 jQuery 加载之前使用 IE 条件编译注释 (//@cc_on)。自此之后,就有一种提交通过将 sourceMapping网址 封装在多行注释中来缓解此问题。要学习的经验,请勿使用条件注释。

此后,通过将语法更改为 //#,此问题已得到解决

工具和资源

下面列出了一些更多资源和工具,建议您查看:

源映射是开发者工具集中一个非常强大的实用程序。让您的 Web 应用保持精简但易于调试非常有用。对于新手开发者来说,它也是一个非常强大的学习工具,能够让经验丰富的开发者了解他们如何构建和编写应用,而无需费力寻找难以阅读的缩减代码。

还等什么?立即开始为所有项目生成源代码映射!