您是否曾希望客户端代码在组合和缩减后仍能保持可读性,更重要的是,能否在性能不受影响的情况下进行调试?现在,您可以通过源代码映射的魔力来实现这一点。
源代码映射是一种将合并/缩减的文件映射回未构建状态的方法。如果您为生产环境构建应用,缩小和合并 JavaScript 文件时,还会生成包含原始文件相关信息的源映射。当您查询生成的 JavaScript 中的某行和某列号时,可以查找源映射,该映射会返回原始位置。开发者工具(目前为 WebKit 每夜 build、Google Chrome 或 Firefox 23 及更高版本)可以自动解析源映射,使其看起来像是您在运行未压缩且未合并的文件。
在演示中,您可以右键点击包含生成的源代码的文本区域中的任意位置。选择“获取原始位置”会通过传入生成的行号和列号来查询源代码映射,并返回原始代码中的位置。确保控制台已打开,以便您查看输出。
真实世界
在查看以下 Source Maps 的实际实现之前,请确保您已在 Chrome Canary 或 WebKit Nightly 中启用 Source Maps 功能,具体方法是点击开发者工具面板中的设置齿轮,然后选中“启用 Source Maps”选项。
Firefox 23 及更高版本的默认内置开发者工具中启用了源映射。
为什么我应该关注源代码映射?
目前,源代码映射仅适用于将未压缩/合并的 JavaScript 映射到压缩/未合并的 JavaScript,但未来前景一片光明,人们在讨论将 CoffeeScript 等编译为 JavaScript 的语言,甚至有可能添加对 SASS 或 LESS 等 CSS 预处理器的支持。
将来,我们可以轻松使用几乎任何语言,就像浏览器在源代码映射中原生支持这些语言一样:
- CoffeeScript
- ECMAScript 6 及更高版本
- SASS/LESS 等
- 几乎所有可编译为 JavaScript 的语言
请观看以下屏幕录制内容,了解如何在 Firefox 控制台的实验性 build 中调试 CoffeeScript:
Google Web Toolkit (GWT) 最近添加了对 Source Maps 的支持。GWT 团队的 Ray Cromwell 制作了一个精彩的屏幕演示,展示了源代码映射支持的运作方式。
我编写的另一个示例使用了 Google 的 Traceur 库,该库可让您编写 ES6(ECMAScript 6 或 Next)并将其编译为与 ES3 兼容的代码。Traceur 编译器还会生成一个源映射。请查看此演示,了解 ES6 trait 和类的使用方式,就像浏览器中原生支持它们一样,这得益于源映射。
您还可以使用演示中的文本区域编写 ES6,系统会动态编译该代码并生成源映射以及等效的 ES3 代码。
演示:编写 ES6、调试 ES6、查看源代码映射的实际运作情况
源映射是如何运作的?
目前,唯一支持生成源映射的 JavaScript 编译器/缩减器是 Closure 编译器。(我稍后会介绍如何使用它。)合并和缩减 JavaScript 后,源映射文件将与其一起存在。
目前,Closure 编译器不会在末尾添加特殊注释,而该注释是向浏览器开发者工具表明有源映射的必要条件:
//# sourceMappingURL=/path/to/file.js.map
这样,开发者工具就可以将调用映射回原始源文件中的位置。之前,注释伪指令为 //@
,但由于该注释和 IE 条件编译注释存在一些问题,因此决定将其更改为 //#
。目前,Chrome Canary、WebKit Nightly 和 Firefox 24 及更高版本支持新的注释 Pragma。此语法变更也会影响 source网址。
如果您不喜欢这种奇怪的注释,则可以改为在已编译的 JavaScript 文件上设置特殊标头:
X-SourceMap: /path/to/file.js.map
像注释一样,它也可以告知源映射使用方在哪里可以查找与 JavaScript 文件关联的源映射。此标头也可以解决以不支持单行注释的语言引用源映射的问题。
只有在您启用了源代码映射并打开了开发者工具的情况下,系统才会下载源代码映射文件。您还需要上传原始文件,以便开发者工具在必要时引用和显示这些文件。
如何生成源映射?
您需要使用 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"
}
如上所示,源映射是一个包含大量有用信息的对象字面量:
- 源映射所依据的版本号
- 生成的代码的文件名(您的 minifed/合并后的正式版文件)
- sourceRoot 允许您在源代码前面添加文件夹结构,这也是一种节省空间的技术
- sources 包含合并的所有文件名
- names 包含代码中出现的所有变量/方法名称。
- 最后,mappings 属性是使用 Base64 VLQ 值发挥魔力的地方。真正的空间节省就在这里实现了。
Base64 VLQ 和缩减源映射的大小
最初,源代码映射规范对所有映射的输出非常详尽,导致源代码映射的大小约为生成的代码的 10 倍。版本 2 将其缩减了大约 50%,版本 3 又将其缩减了 50%,因此对于 133KB 的文件,最终的源映射大小约为 300KB。
那么,他们是如何在缩减大小的同时保留复杂映射的?
VLQ(可变长度数量)应与将值编码为 Base64 值一起使用。映射属性是一个超大的字符串。此字符串中包含英文分号 (;),表示生成的文件中的行号。每行中都有英文逗号 (,),表示该行中的每个细分。在可变长度字段中,这些段中的每个段都是 1、4 或 5。有些可能看起来更长,但包含接续位。每个片段都基于前一个片段构建,这有助于缩减文件大小,因为每个位都与其前面的片段相关。
如上所述,每个片段的长度可以是 1、4 或 5,此图表被视为长度为 4 的可变长度,其中包含一个接续位 (g)。我们将对此路段进行细分,并向您展示源地图如何确定原始位置。
上面显示的值仅是 Base64 解码值,还需要进行一些处理才能得到其真实值。每个细分受众群通常会确定以下五项:
- 生成的列
- 此问题出现在的原始文件
- 原始行号
- 原始列
- 以及原始名称(如果有)
并非每个片段都有名称、方法名称或参数,因此整个片段将在四个和五个可变长度之间切换。上文中分段图中的 g 值被称为接续位,这有助于在 Base64 VLQ 解码阶段进行进一步优化。借助接续位,您可以基于某个段值进行构建,这样您无需存储大数即可存储大数,这是一种非常巧妙的节省空间技术,源自 MIDI 格式。
上图中的 AAgBC
经过进一步处理后会返回 0, 0, 32, 16, 1,其中 32 是帮助构建下一个值 16 的接续位。仅使用 Base64 解码的 B 为 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 的每个位中返回 1。因此,Base64 解码后的 33 & 32
值会返回 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) 之前的所有字符。
sourceURL
和 displayName
的运作方式:eval 和匿名函数
虽然不是 Source Map 规范的一部分,但以下两个惯例可以让您在处理 eval 和匿名函数时将开发变得更轻松。
第一个帮助程序非常类似于 //# sourceMappingURL
属性,并且实际上在 Source Map V3 规范中也有所提及。通过将下面的特殊注释包含到代码中(将进行 eval 处理),您可以命名 eval,使其在开发者工具中以更具逻辑的名称显示。查看使用 CoffeeScript 编译器的简单演示:
演示:查看 eval()
的代码通过 source网址 显示为脚本
//# sourceURL=sqrt.coffee
借助另一个帮助程序,您可以使用匿名函数当前上下文中提供的 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
属性,而不是 (anonymous)
之类的属性。不过,displayName 几乎已被废弃,不会在 Chrome 中使用。不过,并非没有希望,我们提出了一个更好的方案,称为 debugName。
在撰写本文时,eval 命名方式仅适用于 Firefox 和 WebKit 浏览器。displayName
属性仅在 WebKit 夜间版中提供。
让我们一起行动
目前,关于向 CoffeeScript 添加源代码映射支持的讨论非常漫长。请查看该问题,并支持将源代码映射生成功能添加到 CoffeeScript 编译器中。这对 CoffeeScript 及其忠实的追随者来说将是巨大的胜利。
UglifyJS 还有一个源映射问题,您也应查看一下。
许多工具都可以生成源映射,包括 Coffeescript 编译器。我现在认为这是一个无效的论点。
我们可用的可生成源代码映射的工具越多,我们就越能发挥作用,因此请继续提出请求,或为您喜爱的开源项目添加源代码映射支持。
它并不完美
来源映射目前不支持监视表达式。问题在于,尝试在当前执行上下文中检查实参或变量名称不会返回任何内容,因为它实际上并不存在。这需要某种反向映射,以便查找您要检查的参数/变量的真实名称(相对于已编译 JavaScript 中的实际参数/变量名称)。
当然,这是一个可以解决的问题,随着对源代码映射的关注度提高,我们可以开始看到一些令人惊叹的功能和更好的稳定性。
问题
近期,jQuery 1.9 添加了对通过官方 CDN 分发的源代码映射的支持。它还指出了在 jQuery 加载之前使用 IE 条件编译注释 (//@cc_on) 时出现的奇怪 bug。此后,我们进行了一次commit,通过将 sourceMapping网址 封装在多行注释中来缓解此问题。要记住的是,请勿使用条件性评论。
此问题现已得到解决,语法已更改为 //#
。
工具和资源
以下是一些您应查看的其他资源和工具:
- Nick Fitzgerald 提供了一个支持源代码映射的 UglifyJS 分支
- Paul Irish 提供了一个实用的演示,展示了源代码映射
- 查看 WebKit 代码更改集,了解此功能何时弃用
- 该更改集还包含一项布局测试,这项测试是本文的起点
- Mozilla 存在一个bug,您应在内置控制台中关注源代码映射的状态
- Conrad Irwin 为所有 Ruby 用户编写了一个非常实用的源代码映射 gem
- 有关eval 命名和 displayName 属性的进一步阅读
- 您可以查看 Closure Compilers 源代码,了解如何创建源映射
- 有一些屏幕截图和关于支持 GWT 源映射的讨论
源代码映射是开发者工具集中非常强大的实用程序。能够让 Web 应用保持精简但易于调试非常有用。它也是一款非常强大的学习工具,新手开发者可以通过它了解经验丰富的开发者如何构建和编写应用,而无需费心阅读难以阅读的缩减代码。
还等什么?立即开始为所有项目生成源映射!