進階編譯

總覽

使用 Closure 編譯器搭配 compilation_level 進行編譯,壓縮率會比使用 SIMPLE_OPTIMIZATIONSWHITESPACE_ONLY 編譯更高。compilation_levelADVANCED_OPTIMIZATIONS使用 ADVANCED_OPTIMIZATIONS 進行編譯時,會以更積極的方式轉換程式碼和重新命名符號,進而達成額外的壓縮效果。不過,這種更積極的做法表示您在使用 ADVANCED_OPTIMIZATIONS 時必須更加謹慎,確保輸出程式碼的運作方式與輸入程式碼相同。

本教學課程將說明 ADVANCED_OPTIMIZATIONS 編譯層級的作用,以及如何確保程式碼在編譯後能正常運作。ADVANCED_OPTIMIZATIONS此外,本節也會介紹「extern」的概念:這是指在編譯器處理的程式碼外部定義的符號。

閱讀本教學課程之前,您應該熟悉使用 Closure Compiler 工具 (例如以 Java 為基礎的編譯器應用程式) 編譯 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 編譯器會產生空白輸出內容:

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 APIGoogle 地圖 API。舉例來說,如果您的程式碼呼叫 OpenSocial 函式 opensocial.newDataRequest(),但沒有適當的外部函式,Closure 編譯器就會將這個呼叫轉換為 a.b()

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

如果您有重複使用的 JavaScript 程式碼 (做為程式庫),可能想使用 Closure Compiler 縮減程式庫,同時允許未編譯的程式碼呼叫程式庫中的函式。

在這種情況下,解決方法是實作一組定義程式庫公開 API 的外部函式。您的程式碼會為這些外部函式中宣告的符號提供定義。也就是說,外部人員提及的任何類別或函式。這也可能表示您的類別實作了 externs 中宣告的介面。

這些外部人員不僅對您有幫助,對其他人也一樣。程式庫的使用者編譯程式碼時,必須納入這些檔案,因為從使用者的角度來看,您的程式庫代表外部指令碼。請將外部人員視為您與消費者之間的合約,雙方都需要副本。

為此,請務必在編譯程式碼時,一併將外部函式庫納入編譯作業。這可能看起來很奇怪,因為我們通常會將外部項視為「來自其他地方」,但必須告訴 Closure 編譯器您要公開哪些符號,這樣這些符號才不會重新命名。

這裡有一項重要注意事項:您可能會收到有關定義外部符號的程式碼「重複定義」診斷結果。Closure Compiler 會假設外部程式庫提供 externs 中的任何符號,目前無法瞭解您是刻意提供定義。這些診斷資訊可以隱藏,隱藏動作可視為確認您確實要完成 API。

此外,Closure Compiler 也可能會進行型別檢查,確保定義符合外部宣告的型別。這有助於進一步確認定義是否正確。