您是否曾經希望在合併及縮減客戶端程式碼後,仍能保持程式碼的可讀性,並且更重要的是,在不會影響效能的情況下進行偵錯?您現在可以透過來源對應的魔力,輕鬆完成這項工作。
來源對應是一種將合併/經過壓縮的檔案對應回未建構狀態的方式。當您為正式版建構,並壓縮及合併 JavaScript 檔案時,系統會產生來源對應,其中會保留原始檔案的相關資訊。在產生的 JavaScript 中查詢特定行和欄號時,您可以在來源對應中查詢,並傳回原始位置。開發人員工具 (目前為 WebKit 夜間版本、Google Chrome 或 Firefox 23 以上版本) 可自動剖析原始檔案,讓您執行未經精簡和合併的檔案。
在這個示範中,您可以按一下滑鼠右鍵,在包含產生來源的文字方塊中點選任一位置。選取「取得原始位置」會傳入產生的行號和欄號,藉此查詢來源對應表,並傳回原始程式碼中的位置。請務必開啟主控台,以便查看輸出內容。
現實世界
查看下列實際的 Source Maps 實作前,請先確認您已在 Chrome Canary 或 WebKit 夜間版本中啟用 Source Maps 功能,方法是按一下開發人員工具面板中的設定齒輪圖示,然後勾選「啟用 Source Maps」選項。
Firefox 23 以上版本的內建開發人員工具預設啟用原始碼對照圖。
為什麼要使用來源對應?
目前,來源對應功能僅適用於未壓縮/合併的 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 程式碼。
來源對應的運作方式為何?
目前唯一支援產生來源對應圖的 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 檔案相關聯的來源對應。這個標頭也解決了在不支援單行註解的語言中參照來源圖的問題。
只有在您啟用來源對應功能並開啟開發人員工具時,系統才會下載來源對應檔案。您也必須上傳原始檔案,方便開發人員工具在必要時參照及顯示這些檔案。
如何產生來源對應圖?
您必須使用 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。有些可能看起來比較長,但這些是包含接續位元。每個區段都會建立在先前的區段上,因此每個位元都會與先前的區段相關,有助於縮減檔案大小。
如上所述,每個區段的長度可為 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) 的字元。
sourceURL
和 displayName
的運作方式:Eval 和匿名函式
雖然不是來源對應圖規格,但以下兩種慣例可讓您在使用 eval 和匿名函式時,更輕鬆地進行開發。
第一個輔助程式與 //# sourceMappingURL
屬性非常相似,實際上在來源對應 V3 規格中提及。只要在程式碼中加入下列特殊註解 (將進行評估),您就可以命名評估值,讓評估值在開發工具中顯示為更具邏輯性的名稱。請參閱使用 CoffeeScript 編譯器的簡單示範:
示範:透過 sourceURL 查看 eval()
'd 程式碼顯示為指令碼
//# sourceURL=sqrt.coffee
另一個輔助函式可讓您使用匿名函式的目前內容,使用 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
屬性,而非 (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 包在多行註解中,以減輕此問題。教訓:請勿使用條件式註解。
此問題已解決,語法已變更為 //#
。
工具和資源
以下是一些其他資源和工具,建議您查看:
- Nick Fitzgerald 有一個 UglifyJS 的分支版本,支援來源對應
- Paul Irish 有一個實用的示範,展示來源對應
- 查看 WebKit 變更集,瞭解這項功能何時遭到淘汰
- 變更集也包含版面配置測試,這也是本文的起點
- Mozilla 有一個錯誤,您應該在內建控制台中追蹤原始碼對照圖的狀態
- Conrad Irwin 為所有 Ruby 使用者編寫了非常實用的來源地圖 gem
- 進一步瞭解eval 命名和displayName 屬性
- 您可以查看 Closure Compiler 來源,瞭解如何建立來源對應
- 有幾張螢幕截圖和對話提到支援 GWT 來源地圖
在開發人員的工具組中,原始碼對照圖是一項非常強大的公用程式。讓網頁應用程式保持精簡,同時又能輕鬆進行偵錯,這點非常實用。對於新手開發人員來說,這也是非常實用的學習工具,可讓他們瞭解資深開發人員如何建構及編寫應用程式,而無須瀏覽難以閱讀的經過精簡的程式碼。
別再猶豫了,立即開始為所有專案產生來源圖!