高级编译

概览

与使用 SIMPLE_OPTIMIZATIONSWHITESPACE_ONLY 进行编译相比,在 compilation_levelADVANCED_OPTIMIZATIONS 的情况下使用 Closure 编译器的压缩率更高。使用 ADVANCED_OPTIMIZATIONS 进行编译时,可通过更多方式转换代码和重命名符号,从而实现额外的压缩。不过,这种更积极的方法意味着,您在使用 ADVANCED_OPTIMIZATIONS 时必须格外小心,以确保输出代码与输入代码的工作方式相同。

本教程说明了 ADVANCED_OPTIMIZATIONS 编译级别的作用,以及您可以采取哪些措施来确保代码在使用 ADVANCED_OPTIMIZATIONS 进行编译后正常运行。此外,它还引入了 extern 的概念,它是指在由编译器处理的代码外部的代码中定义的符号。

在阅读本教程之前,您应该熟悉如何使用某个 Closure 编译器工具(编译器服务界面编译器服务 API编译器应用)编译 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 编译能够执行的缩减大小的转换的示例。

如何启用高级优化

Closure Compiler 服务界面、服务 API 和应用都有将 compilation_level 设置为 ADVANCED_OPTIMIZATIONS 的不同方法。

如何在 Closure Compiler 服务界面中启用 ADVANCED_OPTIMIZATIONS

如需为 Closure Compiler 服务界面启用 ADVANCED_OPTIMIZATIONS,请点击“Advanced”单选按钮。

如何在 Closure Compiler Service API 中启用 ADVANCED_OPTIMIZATIONS

如需为 Closure Compiler Service API 启用 ADVANCED_OPTIMIZATIONS,请添加名为 compilation_level 且值为 ADVANCED_OPTIMIZATIONS 的请求参数,如以下 Python 程序所示:

#!/usr/bin/python2.4

import httplib, urllib, sys

params = urllib.urlencode([
    ('code_url', sys.argv[1]),
    ('compilation_level', 'ADVANCED_OPTIMIZATIONS'),
    ('output_format', 'text'),
    ('output_info', 'compiled_code'),
  ])

headers = { "Content-type": "application/x-www-form-urlencoded" }
conn = httplib.HTTPSConnection('closure-compiler.appspot.com')
conn.request('POST', '/compile', params, headers)
response = conn.getresponse()
data = response.read()
print data
conn.close()

如何在 Closure 编译器应用中启用 ADVANCED_OPTIMIZATIONS

如需为 Closure Compiler 应用启用 ADVANCED_OPTIMIZATIONS,请添加命令行标记 --compilation_level ADVANCED_OPTIMIZATIONS,如以下命令所示:

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

使用 ADVANCED_OPTIMIZATIONS 时需要注意的事项

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

移除要保留的代码

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

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

由于在您传递到编译器的 JavaScript 中从不调用此函数,因此 Closure 编译器会假定不需要此代码!

在许多情况下,这种行为完全符合您的预期。例如,如果您使用大型库来编译代码,Closure 编译器可以确定您实际使用该库中哪些函数,并舍弃您不使用的函数。

不过,如果您发现 Closure 编译器正在移除要保留的函数,可以通过以下两种方法避免这种情况:

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

后续部分将更详细地讨论每个选项。

解决方案:将您的函数调用移至 Closure 编译器处理的代码中

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

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

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

在这种情况下,displayNoteTitle() 函数不会移除,因为 Closure Compiler 发现它已被调用。

换言之,您可以通过在传递给 Closure Compiler 的代码中添加程序的入口点来阻止不必要的代码移除。程序的入口点是代码中开始执行程序的位置。例如,在上一部分的花卉程序中,当浏览器加载 JavaScript 后,系统会立即执行最后三行代码。它是此计划的入口点。为确定您需要保留的代码,Closure Compiler 会从此入口点开始,并据此跟踪程序的控制流。

解决方案:为想要提供的函数添加 Extern

如需详细了解此解决方案,请参阅下文以及有关结账和导出的页面。

属性名称不一致

Closure 编译器编译绝不会更改代码中的字符串字面量,无论您使用哪种编译级别。这意味着,使用 ADVANCED_OPTIMIZATIONS 进行编译时,属性的处理方式有所不同,具体取决于您的代码是否使用字符串访问这些属性。如果将对属性的字符串引用与点语法引用混合,Closure 编译器会重命名对该属性的某些引用,而不是其他引用。因此,您的代码可能无法正确运行。

例如,以下代码:

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 Compiler 执行重命名操作时(例如当名称来自外部 JSON,例如解码的 JSON 时),才使用带英文引号的字符串属性访问权限。

分别编译两个部分的代码

如果您将应用拆分为不同的代码块,可能需要单独编译这些块。不过,如果两块代码完全交互,则这样做可能会造成困难。即使您成功了,两个 Closure 编译器运行的输出也不兼容。

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

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

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 编译器处理的代码与任何其他代码之间的通信。编译会重命名源代码中定义的函数。编译后调用函数的任何外部代码都将中断,因为它仍引用旧的函数名称。同样,编译代码中对外部定义的符号的引用可以由 Closure Compiler 更改。

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

请注意,这些问题虽然存在关联,但却很明显:保持编译到外部的通信,以及维持从外部编译的通信。这些单独的问题共用一个解决方案,但两方面都有细微差别。为充分利用 Closure 编译器,您必须了解自身情况。

在继续操作之前,您可能需要熟悉结账和导出

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

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

常见示例包括 OpenSocial APIGoogle Maps API 等 API。例如,如果您的代码在没有适当的外部调用的情况下调用 OpenSocial 函数 opensocial.newDataRequest(),则 Closure 编译器会将此调用转换为 a.b()

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

如果您有可重复使用的库的 JavaScript 代码,不妨使用 Closure Compiler 来缩减该库,同时仍允许未编译的代码调用该库中的函数。

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

这些外星人不仅对您自己,也十分实用。库的使用者在编译代码时需要包含这些库,因为您库的角度代表着一个外部脚本。您可以把这些外部对象想象成您与消费者之间的合约,都是一份副本。

为此,请确保在编译代码时,也在编译中包含 extern。这似乎不寻常,因为我们通常将 extern 视为“来自别处”,但我们需要告知 Closure 编译器您要公开的符号,以便不能重命名它们。

这里需要注意的一点是,您可能会获得与定义外部符号的代码有关的“重复定义”诊断信息。Closure Compiler 假定外部库中的任何符号都由外部库提供,并且目前无法理解您有意提供定义。这些诊断结果可以安全地进行抑制,您可以将抑制视为确认您确实在执行 API 的确认。

此外,Closure 编译器可能会对您的定义与 extern 声明的类型进行类型检查。这进一步确认您的定义正确无误。