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 以上版本均支援新的註解區塊。這項語法變更也會影響 sourceURL。

如果您不喜歡這個奇怪的留言,可以在編譯後的 JavaScript 檔案中設定特殊標頭:

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

就像註解一樣,可讓您的來源地圖用戶端尋找與 JavaScript 檔案相關聯的來源地圖。這個標頭也會說明在不支援單行註解的語言中參照來源對應的問題。

WebKit Devtools 範例:來源對應和來源對應。

只有在已啟用來源對應且開發工具開啟時,才會下載來源對應檔案。您還需要上傳原始檔案,開發工具才能參照及顯示必要檔案。

如何產生來源對應?

您必須使用 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 編譯器產生的來源對應檔案範例,並進一步說明「對應」部分的運作方式。以下範例是與 V3 規格示例中的細微差異。

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

如上圖所示,來源對應是物件常值,包含大量煙火資訊:

  • 來源對應的版本號碼
  • 產生的程式碼檔案名稱 (完成壓縮/合併的實際檔案)
  • sourceRoot 可讓您在資料夾結構前面加上來源,這也是一種節省空間的技巧
  • source 會列出所有已合併的檔案名稱
  • 名稱包含整個程式碼中的所有變數/方法名稱。
  • 最後,Mappings 屬性是使用 Base64 VLQ 值產生神奇效果的地方。立即儲存實際空間。

Base64 VLQ 並保持來源地圖的尺寸

最初,來源對應規格輸出所有對應關係後,產生了相當詳細的輸出內容,因此來源對應造成來源對應大小,是產生程式碼大小的 10 倍。第二版減少了 50% 左右,版本 3 又減少了 50%。因此,133 kB 的檔案會得到約 300 KB 的來源對應。

那麼他們如何縮減尺寸,同時保持複雜的對應?

VLQ (可變長度數量) 會與將值編碼為 Base64 值搭配使用。對應屬性是超大型字串。在這個字串中,分號 (;) 代表產生的檔案中的行數。每行都有半形逗號 (,),代表該線條內的每個區隔。這些區隔在可變長度欄位中各為 1、4 或 5。有些則可能較長,但內含連續位元。每個區隔都是以前一個區隔為基礎建立而成,這種做法有助於縮減檔案大小,因為每個位元均與先前區隔相關。

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

如上所述,每個區隔的長度可變為 1、4 或 5。此圖表被視為可變長度為 4,還有一個接續位元 (g)。我們會詳細介紹此區段,並顯示來源對應的運作方式。

上方顯示的值只是 Base64 解碼值,還需要經過處理才能取得真正的值。每個區隔的彙整依據通常有五項:

  • 產生的資料欄
  • 出現這個原始檔案的原始檔案
  • 原始行號
  • 原始資料欄
  • (如果有的話) 原始名稱

不是每個區隔都有名稱、方法名稱或引數,因此整個區隔會在 4 到 5 個變數長度之間切換。上方區隔圖中的 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 的來源對應 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 位元的位置,如上圖所示。這樣一來,每個前一個接續位元的位元偏移值就會增加 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

簡單來說,就是 1 變成 16 了。這個過程看似複雜,但只要數據越大,就會更加合理。

潛在的 XSSI 問題

規格提到使用來源對應,因而導致的跨網站指令碼包含問題。為減緩此問題,建議您在來源對應的第一行前面加上「)]}」,藉此刻意使 JavaScript 失效,讓系統擲回語法錯誤。WebKit 開發人員工具已能處理這個問題。

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

如上所示,前三個字元已進行分割,檢查這些字元是否與規格中的語法錯誤相符;如果是的話,請移除第一個新行實體 (\n) 前的所有字元。

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

雖然來源對應規格並未遵循下列兩個慣例,但可讓您在使用評估和匿名函式時,大幅簡化開發作業。

第一個輔助程式看起來與 //# sourceMappingURL 屬性非常類似,實際上是在來源對應 V3 規格中提及。在程式碼中加入以下特殊註解並加以評估,即可為評估項目命名,以便在開發人員工具中呈現更符合邏輯的名稱。以下是使用 CoffeeScript 編譯器的簡易示範:

示範:透過 sourceURL 將 eval() 的程式碼顯示為指令碼

//# 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都能產生來源對應,包括 Cupscript 編譯器。我現在想到這個點子。

現有越多工具提供給我們,就能更完善地產生來源地圖。因此,在您最愛的開放原始碼專案裡,您可以尋求或新增來源地圖支援。

非常不完美

目前,來源對應還不支援監控運算式。問題在於,嘗試檢查目前執行環境中的引數或變數名稱不會傳回任何內容,因為它實際上不存在。這就需要某種反向對應,才能比對想檢查的引數/變數的實際名稱,以及已編譯 JavaScript 中的實際引數/變數名稱。

當然,這是一個可解決的問題,而且來源地圖越受關注,我們就能開始看到一些令人驚豔的功能,以及更加穩定的穩定性。

問題

最近 jQuery 1.9 開始在離線 CDN 提供時對來源對應的支援。並在 jQuery 載入前使用 IE 條件編譯註解 (//@cc_on) 時,也指出發生錯誤錯誤。為了緩解這種情況,我們已修訂,將 sourceMappingURL 納入多行註解中。學習課程請不要使用條件式註解。

因此,我們已解決將語法變更為 //# 之後會發生的問題。

工具與資源

建議您參考下列額外資源和工具:

來源對應是開發人員工具集中強大的工具。將網頁應用程式保持精簡,但可輕鬆進行偵錯,是非常有用的工具。此外,這對剛起步的開發人員來說,也是十分強大的學習工具。透過這項工具,開發人員可以查看有經驗的開發人員建構應用程式,並撰寫自己的應用程式,完全不必徹底處理無法讀取的壓縮程式碼。

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