深刻理解SourceMap

一 Source Map是什麼?

Source Map,顧名思義,是保存源代碼映射關係的文件,相信用過webpack的開發者對它應該不會陌生。在項目開發完進行打包後,在打包的文件夾裏一般除了js,css,圖片字體等資源文件外,你們必定還見過xxx.js.map的文件。這種帶map後綴的文件就是Source Map文件——它保存了源代碼和轉換以後代碼(一般通過壓縮混淆和其餘轉換)的關係。 下圖展現了部分打包以後生成的Source Map文件:css

WX20190614-151320.png

下面是一個典型的Source Map文件的格式:前端

{
  "version": 3,
  "sources": [
    "log.js",
    "main.js"
  ],
  "names": [
    "sayHello",
    "name",
    "length",
    "substr",
    "console",
    "log"
  ],
  "mappings": "AAAA,SAASA,SAAUC,MACjB,GAAIA,KAAKC,OAAS,EAAG,CACnBD,KAAOA,KAAKE,OAAO,EAAG,GAAK,MAE7BC,QAAQC,IAAI,QAASJ,MCJvBD,SAAS"
}
複製代碼

二 爲何使用Source Map?

明白了什麼是Source Map以後,你們確定有個疑問,咱們爲何須要Source Map? 因爲現代前端項目的發展,前端代碼變得愈來愈龐大和複雜。大部分源碼都須要通過轉換,才能投入到生產環境中使用。 常見的轉換過程包括但不限於:webpack

  • 壓縮混淆(UglifyJS)
  • 編譯(TypeScript, CoffeeScript)
  • 轉譯(Babel)
  • 合併多個文件,減小帶寬請求。

通過上述轉換過程的代碼,每每都會變得面目全非,就像下面這樣:web

WX20190614-154341.png
這樣雖然對帶寬很友好,可是調試起來就不是那麼輕鬆了。咱們在代碼出錯的時候,確定最但願能定位其在源碼中的位置。 好比下面這兩個錯誤提示:
WX20190614-160302.png
WX20190614-160413.png
對於大多數開發者來講,但願看到的應該是第二種提示方式,而這就是Source Map可以輸出的能力。

三 Source Map是怎麼實現映射的?

在探索這個問題以前,能夠先想一想真實世界裏對這種語言轉換是怎麼作的?數組

I AM CHRIS ——> Map ——> CHRIS I AM
複製代碼

如今咱們要從CHRIS I AM還原到I AM CHRIS,Map裏應該存儲哪些信息呢?bash

3.1 最簡單粗暴的方法

將輸出文件中每一個字符位置對應在輸入文件名中的原位置保存起來,並一一進行映射。上面的這個映射關係應該獲得下面的表格:app

字符 輸出位置 在輸入中的位置 輸入的文件名
c 行1,列1 行1,列6 輸入文件1.txt
h 行1,列2 行1,列7 輸入文件1.txt
r 行1,列3 行1,列8 輸入文件1.txt
i 行1,列4 行1,列9 輸入文件1.txt
s 行1,列5 行1,列10 輸入文件1.txt
i 行1,列7 行1,列1 輸入文件1.txt
a 行1,列9 行1,列3 輸入文件1.txt
m 行1,列10 行1,列4 輸入文件1.txt

備註: 因爲輸入信息可能來自多個文件,因此這裏也同時記錄輸入文件的信息。函數

將上面表格整理成映射表的話,看起來就像這樣(使用"|"符號分割字符)字體

mappings: "1|1|輸入文件1.txt|1|6,1|2輸入文件1.txt|1|7,1|3|輸入文件1.txt|1|8,1|4|輸入文件1.txt|1|9,1|5|輸入文件1.txt|1|10,1|7|輸入文件1.txt|1|1,1|9|輸入文件1.txt|1|3,1|10|輸入文件1.txt|1|4"(長度:144複製代碼

這種方法確實能將處理後的內容還原成處理前的內容,可是隨着內容的增長,轉換規則的複雜,這個編碼表的記錄將飛速增加。目前僅僅10個字符,映射表的長度已經達到了144個字符。如何進一步優化這個映射表呢?優化

備註: mappings: "輸出文件行位置|輸出文件列位置|輸入文件名|輸入文件行號|輸入文件列號,....."

3.2 優化手段1:不要輸出文件中的行號

在經歷過壓縮和混淆以後,代碼基本上不會有多少行(特別是JS,一般只有1到2行)。這樣的話,就能夠在上節的基礎上移除輸出位置的行數,使用";"號來標識新行。 那麼映射信息就變成了下面這樣

mappings: "1|輸入文件1.txt|1|6,2|輸入文件1.txt|1|7,3|輸入文件1.txt|1|8,4|輸入文件1.txt|1|9,5|輸入文件1.txt|1|10,7|輸入文件1.txt|1|1,9|輸入文件1.txt|1|3,10|輸入文件1.txt|1|4; 若是有第二行的話"(長度:129複製代碼

備註: mappings: "輸出文件列位置|輸入文件名|輸入文件行號|輸入文件列號,....."

3.3 優化手段2:提取輸入文件名

因爲可能存在多個輸入文件,且描述輸入文件的信息比較長,因此能夠將輸入文件的信息存儲到一個數組裏,記錄文件信息時,只記錄它在數組裏的索引值就行了。 通過這步操做後,映射信息以下所示:

sources: ['輸入文件1.txt'],
mappings: "1|0|1|6,2|0|1|7,3|0|1|8,4|0|1|9,5|0|1|10,7|0|1|1,9|0|1|3,10|0|1|4;" (長度:65)
複製代碼

通過轉換後mappings字符數從129降低到了65。

備註: mappings: "輸出文件列位置|輸入文件名索引|輸入文件行號|輸入文件列號,....."

3.4 優化手段3: 可符號化字符的提取

通過上一步的優化,mappings字符數有了很大的降低,可見提取信息是一個頗有用的簡化手段,那麼還有什麼信息是可以提取的麼? 固然。已輸出文件中的Chris字符爲例,當咱們找到了它的首字符C在源文件中的位置(行1,列6)時,就不須要再去找剩下的hris的位置了,由於Chris能夠做爲一個總體來看待。想一想源碼裏的變量名,函數名,都是做爲一個總體存在的。 如今能夠把做爲總體的字符提取並存儲到一個數組裏,而後和文件名同樣,在mapping裏只記錄它們的索引值。這樣就避免了每個字符都要記的窘境,大大縮減mappings的長度。

添加一個包含全部可符號化字符的數組:

names: ['I', 'am', 'Chris']
複製代碼

那麼以前Chris的映射就從

1|0|1|6,2|0|1|7,3|0|1|8,4|0|1|9,5|0|1|10
複製代碼

變成了

1|0|1|6|2
複製代碼

最終的映射信息變成了:

sources: ['輸入文件1.txt'],
names: ['I', 'am', 'Chris'],
mappings: "1|0|1|6|2,7|0|1|1|0,9|0|1|3|1" (長度: 29)
複製代碼

備註: 1. 「I am Chris"中的"I"抽出來放在數組裏,其實意義不大,由於它自己也就只有一個字符。可是爲了演示方便,因此拆出來放在數組裏。 2. mappings: "輸出文件列位置|輸入文件名索引|輸入文件行號|輸入文件列號|字符索引,....."

3.5 優化手段4: 記錄相對位置

前面記錄位置信息(主要是列)時,記錄的都是絕對位置信息,設想一下,當文件內容比較多時,這些數字可能會變的很大,這個問題怎麼解決呢? 能夠經過只記錄相對位置來解決這個問題(除了第一個字符)。 來看一下具體怎麼實現的,以以前的mappings編碼爲例:

mappings: "1(輸出列的絕對位置)|0|1|6(輸入列的絕對位置)|2,7(輸出列的絕對位置)|0|1|1(輸入列的絕對位置)|0,9(輸出列的絕對位置)|0|1|3(輸入列的絕對位置)|1"
複製代碼

轉換成只記錄相對位置

mappings: "1(輸出列的絕對位置)|0|1|6(輸入列的絕對位置)|2,6(輸出列的相對位置)|0|1|-3(輸入列的相對位置)|0,2(輸出列的相對位置)|0|1|-2(輸入列的絕對位置)|1"
複製代碼

從上面的例子可能看不太出這個方法的好處,可是當文件慢慢大起來,使用相對位置能夠節省不少字符長度,特別是對於記錄輸出文件列信息的字符來講。

3.6 優化手段5: VLQ編碼

通過上面幾步操做以後,如今最應該優化的地方應該就是用來分割數字的"|"號了。 這個優化應該怎麼實現呢? 在回答以前,先來看這樣一個問題——若是你想順序的記錄4組數字,最簡單的就是用"|"號進行分割。

1|2|3|4
複製代碼

若是每一個數字只有1位的話,能夠直接表示成

1234
複製代碼

可是不少時候每一個數字不止有1位,好比

12|3|456|7
複製代碼

這個時候,就必定得用符號把各個數字分割開,像咱們上面例子中同樣。還有好的方法嘛? 經過VLQ編碼的方式,你能夠很好的處理這種狀況,先來看看VLQ的定義:

3.6.1 VLQ定義

A variable-length quantity (VLQ) is a universal code that uses an arbitrary number of binary octets (eight-bit bytes) to represent an arbitrarily large integer. 翻譯一下:VLQ是用任意個2進制字節組去表示一個任意數字的編碼形式。

VLQ的編碼形式不少,這篇文章中要說明的是下面這種:

WX20190615-165403.png

  • 一個組包含6個二進制位。
  • 在每組中的第一位C用來標識其後面是否會跟着另外一個VLQ字節組,值爲0表示其是最後一個VLQ字節組,值爲1表示後面還跟着另外一個VLQ字節組。
  • 在第一組中,最後1位用來表示符號,值爲0則表示正數,爲1表示負數。其餘組的最後一位都是表示數字。
  • 其餘組都是表示數字。

這種編碼方式也稱爲Base64 VLQ編碼,由於每個組對應一個Base64編碼。

3.6.2 小例子說明VLQ

如今咱們用這套VLQ編碼規則對12|3|456|7進行編碼,先將這些數字轉換爲二進制數。

12  ——> 1100
3   ——> 11
456 ——> 111001000
7   ——> 111
複製代碼
  • 對12進行編碼

12須要1位表示符號,1位表示是否延續,剩下的4位表示數字

B5(C) B4 B3 B2 B1 B0
0 1 1 0 0 0
  • 對3進行編碼
B5(C) B4 B3 B2 B1 B0
0 0 0 1 1 0
  • 對456進行編碼

從轉換關係中可以看到,456對應的二進制已經超過了6位,用1組來表示確定是不行的,這裏須要用兩組字節組來表示。先拆除最後4個數(1000)放入第一個字節組,剩下的放在跟隨字節組中。

B5(C) B4 B3 B2 B1 B0 B5(c) B4 B3 B2 B1 B0
1 1 0 0 0 0 0 1 1 1 0 0
  • 對7進行編碼
B5(C) B4 B3 B2 B1 B0
0 0 1 1 1 0

最後獲得下列VLQ編碼:

011000 000110 110000 011100 001110
複製代碼

經過Base64進行轉換以後:

bg2013012202 (1).png
最終獲得下列結果:

YGwcO
複製代碼

3.6.3 轉換以前的例子

經過上面這套VLQ的轉換流程轉換以前的例子,先來編碼1|0|1|6|2. 轉換成VLQ爲:

1 ——> 1(二進制) ——> 000010(VLQ)
0 ——> 0(二進制) ——> 000000(VLQ)
1 ——> 1(二進制) ——> 000010(VLQ)
6 ——> 110(二進制) ——> 001100(VLQ)
2 ——> 10(二進制) ——> 000100(VLQ)
複製代碼

合併後編碼爲

000010 000000 000010 001100 000100
複製代碼

轉換成Base64

BABME
複製代碼

其餘也是按這種方式編碼,最後獲得的mapping文件以下:

sources: ['輸入文件1.txt'],
names: ['I', 'am', 'Chris'],
mappings: "BABME,OABBA,SABGB" (長度: 17)
複製代碼

和第一節的mappings文件對比同樣,是否是同樣呢?

3.6.4 one more thing

在真實場景中,咱們在mappings中常常能夠看到不是5位的字符。大於5位好理解,可能表示的數字太大。好比123456789轉換成Base64 VLQ編碼就是qxmvrH。而少於5位的狀況在於mappings的編碼片斷中可能不須要那麼多信息就能進行映射,好比不須要Names屬性,這樣只經過4位信息也就能獲取到映射關係了。一個編碼片斷可能會有如下幾種長度:

  • 5 - 包含所有五個部分:輸出文件中的列號,輸入文件索引,輸入文件中的行號,輸入文件中的列號,符號索引
  • 4 - 輸出文件中的列號,輸入文件索引,輸入文件中的行號,輸入文件中的列號
  • 1 - 輸出文件中的列號

經過上面的講解,相信你們必定對Source Map是如何映射源碼轉換後代碼之間的位置關係有所瞭解。在瞭解了Source Map原理以後,往後再去使用它確定可以駕輕就熟。

相關文章
相關標籤/搜索