總覽
使用 Closure 編譯器搭配 compilation_level
進行編譯,壓縮率會比使用 SIMPLE_OPTIMIZATIONS
或 WHITESPACE_ONLY
編譯更高。compilation_level
ADVANCED_OPTIMIZATIONS
使用 ADVANCED_OPTIMIZATIONS
進行編譯時,會以更積極的方式轉換程式碼和重新命名符號,進而達成額外的壓縮效果。不過,這種更積極的做法表示您在使用 ADVANCED_OPTIMIZATIONS
時必須更加謹慎,確保輸出程式碼的運作方式與輸入程式碼相同。
本教學課程將說明 ADVANCED_OPTIMIZATIONS
編譯層級的作用,以及如何確保程式碼在編譯後能正常運作。ADVANCED_OPTIMIZATIONS
此外,本節也會介紹「extern」的概念:這是指在編譯器處理的程式碼外部定義的符號。
閱讀本教學課程之前,您應該熟悉使用 Closure Compiler 工具 (例如以 Java 為基礎的編譯器應用程式) 編譯 JavaScript 的程序。
術語說明:--compilation_level
指令列旗標支援較常用的縮寫 ADVANCED
和 SIMPLE
,以及更精確的 ADVANCED_OPTIMIZATIONS
和 SIMPLE_OPTIMIZATIONS
。本文使用較長的格式,但這些名稱可在指令列上互換使用。
更優異的壓縮效果
在 SIMPLE_OPTIMIZATIONS
的預設編譯層級中,Closure 編譯器會重新命名區域變數,縮減 JavaScript 大小。不過,除了區域變數之外,還有其他符號可以縮短,而且除了重新命名符號之外,還有其他縮減程式碼的方法。使用 ADVANCED_OPTIMIZATIONS
進行編譯時,可充分運用程式碼縮減功能。
比較下列程式碼的 SIMPLE_OPTIMIZATIONS
和 ADVANCED_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 編譯器會產生空白輸出內容:
function displayNoteTitle(note) { alert(note['myTitle']); }
因為您傳遞給編譯器的 JavaScript 中從未呼叫該函式,Closure 編譯器會假設不需要這段程式碼!
在許多情況下,這正是您想要的行為。舉例來說,如果您將程式碼與大型程式庫一起編譯,Closure 編譯器可以判斷您實際使用的程式庫函式,並捨棄未使用的函式。
不過,如果您發現 Closure Compiler 移除了您想保留的函式,可以透過下列兩種方式避免這種情況:
- 將函式呼叫移至 Closure 編譯器處理的程式碼中。
- 加入要公開的函式適用的外部函式。
接下來的章節將詳細說明各個選項。
解決方法:將函式呼叫移至 Closure 編譯器處理的程式碼中
如果您只使用 Closure Compiler 編譯部分程式碼,可能會遇到不必要的程式碼移除作業。舉例來說,您可能有一個只包含函式定義的程式庫檔案,以及一個包含該程式庫的 HTML 檔案,其中含有呼叫這些函式的程式碼。在這種情況下,如果您使用 ADVANCED_OPTIMIZATIONS
編譯程式庫檔案,Closure 編譯器會移除所有程式庫函式。
解決這個問題最簡單的方法,就是將函式與呼叫這些函式的程式部分一起編譯。舉例來說,Closure 編譯器在編譯下列程式時,不會移除 displayNoteTitle()
:
function displayNoteTitle(note) { alert(note['myTitle']); } displayNoteTitle({'myTitle': 'Flowers'});
在本例中,系統不會移除 displayNoteTitle()
函式,因為 Closure Compiler 會發現該函式已呼叫。
換句話說,只要在傳遞至 Closure 編譯器的程式碼中加入程式的進入點,即可避免移除不必要的程式碼。程式的進入點是程式開始執行的程式碼位置。舉例來說,在上一節的花朵附註程式中,JavaScript 一載入瀏覽器,就會立即執行最後三行。這是這個程式的進入點。為判斷需要保留哪些程式碼,Closure 編譯器會從這個進入點開始,並從該處追蹤程式的控制流程。
解決方案:為要公開的函式加入 Externs
如要進一步瞭解這項解決方案,請參閱下文和外部人員與匯出頁面。
屬性名稱不一致
無論使用哪個編譯層級,Closure Compiler 編譯作業一律不會變更程式碼中的字串常值。也就是說,使用 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 編譯器重新命名時,才使用以引號括住的字串屬性存取權,例如名稱來自外部來源 (如已解碼的 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 編譯器變更。
請注意,「未編譯的程式碼」包括以字串形式傳遞至 eval()
函式的任何程式碼。Closure 編譯器絕不會變更程式碼中的字串常值,因此不會變更傳遞至 eval()
陳述式的字串。
請注意,這兩個問題相關但不同:維護編譯至外部的通訊,以及維護外部至編譯的通訊。這些問題的解決方法相同,但兩者之間仍有細微差異。如要充分發揮 Closure Compiler 的效用,請務必瞭解您屬於哪種情況。
繼續操作前,建議您先熟悉外部和匯出內容。
解決方案:從已編譯的程式碼呼叫外部程式碼:使用 Externs 編譯
如果您使用其他指令碼提供給網頁的程式碼,請務必確認 Closure 編譯器不會重新命名您對該外部程式庫中定義符號的參照。如要這麼做,請在編譯中加入包含外部程式庫外部項目的檔案。這樣 Closure Compiler 就會知道您無法控制哪些名稱,因此無法變更。程式碼必須使用與外部檔案相同的名稱。
常見的例子包括 OpenSocial API 和 Google 地圖 API。舉例來說,如果您的程式碼呼叫 OpenSocial 函式 opensocial.newDataRequest()
,但沒有適當的外部函式,Closure 編譯器就會將這個呼叫轉換為 a.b()
。
解決方案:從外部程式碼呼叫已編譯的程式碼:實作 Externs
如果您有重複使用的 JavaScript 程式碼 (做為程式庫),可能想使用 Closure Compiler 縮減程式庫,同時允許未編譯的程式碼呼叫程式庫中的函式。
在這種情況下,解決方法是實作一組定義程式庫公開 API 的外部函式。您的程式碼會為這些外部函式中宣告的符號提供定義。也就是說,外部人員提及的任何類別或函式。這也可能表示您的類別實作了 externs 中宣告的介面。
這些外部人員不僅對您有幫助,對其他人也一樣。程式庫的使用者編譯程式碼時,必須納入這些檔案,因為從使用者的角度來看,您的程式庫代表外部指令碼。請將外部人員視為您與消費者之間的合約,雙方都需要副本。
為此,請務必在編譯程式碼時,一併將外部函式庫納入編譯作業。這可能看起來很奇怪,因為我們通常會將外部項視為「來自其他地方」,但必須告訴 Closure 編譯器您要公開哪些符號,這樣這些符號才不會重新命名。
這裡有一項重要注意事項:您可能會收到有關定義外部符號的程式碼「重複定義」診斷結果。Closure Compiler 會假設外部程式庫提供 externs 中的任何符號,目前無法瞭解您是刻意提供定義。這些診斷資訊可以隱藏,隱藏動作可視為確認您確實要完成 API。
此外,Closure Compiler 也可能會進行型別檢查,確保定義符合外部宣告的型別。這有助於進一步確認定義是否正確。