JavaScript 來源對應簡介

Ryan Seddon

您是否曾經希望在合併及縮減客戶端程式碼後,仍能保持程式碼的可讀性,並且更重要的是,在不會影響效能的情況下進行偵錯?您現在可以透過來源對應的魔力,輕鬆完成這項工作。

來源對應是一種將合併/經過壓縮的檔案對應回未建構狀態的方式。當您為正式版建構,並壓縮及合併 JavaScript 檔案時,系統會產生來源對應,其中會保留原始檔案的相關資訊。在產生的 JavaScript 中查詢特定行和欄號時,您可以在來源對應中查詢,並傳回原始位置。開發人員工具 (目前為 WebKit 夜間版本、Google Chrome 或 Firefox 23 以上版本) 可自動剖析原始檔案,讓您執行未經精簡和合併的檔案。

在這個示範中,您可以按一下滑鼠右鍵,在包含產生來源的文字方塊中點選任一位置。選取「取得原始位置」會傳入產生的行號和欄號,藉此查詢來源對應表,並傳回原始程式碼中的位置。請務必開啟主控台,以便查看輸出內容。

範例:Mozilla JavaScript 來源對應程式庫的實際運作情形。

現實世界

查看下列實際的 Source Maps 實作前,請先確認您已在 Chrome Canary 或 WebKit 夜間版本中啟用 Source Maps 功能,方法是按一下開發人員工具面板中的設定齒輪圖示,然後勾選「啟用 Source Maps」選項。

如何在 WebKit 開發人員工具中啟用來源對應。

Firefox 23 以上版本的內建開發人員工具預設啟用原始碼對照圖。

如何在 Firefox 開發人員工具中啟用來源對應。

為什麼要使用來源對應?

目前,來源對應功能僅適用於未壓縮/合併的 JavaScript 與已壓縮/合併的 JavaScript,但未來前景看好,因為我們正在討論將其擴展至 CoffeeScript 等編譯為 JavaScript 的語言,甚至可能會新增對 SASS 或 LESS 等 CSS 前置處理器的支援。

日後,我們幾乎可以使用任何語言,就像瀏覽器原生支援的語言一樣,只要使用來源對應圖即可:

  • CoffeeScript
  • ECMAScript 6 以上版本
  • SASS/LESS 和其他
  • 幾乎所有編譯為 JavaScript 的語言

請觀看這部螢幕側錄,瞭解如何在 Firefox 控制台的實驗版本中偵錯 CoffeeScript:

Google Web Toolkit (GWT) 最近新增了支援來源圖。GWT 團隊的 Ray Cromwell 製作了精彩的螢幕錄影,展示實際運作中的原始碼對照圖支援功能。

我另外整合的另一個範例使用 Google 的 Traceur 程式庫,可讓您編寫 ES6 (ECMAScript 6 或 Next),並編譯為與 ES3 相容的程式碼。Traceur 編譯器也會產生來源對應圖。請參閱這個示範,瞭解如何使用 ES6 特徵和類別,就像瀏覽器原生支援這些特徵和類別一樣。

您也可以透過範例中的文字方塊編寫 ES6,系統會即時編譯並產生原始碼對應表和等效的 ES3 程式碼。

使用原始碼對應檔案進行 Traceur ES6 偵錯。

示範:編寫 ES6、進行偵錯,並查看實際執行中的來源對應

來源對應的運作方式為何?

目前唯一支援產生來源對應圖的 JavaScript 編譯器/壓縮器是 Closure 編譯器。(稍後會說明如何使用)。將 JavaScript 合併並縮減後,來源對應檔案就會隨之產生。

目前,Closure 編譯器不會在結尾處加入特殊註解,這類註解可向瀏覽器開發人員工具表示有可用的原始碼對照圖:

//# sourceMappingURL=/path/to/file.js.map

這樣開發人員工具就能將呼叫對應至原始來源檔案中的位置。先前的註解宣言為 //@,但由於該宣言和 IE 條件編譯註解出現一些問題,因此決定將其變更為 //#。目前 Chrome Canary、WebKit Nightly 和 Firefox 24 以上版本支援新的註解 Pragma。這項語法異動也會影響 sourceURL。

如果您不喜歡使用奇怪的註解,也可以在編譯的 JavaScript 檔案中設定特殊標頭:

X-SourceMap: /path/to/file.js.map

就像註解一樣,這會告訴來源對應消費者,要從何處尋找與 JavaScript 檔案相關聯的來源對應。這個標頭也解決了在不支援單行註解的語言中參照來源圖的問題。

WebKit 開發人員工具的範例,顯示開啟和關閉來源對應的情況。

只有在您啟用來源對應功能並開啟開發人員工具時,系統才會下載來源對應檔案。您也必須上傳原始檔案,方便開發人員工具在必要時參照及顯示這些檔案。

如何產生來源對應圖?

您必須使用 Closure 編譯器為 JavaScript 檔案縮減、串接及產生來源對應項目。指令如下:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

兩個重要的指令標記是 --create_source_map--source_map_format。這是必要步驟,因為預設版本是 V2,而我們只想使用 V3。

來源對應的架構

為了進一步瞭解原始檔對應,我們將以 Closure 編譯器產生的原始檔對應檔案為例,深入探討「mappings」部分的運作方式。以下範例與 V3 規格範例略有不同。

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

如上所示,來源對應項目是包含許多實用資訊的物件字面值:

  • 來源對應的版本號碼
  • 產生程式碼的檔案名稱 (您的 minifed/合併實際工作環境檔案)
  • sourceRoot 可讓您在來源前方加上資料夾結構,這也是節省空間的一種方法
  • sources 包含所有已合併的檔案名稱
  • names 包含程式碼中出現的所有變數/方法名稱。
  • 最後,mappings 屬性會使用 Base64 VLQ 值執行魔法。這就是真正的空間節省方式。

Base64 VLQ 和保持來源地圖小

原本,來源對應規格會針對所有對應項目輸出非常詳細的內容,導致來源對應檔案的大小約為產生的程式碼的 10 倍。版本 2 將這項數字減少約 50%,而版本 3 又將這項數字再減少 50%,因此對於 133 KB 的檔案,您最終會得到約 300 KB 的來源圖。

那麼,他們如何在縮減大小的同時,仍保留複雜的對應項目?

VLQ (可變長度數量) 會與將值編碼為 Base64 值的操作一起使用。對應屬性是超大的字串。這個字串中的分號 (;) 代表產生檔案中的行號。每個行內都有逗號 (,),代表該行中的每個片段。這些片段在可變長度欄位中為 1、4 或 5。有些可能看起來比較長,但這些是包含接續位元。每個區段都會建立在先前的區段上,因此每個位元都會與先前的區段相關,有助於縮減檔案大小。

來源對照圖 JSON 檔案中區段的細目。

如上所述,每個區段的長度可為 1、4 或 5,這張圖表的變數長度為四,且有一個接續位元 (g)。我們將分解這段文字,並說明來源對應表如何找出原始位置。

上述顯示的值純粹是 Base64 解碼值,還需要進一步處理才能取得實際值。每個區隔通常會計算五項資訊:

  • 產生的資料欄
  • 原始檔案
  • 原始行號
  • 原始欄
  • 以及原始名稱 (如有)

並非每個片段都有名稱、方法名稱或引數,因此片段的長度會在四到五個變數長度之間切換。上方區段圖表中的 g 值稱為接續位元,可在 Base64 VLQ 解碼階段進一步進行最佳化。接續位元可讓您建立區段值,因此您可以儲存大數字,而無須儲存大數字,這是一種非常聰明的省空間技術,源自於 MIDI 格式。

上圖 AAgBC 經過進一步處理後,會傳回 0, 0, 32, 16, 1 - 其中 32 是連續位元,可用於建立 16 的後續值。以 Base64 純解碼的 B 為 1。因此,重要的值為 0、0、16、1。這表示產生檔案的第 1 行 (行數以分號分隔) 的第 0 欄會對應至檔案 0 (檔案 0 的陣列是 foo.js),第 16 行位於第 1 欄。

為了說明如何解碼這些區段,我將參照 Mozilla 的 Source Map JavaScript 程式庫。您也可以查看 WebKit 開發人員工具的來源對應程式碼,這也是以 JavaScript 編寫。

為了正確瞭解如何從 B 取得值 16,我們需要對位元運算子有基本瞭解,以及瞭解規格如何在來源對應中運作。使用位元 AND (&) 運算子比較位元 (32) 和 VLQ_CONTINUATION_BIT (二進位 100000 或 32),將前一個位元 (g) 標示為接續位元。

32 & 32 = 32
// or
100000
|
|
V
100000

這會在兩個值都出現的位元位置傳回 1。因此,Base64 解碼的 33 & 32 值會傳回 32,因為它們只會共用 32 位元位置,如上圖所示。接著,每個前置的接續位元都會將位元移位值增加 5。在上述情況中,它只會向左移 5 位元一次,因此會將 1 (B) 向左移 5 位元。

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

然後將該值從 VLQ 帶符號值轉換,方法是將數字 (32) 向右移一位。

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

因此,這就是將 1 轉換為 16 的方法。這項程序看起來可能過於複雜,但當數字開始變大時,就會比較合理。

潛在的 XSSI 問題

規格提到,使用原始碼對照圖時可能會發生跨網站指令碼加入問題。為避免這種情況發生,建議您在來源對應的第一行前方加上「)]}」,刻意讓 JavaScript 失效,以便擲回語法錯誤。WebKit 開發人員工具已可處理這項問題。

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

如上所示,系統會切割前三個字元,檢查是否與規格中的語法錯誤相符,如果相符,則移除所有導致第一個新行實體 (\n) 的字元。

sourceURLdisplayName 的運作方式:Eval 和匿名函式

雖然不是來源對應圖規格,但以下兩種慣例可讓您在使用 eval 和匿名函式時,更輕鬆地進行開發。

第一個輔助程式與 //# sourceMappingURL 屬性非常相似,實際上在來源對應 V3 規格中提及。只要在程式碼中加入下列特殊註解 (將進行評估),您就可以命名評估值,讓評估值在開發工具中顯示為更具邏輯性的名稱。請參閱使用 CoffeeScript 編譯器的簡單示範:

示範:透過 sourceURL 查看 eval()'d 程式碼顯示為指令碼

//# sourceURL=sqrt.coffee
開發人員工具中 sourceURL 特殊註解的樣貌

另一個輔助函式可讓您使用匿名函式的目前內容,使用 displayName 屬性命名匿名函式。請分析以下示範,瞭解 displayName 屬性的實際運作情形。

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
顯示 displayName 屬性的實際運作情形。

在開發人員工具中剖析程式碼時,系統會顯示 displayName 屬性,而非 (anonymous) 之類的內容。不過,displayName 幾乎已無用武之地,不會納入 Chrome。不過,我們已提出更佳的建議,稱為 debugName

截至本文撰寫時,eval 命名僅適用於 Firefox 和 WebKit 瀏覽器。displayName 屬性僅適用於 WebKit 夜間版本。

一起集氣

目前,我們正在討論是否要為 CoffeeScript 新增來源對應支援。請查看這個問題,並提供意見,讓我們在 CoffeeScript 編譯器中加入原始碼產生功能。這對 CoffeeScript 和其忠實的追隨者來說,都是一大利多。

UglifyJS 也有來源對應問題,請一併查看。

許多工具會產生原始碼對照表,包括 CoffeeScript 編譯器。我認為現在這個問題已無討論必要。

我們可用的工具越多,產生來源圖的效果就越好,因此請繼續要求或為您喜愛的開放原始碼專案新增來源圖支援功能。

這並非完美解決方案

目前來源對應不支援的項目之一是 watch 運算式。問題是,嘗試在目前執行作業內容中檢查引數或變數名稱時,由於實際上不存在,因此不會傳回任何內容。這需要某種反向對應,才能查詢要檢查的引數/變數的實際名稱,並與編譯 JavaScript 中的實際引數/變數名稱進行比較。

這當然是一個可解決的問題,只要更重視原始碼對照圖,我們就能開始看到一些令人驚豔的功能和更穩定的系統。

問題

近期推出的 jQuery 1.9 新增了對來源對應檔案的支援功能,可在官方 CDN 外提供服務。它也指出,在 jQuery 載入前使用 IE 條件編譯註解 (//@cc_on) 時,會發生特殊錯誤。此後,我們已commit一項修正,將 sourceMappingURL 包在多行註解中,以減輕此問題。教訓:請勿使用條件式註解。

此問題已解決,語法已變更為 //#

工具和資源

以下是一些其他資源和工具,建議您查看:

在開發人員的工具組中,原始碼對照圖是一項非常強大的公用程式。讓網頁應用程式保持精簡,同時又能輕鬆進行偵錯,這點非常實用。對於新手開發人員來說,這也是非常實用的學習工具,可讓他們瞭解資深開發人員如何建構及編寫應用程式,而無須瀏覽難以閱讀的經過精簡的程式碼。

別再猶豫了,立即開始為所有專案產生來源圖!