高级编译

概览

使用 Closure 编译器并指定 compilation_levelADVANCED_OPTIMIZATIONS 时,压缩率比使用 SIMPLE_OPTIMIZATIONSWHITESPACE_ONLY 进行编译时更高。使用 ADVANCED_OPTIMIZATIONS 进行编译时,会以更激进的方式转换代码和重命名符号,从而实现额外的压缩。不过,这种更激进的方法意味着,您在使用 ADVANCED_OPTIMIZATIONS 时必须更加谨慎,以确保输出代码的运行方式与输入代码相同。

本教程说明了 ADVANCED_OPTIMIZATIONS 编译级别的作用,以及如何确保代码在通过 ADVANCED_OPTIMIZATIONS 编译后正常运行。它还介绍了 extern 的概念:一种在编译器处理的代码之外的代码中定义的符号。

在阅读本教程之前,您应该熟悉使用基于 Java 的编译器应用等 Closure 编译器工具编译 JavaScript 的过程。

关于术语的说明:--compilation_level 命令行标志支持更常用的缩写 ADVANCEDSIMPLE,以及更精确的 ADVANCED_OPTIMIZATIONSSIMPLE_OPTIMIZATIONS。本文档使用较长的形式,但这些名称可以在命令行中互换使用。

  1. 更出色的压缩效果
  2. 如何启用 ADVANCED_OPTIMIZATIONS
  3. 使用 ADVANCED_OPTIMIZATIONS 时需要注意的事项
    1. 移除您想保留的代码
    2. 属性名称不一致
    3. 分别编译两部分代码
    4. 已编译代码与未编译代码之间的引用中断

压缩效果更佳

在默认编译级别 SIMPLE_OPTIMIZATIONS 下,Closure 编译器会通过重命名局部变量来缩小 JavaScript 代码。不过,除了局部变量之外,还有其他符号可以缩短,而且除了重命名符号之外,还有其他方法可以缩小代码。使用 ADVANCED_OPTIMIZATIONS 进行编译可充分利用各种代码缩减可能性。

比较以下代码中 SIMPLE_OPTIMIZATIONSADVANCED_OPTIMIZATIONS 的输出:

function unusedFunction(note) {
  alert(note['text']);
}

function displayNoteTitle(note) {
  alert(note['title']);
}

var flowerNote = {};
flowerNote['title'] = "Flowers";
displayNoteTitle(flowerNote);

使用 SIMPLE_OPTIMIZATIONS 进行编译会将代码缩短为以下形式:

function unusedFunction(a){alert(a.text)}function displayNoteTitle(a){alert(a.title)}var flowerNote={};flowerNote.title="Flowers";displayNoteTitle(flowerNote);

使用 ADVANCED_OPTIMIZATIONS 进行编译可将代码完全缩短为以下形式:

alert("Flowers");

这两个脚本都会生成一条内容为 "Flowers" 的提醒,但第二个脚本要小得多。

ADVANCED_OPTIMIZATIONS 级优化在多个方面超越了简单的变量名称缩短,包括:

  • 更激进的重命名

    使用 SIMPLE_OPTIMIZATIONS 进行编译只会重命名 displayNoteTitle()unusedFunction() 函数的 note 参数,因为这些是脚本中唯一局部于函数的变量。ADVANCED_OPTIMIZATIONS 还会重命名全局变量 flowerNote

  • 移除无用代码

    使用 ADVANCED_OPTIMIZATIONS 进行编译会完全移除函数 unusedFunction(),因为该函数从未在代码中调用。

  • 函数内联

    使用 ADVANCED_OPTIMIZATIONS 进行编译会将对 displayNoteTitle() 的调用替换为构成函数正文的单个 alert()。这种将函数调用替换为函数正文的做法称为“内联”。如果函数更长或更复杂,内联可能会改变代码的行为,但 Closure 编译器会确定在这种情况下内联是安全的,并且可以节省空间。使用 ADVANCED_OPTIMIZATIONS 进行编译时,如果确定可以安全地内联常量和某些变量,也会这样做。

此列表仅列出了 ADVANCED_OPTIMIZATIONS 编译可执行的部分缩减大小的转换。

如何启用 ADVANCED_OPTIMIZATIONS

如需为 Closure 编译器应用启用 ADVANCED_OPTIMIZATIONS,请添加命令行标志 --compilation_level ADVANCED_OPTIMIZATIONS,如以下命令所示:

java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js hello.js

使用 ADVANCED_OPTIMIZATIONS 时需要注意的事项

下面列出了一些 ADVANCED_OPTIMIZATIONS 的常见意外影响,以及您可以采取的避免措施。

移除您想保留的代码

如果您仅使用 ADVANCED_OPTIMIZATIONS 编译以下函数,Closure Compiler 会生成空输出:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}

由于您传递给编译器的 JavaScript 中从未调用过该函数,因此 Closure 编译器会假设不需要此代码!

在许多情况下,此行为正是您想要的。例如,如果您将代码与大型库一起编译,Closure 编译器可以确定您实际使用了该库中的哪些函数,并舍弃未使用的函数。

不过,如果您发现 Closure Compiler 正在移除您想要保留的函数,可以通过以下两种方式来防止这种情况发生:

  • 将函数调用移至由 Closure Compiler 处理的代码中。
  • 为要公开的函数添加外部声明。

接下来的部分将更详细地讨论每个选项。

解决方案:将函数调用移至由 Closure Compiler 处理的代码中

如果您仅使用 Closure Compiler 编译部分代码,可能会遇到不必要的代码移除情况。例如,您可能有一个仅包含函数定义的库文件,以及一个包含该库并包含调用这些函数的代码的 HTML 文件。在这种情况下,如果您使用 ADVANCED_OPTIMIZATIONS 编译库文件,Closure Compiler 会移除所有库函数。

解决此问题的最简单方法是将函数与调用这些函数的程序部分一起编译。例如,Closure Compiler 在编译以下程序时不会移除 displayNoteTitle()

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
displayNoteTitle({'myTitle': 'Flowers'});

在这种情况下,系统不会移除 displayNoteTitle() 函数,因为 Closure 编译器会发现该函数被调用了。

换句话说,您可以通过在传递给 Closure Compiler 的代码中添加程序的入口点,来防止系统移除不需要的代码。程序的入口点是指程序开始执行的代码中的位置。例如,在上一部分中的鲜花注释程序中,最后三行会在浏览器中加载 JavaScript 后立即执行。这是此程序的入口点。为了确定需要保留哪些代码,Closure 编译器会从此入口点开始,并从此处开始向前跟踪程序的控制流。

解决方案:为要公开的函数添加 Externs

有关此解决方案的更多信息,请参阅下文以及有关外部变量和导出的页面。

不一致的媒体资源名称

无论您使用哪个编译级别,Closure Compiler 编译都不会更改代码中的字符串字面量。这意味着,使用 ADVANCED_OPTIMIZATIONS 进行编译时,系统会根据您的代码是否使用字符串访问属性,以不同的方式处理这些属性。如果您将属性的字符串引用与点语法引用混用,Closure Compiler 会重命名该属性的某些引用,但不会重命名其他引用。因此,您的代码可能无法正常运行。

例如,请看以下代码:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
var flowerNote = {};
flowerNote.myTitle = 'Flowers';

alert(flowerNote.myTitle);
displayNoteTitle(flowerNote);

此源代码中的最后两条语句的作用完全相同。不过,当您使用 ADVANCED_OPTIMIZATIONS 压缩代码时,会得到以下结果:

var a={};a.a="Flowers";alert(a.a);alert(a.myTitle);

压缩代码中的最后一条语句会产生错误。对 myTitle 属性的直接引用已重命名为 a,但 displayNoteTitle 函数中对 myTitle 的带引号引用尚未重命名。因此,最后一条语句引用的是已不存在的 myTitle 属性。

解决方案:保持媒体资源名称的一致性

此解决方案非常简单。对于任何给定的类型或对象,请仅使用点语法或带引号的字符串。请勿混用这两种语法,尤其是在引用同一属性时。

此外,请尽可能使用点式语法,因为它可以提供更好的检查和优化。仅当您不希望 Closure 编译器执行重命名操作时(例如,当名称来自外部来源(如解码的 JSON)时),才使用带引号的字符串属性访问。

分别编译两部分代码

如果您将应用拆分为不同的代码块,可能需要单独编译这些代码块。不过,如果两段代码有任何交互,这样做可能会导致困难。即使您成功了,两次运行 Closure Compiler 的输出结果也不兼容。

例如,假设某个应用分为两部分:一部分用于检索数据,另一部分用于显示数据。

以下是用于检索数据的代码:

function getData() {
  // In an actual project, this data would be retrieved from the server.
  return {title: 'Flower Care', text: 'Flowers need water.'};
}

以下是用于显示数据的代码:

var displayElement = document.getElementById('display');
function displayData(parent, data) {
  var textElement = document.createTextNode(data.text);
  parent.appendChild(textElement);
}
displayData(displayElement, getData());

如果您尝试分别编译这两段代码,会遇到几个问题。首先,Closure 编译器会移除 getData() 函数,原因如移除您想要保留的代码中所述。其次,Closure 编译器在处理显示数据的代码时会产生严重错误。

input:6: ERROR - variable getData is undefined
displayData(displayElement, getData());

由于编译器在编译显示数据的代码时无法访问 getData() 函数,因此会将 getData 视为未定义。

解决方案:将网页的所有代码一起编译

为确保正确编译,请在一次编译运行中一起编译网页的所有代码。Closure 编译器可以接受多个 JavaScript 文件和 JavaScript 字符串作为输入,因此您可以在单个编译请求中同时传递库代码和其他代码。

注意:如果您需要混合使用已编译和未编译的代码,此方法将不起作用。如需获取有关处理此情况的提示,请参阅已编译代码与未编译代码之间的引用中断

编译后代码与未编译代码之间的引用中断

ADVANCED_OPTIMIZATIONS 中的符号重命名会中断 Closure Compiler 处理的代码与任何其他代码之间的通信。编译会重命名源代码中定义的函数。编译后,任何调用您函数的外部代码都会中断,因为它们仍引用旧的函数名称。同样,已编译代码中对外部定义符号的引用可能会被 Closure 编译器更改。

请注意,“未编译的代码”包括以字符串形式传递给 eval() 函数的任何代码。Closure 编译器绝不会更改代码中的字符串字面量,因此 Closure 编译器不会更改传递给 eval() 语句的字符串。

请注意,以下是相关但不同的问题:维护编译到外部的通信,以及维护从外部到编译的通信。这些单独的问题有一个共同的解决方案,但每个方面都有细微差别。为了充分利用 Closure 编译器,您必须了解自己属于哪种情况。

在继续之前,您可能需要先熟悉 extern 和 export

从已编译的代码调用外部代码的解决方案:使用 Extern 进行编译

如果您使用由其他脚本提供给网页的代码,则需要确保 Closure Compiler 不会重命名您对该外部库中定义的符号的引用。为此,请在编译中包含一个包含外部库 extern 的文件。这样一来,Closure 编译器就会知道哪些名称不受您控制,因此无法更改。您的代码必须使用与外部文件相同的名称。

这方面的常见示例包括 OpenSocial APIGoogle Maps API 等 API。例如,如果您的代码调用了 OpenSocial 函数 opensocial.newDataRequest(),但没有相应的外部声明,Closure 编译器会将此调用转换为 a.b()

从外部代码调用到已编译代码的解决方案:实现外部声明

如果您有重复用作库的 JavaScript 代码,可能希望使用 Closure 编译器仅缩小库,同时仍允许未编译的代码调用库中的函数。

在这种情况下,解决方案是实现一组用于定义库的公共 API 的外部声明。您的代码将为这些外部声明中声明的符号提供定义。这意味着您的外部声明中提及的任何类或函数。它还可能意味着让您的类实现外部声明中声明的接口。

这些外部声明不仅对您有用,对其他人也有用。如果您的库的使用者要编译其代码,就需要添加这些库,因为从他们的角度来看,您的库代表的是外部脚本。您可以将外部函数声明视为您与消费者之间的合同,双方都需要一份副本。

为此,请确保在编译代码时,您还会在编译中包含 extern。这可能看起来很奇怪,因为我们通常认为外部变量是“来自其他地方”的,但有必要告知 Closure 编译器您要公开哪些符号,以免它们被重命名。

这里需要注意一个重要事项,即您可能会收到有关定义外部符号的代码的“重复定义”诊断信息。Closure Compiler 会假定外部库提供了 externs 中的任何符号,并且目前无法理解您是有意提供定义。 这些诊断信息可以安全地抑制,您可以将抑制视为确认您确实在实现 API。

此外,Closure 编译器可能会检查您的定义是否与外部声明的类型匹配。这可进一步确认您的定义是否正确。