進階編譯

總覽

與使用 SIMPLE_OPTIMIZATIONSWHITESPACE_ONLY 的編譯相比,使用 Closure Compiler 搭配 compilation_levelADVANCED_OPTIMIZATIONS 提供更好的壓縮速率。利用 ADVANCED_OPTIMIZATIONS 進行編譯能夠以更積極的方式轉換程式碼並重新命名符號,進而達到額外的壓縮效果。不過,這種更積極的做法意味著在使用 ADVANCED_OPTIMIZATIONS 時必須更加謹慎,以確保輸出程式碼的運作方式與輸入程式碼相同。

本教學課程說明 ADVANCED_OPTIMIZATIONS 編譯層級的用途,以及如何透過 ADVANCED_OPTIMIZATIONS 編譯後,確保程式碼運作正常。此外還介紹了 extern 的概念:這個符號會定義於編譯器處理的程式碼之外的外部程式碼。

在閱讀本教學課程之前,您應該已經熟悉使用 Closure Compiler 工具 (編譯器服務 UI編譯器服務 API編譯器應用程式) 編譯 JavaScript 的程序。

術語注意事項:--compilation_level 指令列旗標支援較常用的縮寫 ADVANCEDSIMPLE,以及較精確的 ADVANCED_OPTIMIZATIONSSIMPLE_OPTIMIZATIONS。 本文件使用較長的表單,但這些指令可在指令列中交替使用。

  1. 進一步壓縮
  2. 如何啟用 ADVANCED_OPTIMIZATIONS
  3. 使用 ADVANCED_OPTIMIZATIONS 時應注意的事項
    1. 移除您想保留的程式碼
    2. 屬性名稱不一致
    3. 單獨編譯兩組程式碼
    4. 編譯程式碼與未編譯程式碼之間的參照無效

壓縮效果更佳

預設編譯等級為 SIMPLE_OPTIMIZATIONS 時,Closure Compiler 會重新命名本機變數,藉此減少 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 Compiler 判斷,在這種情況下,內嵌函式會安全無虞且可節省空間。使用 ADVANCED_OPTIMIZATIONS 進行編譯時,也會在確定安全時進行內嵌和部分變數。

這份清單只是 ADVANCED_OPTIMIZATIONS 編譯可執行的大小縮減轉換範例。

如何啟用 ADVANCED_OPTIMIZATIONS

Closure Compiler 服務 UI、Service API 和應用程式都具備不同的將 compilation_level 設為 ADVANCED_OPTIMIZATIONS 的方法。

如何在 Closure Compiler 服務 UI 中啟用 ADVANCED_OPTIMIZATIONS

如要為 Closure Compiler 服務 UI 啟用 ADVANCED_OPTIMIZATIONS,請按一下 [進階] 圓形按鈕。

如何在 Closure Compiler 服務 API 中啟用 ADVANCED_OPTIMIZATIONS

如要為 Closure Compiler 服務 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 Compiler 應用程式中啟用 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 Compiler 會產生空白輸出:

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

由於您傳遞給編譯器的 JavaScript 中不會呼叫該函式,因此 Closure Compiler 會假設不需要這個程式碼!

在多數情況下,這個行為會符合您的需求。例如,如果您用大型程式庫編譯程式碼,Closure Compiler 可以確定您使用該程式庫中的函式,並捨棄未使用的函式。

不過,如果您發現 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 Compiler 看見該函式被呼叫。

換句話說,您可以在傳遞至 Closure Compiler 的程式碼中,加入程式的進入點,以防止不必要的程式碼移除。程式的進入點是程式在程式中執行的位置。舉例來說,在上一節的花朵記事程式中,瀏覽器將載入 JavaScript 後,最後 3 行就會立即執行。這是此程式的進入點。為判斷您需要保留的程式碼,Closure Compiler 從這個進入點開始,並從該程式追蹤程式的控制流程開始。

解決方案:包含您想公開的函式的適用範圍

如要進一步瞭解這項解決方案,請參閱下方外部與匯出頁面。

屬性名稱不一致

Closure Compiler 編譯絕不會變更程式碼中的字串常值,而不影響您使用的編譯層級。這表示進行 ADVANCED_OPTIMIZATIONS 編譯時,會以不同的方式處理程式碼,取決於您的程式碼是否使用字串存取屬性。如果您結合使用字串參照與屬性因此,您的程式碼可能無法正確執行。

以下列程式碼為例:

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) 時,才使用引用字串屬性的存取權。

單獨編譯兩個程式碼部分

如果您將應用程式拆分成不同的程式碼區塊,則建議您個別編譯區塊。不過,如果兩個區塊區塊完全發生互動,這麼做可能會導致作業困難。即使您成功解決問題,兩個 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 Compiler 會移除 getData() 函式,其原因為移除您想保留的程式碼。其次,Closure Compiler 在處理顯示資料的程式碼時會產生嚴重錯誤。

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

由於編譯器在編譯顯示資料的程式碼時無法存取 getData() 函式,因此會將 getData 視為未定義。

解決方案:結合單一網頁的所有程式碼

為確保經過正確的編譯,請在單一編譯執行作業中將所有頁面的所有程式碼編譯在一起。Closure Compiler 可接受多個 JavaScript 檔案和 JavaScript 字串做為輸入內容,方便您在單一編譯要求中同時傳遞程式庫程式碼和其他程式碼。

注意:如果需要混合經過編譯和未編譯的程式碼,就不適用這個方法。請參閱編譯和未編譯程式碼之間的參照參考資料,瞭解處理此情況的提示。

編譯與未編譯的程式碼之間的參照無效

ADVANCED_OPTIMIZATIONS 中的符號重新命名會使 Closure Compiler 處理的程式碼與其他任何程式碼之間的通訊中斷。編譯會重新命名原始碼中定義的函式。任何呼叫您的函式的外部程式碼在編譯後都會中斷,因為其仍然參照舊函式名稱。同樣地,Closure Compiler 也可以變更編譯程式碼中對外部定義符號的參照。

請注意,「未經編譯的程式碼」包括以字串形式傳送至 eval() 函式的任何程式碼。Closure Compiler 不會更改程式碼的字串常值,因此 Closure Compiler 不會變更傳遞至 eval() 陳述式的字串。

請注意,它們是相關但不同的問題:維護經過編譯至外部的通訊,以及維護外部到編譯的通訊。這些單獨的問題有共通的解決方法,但每邊都有細微差異。為充分運用 Closure Compiler,請務必瞭解您的情況為何。

建議您先參閱外部與匯出工具一文,然後再繼續操作。

透過編譯的程式碼呼叫外部程式碼的解決方案:使用外部編譯

如果您使用其他指令碼提供的程式碼,請務必確保 Closure Compiler 參照該外部程式庫所定義的符號重新命名。為此,請納入內含外部程式庫外部檔案的檔案,這會讓 Closure Compiler 知道您無法控制的名稱,因此無法變更。您的程式碼必須使用與外部檔案相同的名稱。

常見的範例包括 OpenSocial APIGoogle Maps API。例如,如果您的程式碼呼叫了 OpenSocial 函式 opensocial.newDataRequest(),且沒有適當的外來,Closure Compiler 會將此呼叫轉換為 a.b()

從外部程式碼呼叫編譯程式碼的解決方案:實作外部

如果您的 JavaScript 程式碼可重複使用為程式庫,建議您使用 Closure Compiler 僅縮小程式庫,同時允許未經編譯的程式碼呼叫程式庫中的函式。

在這種情況下,您可以實作一組程式庫,用以定義程式庫公開 API 的 API。您的程式碼將針對這些外部宣告的符號提供定義。這表示您的類別所提及的任何類別或函式。這也可能導致類別實作外部宣告的介面。

這些外人也能造福其他人,不僅限於你自己。程式庫的消費者在編譯程式碼時,必須納入這些程式碼,因為您的程式庫代表的是其外部的外部指令碼。請將外界視為你和消費者之間的合約。 兩者都需要一份複本。

為此,請確保您在編譯程式碼時,也在編譯中包含了外部。這看起來並不尋常,因為我們通常會將外界視為「從其他位置開始」的外界,但是需要告知 Closure Compiler 您要公開哪些符號,所以這些名稱不會被重新命名。

此處的一大重點是,您可能會對定義外部符號的程式碼執行「重複的定義」診斷。Closure Compiler 假設外部程式庫中的任何符號是由外部程式庫提供,但目前無法瞭解您有意提供定義。您可以放心略過這些診斷作業,而且您可以把略過操作視為是可以確實執行 API 的確認。

此外,Closure Compiler 也可以進行檢查,確認您的定義是否與外部宣告的類型相符。即可進一步確認您的定義是否正確。