JavaScript 來源對應簡介

Ryan Seddon

您是否曾希望在整合和壓縮後,仍可以讀取及壓縮用戶端程式碼,且程式碼可以更清晰易讀,同時不影響效能。現在,您可以運用我們實用的來源地圖功能了!

來源對應可用來將合併/縮小檔案對應至未建構狀態。當您在實際工作環境中建構、壓縮及合併 JavaScript 檔案時,會產生來源對應,其中包含原始檔案的相關資訊。當您在產生的 JavaScript 中查詢特定的行和欄號時,可以在來源對應中查詢並傳回原始位置。開發人員工具 (目前為 WebKit 夜間版本、Google Chrome 或 Firefox 23 以上版本) 可以自動剖析來源對應,看起來就像執行未合併且未合併的檔案一樣。

在含有所產生來源的文字區域中,您可以在任何位置按一下滑鼠右鍵。選取「取得原始位置」,就會傳入產生的行和欄號來查詢來源對應,並傳回原始程式碼中的位置。必須開啟主控台,才能看到輸出內容。

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

現實世界

查看以下的原始碼地圖實際實作之前,請確認您已在 Chrome Canary 或 WebKit 夜間啟用來源對應功能,方法是按一下開發人員工具面板中的設定齒輪圖示,然後勾選 [啟用來源地圖] 選項。

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

Firefox 23+ 內建開發工具,預設啟用來源地圖。

如何在 Firefox 開發工具中啟用來源地圖

為何應該關注來源對應?

目前來源對應僅適用於未壓縮/合併的 JavaScript 以及壓縮/未合併的 JavaScript,但未來我們相信 CoffeeScript 等已編譯對 JavaScript 語言的對話,也有機會為 SASS 或 LESS 等 CSS 預先處理器提供支援。

日後,我們能輕鬆使用幾乎任何語言,就像瀏覽器內建的來源地圖一樣,都能使用:

  • CoffeeScript
  • ECMAScript 6 以上版本
  • 不/包含其他項目
  • 絕大多數能編譯為 JavaScript 的語言

請查看以下的 CoffeeScript 螢幕側錄,這是在 Firefox 控制台的實驗性版本中進行偵錯:

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。這是預設版本 (第 2 版),而我們只想與 V3 版本搭配使用。

來源對應的剖析

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

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

如上所述,來源地圖是包含大量果汁資訊的物件常值:

  • 來源對應的版本編號
  • 所產生程式碼的檔案名稱 (您的剪輯/合併正式環境檔案)
  • sourceRoot 能讓您在來源前面加上資料夾結構,這也是一種節省空間的技術
  • 來源包含合併的所有檔案名稱
  • 名稱包含在整個程式碼中顯示的所有變數/方法名稱。
  • 最後,對應屬性是使用 Base64 VLQ 值產生神奇效果。真正省下的空間就在這裡。

Base64 VLQ 並讓來源對應保持精簡

原先的來源對應規格原本含有所有對應項目非常詳細的輸出內容,因此來源對應相當於所產生程式碼大小的 10 倍。第二版縮減了約 50%,第 3 版則再次降低了 50%,所以您要建立 133 kB 檔案時,最終會得到約 300kB 的來源對應。

那麼,他們如何在維持複雜的對應,同時縮減大小呢?

VLQ (變數長度數量) 可與值編碼為 Base64 值。對應屬性是超大型字串。這個字串有分號 (;),代表產生的檔案中的行數。每一行中都有逗號 (,) 來代表該行中的每個區隔。這些區隔各為變數長度欄位中的 1、4 或 5。有些畫面可能較長,但包含連續位元。每個區隔都是以上一個片段為基礎,因此會減少檔案大小,因為每個位元都是相對於先前的片段。

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

如上所述,每個區隔的長度可能都是 1、4 或 5。此圖視為一個可變長度四項,以及一個連續位元 (g)。我們將細分這個線段,並顯示來源對應如何與原始位置搭配運作。

上列顯示的值只是 Base64 解碼值,還需完成一些處理作業才能取得真實值。每個區隔通常可以分為五件事:

  • 產生的資料欄
  • 出現在下列位置的原始檔案:
  • 原始行號
  • 原始資料欄
  • 原始的名稱 (如有)

並非所有區隔都有名稱、方法名稱或引數,因此片段長度會介於 4 到 5 個變數長度之間。上區段圖中的 g 值稱為連續位元,這可讓您在 Base64 VLQ 解碼階段進一步進行最佳化。連續位元可讓您根據區隔值建立資料,儲存大量數字時,不必儲存大量數值;這是個非常聰明的空間儲存技巧,以 Mii 格式做為根基。

上圖 AAgBC 經過進一步處理後,會傳回 0、0、32、16、1 - 32 是連續位元,有助於建構下列 16 的值。B 純粹在 Base64 中解碼為 1,因此,應使用的重要值為 0、0、16、1。如此一來,即可知道所產生檔案的第 1 行 (行數是以半形分號) 欄 0 對應至檔案 0 (檔案 0 的陣列為 foo.js),第 16 行位於第 16 行。

為了示範區隔的解碼方式,請參閱 Mozilla 的來源地圖 JavaScript 程式庫。也可以查看 WebKit 開發工具的來源對應程式碼 (同樣以 JavaScript 編寫)。

為了正確瞭解我們如何從 B 中獲得 16 值,我們需要對位元運算子以及規格對於來源對應的運作方式有基本瞭解。使用位元 AND (&) 運算子比較數字 (32) 與 VLQ_CONTINUATION_BIT (二進位 100000 或 32),系統會將上述數字 g 標記為連續位元。

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

這樣做會在每個位元位置傳回 1。因此,33 & 32 解碼的 Base64 值只會傳回 32,因為它們只會共用 32 位元位置,如上圖所示。接著,針對每個連續位元,將位元 shift 值增加 5。在上述案例中,水平位移 5 次,因此左移動 1 (B) 變為 5。

1 <<../ 5 // 32

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

接著,只要將數字 (32) 向右移,即可從 VLQ 簽署值中轉換該值。

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

以上就是你 16 歲這個方式!這個過程看似複雜,但一旦數量開始變大,應該會更有意義。

潛在 XSSI 問題

規格中提到,使用來源對應時可能會發生跨網站指令碼納入問題。為減緩此問題,建議您在來源對應的開頭前面加上「)]}」,刻意將 JavaScript 失效,讓系統擲回語法錯誤。WebKit 開發工具可以處理這項工作。

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

如上所示,前三個字元會切割確認是否符合規格中的語法錯誤;若是如此,便會移除導致第一個新行實體 (\n) 的所有字元。

sourceURLdisplayName 實際使用:評估和匿名函式

雖然不是來源對應規格的一部分,但以下兩個慣例可讓您更輕鬆地使用 eval 和匿名函式來開發開發作業。

第一項輔助工具與 //# sourceMappingURL 屬性非常類似,實際上是在來源對應 V3 規格中提及。在程式碼中加入以下即將節錄的特殊註解,您就能為 eval 命名,讓它們在開發工具中會顯示為更邏輯名稱。查看以下使用 CoffeeScript 編譯器的簡易示範:

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

//# 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

撰寫密碼命名時,只能使用 Firefox 和 WebKit 瀏覽器。displayName 屬性僅適用於 WebKit 夜間版本。

讓我們同樂

目前 CoffeeScript 中有許多關於來源地圖支援的討論。查看問題,並支援將來源對應產生功能新增至 CoffeeScript 編譯器。這將為 CoffeeScript 及其忠實追蹤者帶來莫大助益。

UglifyJS 也有來源地圖問題,您也應留意。

許多tools都會產生來源對應,包括咖啡指令碼編譯器。我現在認為這是個中格點。

我們可用的工具越多,能夠產生來源地圖,我們就能更棒,因此歡迎您隨時向自己最愛的開放原始碼專案提出來源地圖支援,或是要求我們支援來源對應功能。

不太完美

有一個事情來源地圖目前不支援「觀察運算式」。問題是,如果嘗試在目前的執行情境中檢查引數或變數名稱,不會傳回任何不存在的結果,因此需要進行某種反向對應,來查詢您要檢查的引數/變數的實際名稱,並與編譯的 JavaScript 中的實際引數/變數名稱進行比較。

這堂課不僅是一個解決的問題,也對來源地圖有更多的關注,我們就可以開始看到一些令人驚豔的功能,以及更穩定的穩定性。

問題

最近 jQuery 1.9 開始支援非主流 CDN 時的來源對應,在 jQuery 載入前使用 IE 條件編譯註解 (//@cc_on) 時,也有重大錯誤。我們一直做出修訂,用來在多行註解中納入 sourceMappingURL。要學習的課程,請勿使用條件式註解。

自從將語法變更為 //# 後,我們就解決了這個問題。

工具與資源

以下提供幾項額外資源和工具:

來源地圖是開發人員工具組中強大的公用程式,確保網頁應用程式精簡且容易偵錯。此外,它也是功能非常強大的學習工具,可讓開發人員瞭解開發人員如何建構及編寫應用程式,而不必費心處理無法讀取的壓縮程式碼。

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