線上產品代碼一般是編譯過的,前端的編譯處理過程包括不限於 轉譯器/Transpilers (Babel, Traceur) 編譯器/Compilers (Closure Compiler, TypeScript, CoffeeScript, Dart) 壓縮/Minifiers (UglifyJS) ...
線上產品代碼一般是編譯過的,前端的編譯處理過程包括不限於
這裡提及的都是可生成source map 的操作。 經過這一系列騷氣的操作後,發佈到線上的代碼已經面目全非,對帶寬友好了,但對開發者調試並不友好。於是就有了 source map。顧名思義,他是源碼的映射,可以將壓縮後的代碼再對應回未壓縮的源碼。使得我們在調試線上產品時,就好像在調試開發環境的代碼。 來看一個工作的示例準備兩個測試文件,一個 log.js function sayHello(name) { if (name.length > 2) { name = name.substr(0, 1) + '...' } console.log('hello,', name) } 一個 sayHello('世界') sayHello('第三世界的人們') 我們使用 npm install uglify-js -g
uglifyjs log.js main.js -o output.js --source-map "url='/output.js.map'"
安裝並執行後,我們得到了一個輸出文件 output.js function sayHello(name){if(name.length>2){name=name.substr(0,1)+"..."}console.log("hello,",name)}sayHello("世界");sayHello("第三世界的人們"); //# sourceMappingURL=/output.js.map output.js.map {"version":3,"sources":["log.js","main.js"],"names":["sayHello","name","length","substr","console","log"],"mappings":"AAAA,SAASA,SAASC,MACd,GAAIA,KAAKC,OAAS,EAAG,CACjBD,KAAOA,KAAKE,OAAO,EAAG,GAAK,MAE/BC,QAAQC,IAAI,SAAUJ,MCJ1BD,SAAS,MACTA,SAAS"} 為了能夠讓 source map 能夠被瀏覽器載入和解析,
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>source map demo</title> </head> <body> source map demo <script src="output.js"></script> </body> </html>
python3 -m http.server
在瀏覽器中開啟 source map 最後,就可以訪問 在壓縮過的代碼中打斷點 從截圖中可以看到,開啟 source map 後,除了頁面中引用的 為了測試,我們將 output.js 在調試工具中進行格式化,然後在 刷新頁面後,我們發現,斷點正確定位到了 代碼的還原 會否覺得很贊啊! 下麵我們來瞭解它的工作原理。 我們所想象的 source map將現實中的情況簡化一下無非是以下的場景:
上面,輸出無疑就是需要發佈到產品線上的瀏覽器能運行的代碼。這裡只討論 js,所以輸出是 js 代碼,當然,其實source map 也可以運用於其他資源比如 LESS/SASS 等編譯到的 CSS。 而 source map 的功能是幫助我們在拿到輸出後還原回輸入。如果我們自己來實現,應該怎麼做。 最直觀的想法恐怕是,將生成的文件中每個字元位置對應的原位置保存起來,一一映射。請看來自這篇文章中給出的示例:
一個簡單的文本轉換輸出,其中
這裡之所以將輸入文件也作為映射的必需值,它可以告訴我們從哪裡去找源文件。並且,在代碼合併時,生成輸出文件的源文件不止一個,記錄下每處代碼來自哪個文件,在還原時也很重要。 上面可以直觀看出,生成文件中 (1,0) 位置的字元對應源文件中 (1,5)位置的字元,...
這樣確實能夠將處理後的文件映射回原來的文件,但隨著內容的增多,轉換規則更加地複雜,這個記錄映射的編碼將飛速增長。這裡源文件 省去輸出文件中的行號大多數情況下處理後的文件行數都會少於源文件,特別是 js,使用 UglifyJS 壓縮後的文件通常只有一行。基於此,每必要在每條映射中都帶上輸出文件的行號,轉而在這些映射中插入
可符號化字元的提取這個例子中,一共有三個單詞,拿輸出文件中 所以,首先我們將文件中可符號化的字元提取出來,將他們作為整體來處理。
於是得到一個所有包含所有符號的數組:
在記錄時,只需要記錄一個索引,還原時通過索引來這個 所以
可以簡化為
同時,考慮到代碼經常會有合併打包的情況,即輸入文件不止一個,所以可以將輸入文件抽取一個數組,記錄時,只需要記錄一個索引,還原的時候再到這個數組中通過索引取出文件的位置及文件名即可。
所以上面
於是我們得到了完整的映射為:
記錄相對位置當文件內容巨大時,上面精簡後的編碼也有可能會因為數字位數的增加而變得很長,同時,處理較大數字總是不如處理較小數字容易和方便。於是考慮將上面記錄的這些位置用相對值來記錄。比如(1,1001)第一行第999列的符號,如果用相對值,我們就不用每次記錄都從0開始數,假如前一個符號位置為 (1,999),那後面這個符號可記錄為(0,2),類似這樣的相對值幫我們節省了空間,同時降低了數據的維度。 具體到本例中,看看最初的表格中,記錄的輸出文件中的位置:
對應到整個表格則是:
然後我們得到的編碼為:
註意
VLQ (Variable Length Quantities)進一步的優化則需要引入一個新的概念了,VLQ(Variable-length quantity)。 VLQ 以數字的方式呈現如果你想順序記錄4個數字,最簡單的辦法就是將每個數字用特殊的符號隔開:
如果如果提前告訴你這些被記錄的數字都是一位的,那這個分隔線就沒必要了,只需要簡單記錄成如下樣子也能被正確識別出來:
此時這個記錄值的長度是原來的1/2,省了不少空間。 但實際上我們不可能只記錄個位數的數字,使用 VLQ 方式時,如果一個數字後面還跟有剩餘數字,將其標識出來即可。假設我們想記錄如下的四個數字:
我們使用下劃線來標識一個數字後跟有其他數字: 1234567 所以解讀規則為:
VLQ 以二進位方式的方式呈現上面的示例中,引入了數字系統外的符號來標識一個數字還未結束。在二進位系統中,我們使用6個位元組來記錄一個數字(可表示至多64個值),用其中一個位元組來標識它是否未結束(正文 C 標識),不需要引入額外的符號,再用一位標識正負(下方 S),剩下還有四位用來表示數值。用這樣6個位元組組成的一組拼起來就可以表示出我們需要的數字串了。
第一個位元組組(四位作為值) 這樣一個位元組組可以表示的數字範圍為:
* -0 沒有實際意義,但技術上它是存在的 任意數字中,第一個位元組組中已經標明瞭該數字的正負,所以後續的位元組組中無需再標識,於是可以多出一位來作表示值。
未結束的位元組組(五位作為值) 現在我們使用上面的二進位規則來重新編碼之前的這個數字序列 先看每個數字對應的真實二進位是多少:
1需要一位來表示,還好對於首個位元組組,我們有四位來表示值,所以是夠用的。
23的二進位為10111一共需要5位,第一組位元組組只能提供4位來記錄值,所以用一組位元組組不行,需要使用兩組位元組組。將 10111拆分為兩組,後四位0111放入第一個位元組組中,剩下一位1放入第二個位元組組中。
456的二進位111001000需要占用9個位元組,同樣,一個位元組組放不下,先拆出最後四位(1000)放入一個首位位元組組中,剩下的5位(11100)放入跟隨的位元組組中。
3的二進位為111,首位位元組組能夠存放得下,於是編碼為:
將上面的編碼合併得到最終的編碼:
base64 編碼表 結合上面的 Base64 編碼表,上面的結果轉成對應的 base64 字元為:
利用 Base64 VLQ 編碼生成最終的 srouce map通過上面討論的方法,回到開始的示例中,前面我們已經得到的編碼為 sources: ['Yoda_input.txt'] names: ['the','force','feel'] mappings (31 字元): 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1; 現在來編碼
合併後的編碼為:
再轉 Base64 後得到字元形式的結果:
後面兩串數通過類似的做法也能得到對應的 Base64編碼,所以最終我們得到的 source map 看起來是這樣的:
而真實的 srouce map 如我們文章開頭那個示例一樣,是一個 json 文件,所以最後我們得到的一分像模像樣的 source map 為: { "version": 3, "file": "Yoda_output.txt", "sources": ["Yoda_input.txt"], "names": ["the", "force", "feel"], "mappings": "AACKA,IACIC,MACTC;" } 略去不必要的欄位上面的例子中,每一片段的編碼由五位組成。真實場景中,有些情況下某些欄位其實不必要,這時就可以將其省略。當然,這裡給出的這個例子看不出來。 省略其中某些欄位後,一個編碼片段就不一定是5位了,他的長度有可能為1,4或者5。
以上,便探究完了 srouce map 生成的全過程,瞭解了其原理。 如果感興趣,這個Source map visualizer tool 工具可以線上將 source map 與對應代碼可見化展現出來,方便理解。 另外需要介紹的是,儘管 source map 對於線上調試非常有用,各主流瀏覽器也實現對其的支持,但關於它的規範沒有見諸各 Web 工作組或團體的官方文檔中,它的規範是寫在一個 Google 文檔中的!這你敢信,不信去看一看嘍~ Source Map Revision 3 Proposal 相關資料
後續
|