聊一聊 SourceMap
前言
我們項目的代碼在經過編譯打包後,會將開發時多個文件的代碼合併到同一份文件中,而且還會經過各種壓縮,合併,代碼醜化等等操作,轉換完最終生成的代碼纔會用於線上環境,所以我們線上實際運行的代碼跟我們開發時的代碼是有非常大的不同,如果此時出現了 bug,那麼我們只能定位到轉換後代碼的位置,但此時的代碼已經面目全非了
轉換後的代碼類似下面這樣
雖然這種代碼對計算機非常友好,但是我們 debug 將會變得很困難,這時候就需要 sourcemap 了
什麼是 SourceMap
簡單來說,Sourcemap 就是一個信息文件,它裏面存儲着代碼轉換前後的對應位置信息,也就是轉換壓縮後的代碼所對應的轉換前的源代碼位置,是源代碼和生產代碼的映射, Sourcemap 解決了在打包過程中,代碼經過壓縮,去空格以及 babel 編譯轉化後,由於代碼之間差異性過大,debug 困難的問題
大家的項目在開發完進行 build 後,在打包文件夾裏除了有 js,css,圖片等資源,一定還見過 .js.map 文件,這種就是 sourcemap 文件
點開一個打包後的 js 文件,拉到最後一行,可以看到 //# sourceMappingURL=main.js.map
有了這行,就可以啓用 sourcemap,這個 sourceMappingURL 就是標記了該文件的 sourcemap 的地址,這個 sourcemap 文件可以放在本地,也可以放在網絡上
再點開一個 .js.map 文件看看,有一堆類似亂碼的東西,後面看一個簡單一點的
SourceMap 的生成
生成的方法有很多,而且有很多前端的工具都支持,如 webpack,uglifyjs,gulp 等,這裏就不詳細講述了,對怎麼生成 sourcemap 感興趣可以看這個文章
https://code.tutsplus.com/tutorials/source-maps-101--net-29173
SourceMap 的屬性
我們看一個簡單一點的代碼
const value = 123;
console.log(value);
用 webpack 打包後的代碼
console.log(123);
//# sourceMappingURL=bundle.js.map
生成的 sourcemap 文件
{
version : 3,
file : bundle.js ,
mappings : AACAA,QAAQC,IADM ,
sources : [
webpack://studysourcemap/./test.js
],
sourcesContent : [
const value = 123;\nconsole.log(value);
],
names : [
console ,
log
],
sourceRoot :
}
每個屬性的含義如下
-
version:遵循的是哪一個 sourcemap 版本的規範(下面會淺提一下)
-
sources:轉換前的源文件 url 數組(數組是因爲存在多個文件合併的情況)
-
names:在 mappings 中引用的標識符數組(可以理解爲轉換前代碼的所有變量名和屬性名)
-
sourceRoot:源文件的根路徑
-
sourcesContent:轉換前源文件的原始內容,也是一個數組
-
mappings:記錄源碼和編譯後代碼的位置信息的 base64 VLQ 字符串,是最重要的內容
-
file:生成的與該 sourcemap 文件關聯的文件名,也就是打包編譯後的文件名
SourceMap 的版本
關於 sourcemap 的版本
-
2009 年,google 介紹他的一個編譯器 Cloure Compiler 時,也順便推出了一個調試插件 Closure Inspector,可以方便調試編譯後的代碼,這個就是 sourcemap 的雛形
-
2010 年,Closure Compiler Source Map 2.0 中,共同制定了一些標準,已決定使用 base64 編碼,但是生成的 map 文件要比現在大很多
-
2011 年,第三代出爐, Source Map Revision 3 Proposal,也就是我們現在用的 sourcemap 的版本,這也就是爲什麼我們上面 map 文件的 version=3 了,這一版對算法進行了優化,大大縮小了 map 文件的體積
第一版生成的 map 文件大概有轉化後文件的 10 倍大,第二版則將體積減少了 20%~30%,第三版又在 v2 的基礎上體積減少了一半
正是因爲有了第三代 Source Map Revision 3 Proposal 這個標準,不同的打包工具和瀏覽器才能使用 sourcemap,github 上的一個根據這個標準生成 sourcemap 的庫 https://github.com/mozilla/source-map
SourceMap 的原理
這裏主要關注 mappings 和 names 屬性,mappings 屬性是一個很長的字符串,它分成三個部分
-
分號 (;),表示行對應,生成的文件的每一行用分號(;) 分隔,一個分號代表轉換後源碼的一行
-
逗號 (,),位置對應,每一段用逗號(,) 分隔,一個逗號對應轉換後源碼的一個位置
-
英文字母,每一段由 1,4 或 5 塊可變長度的字段組成,記錄原始代碼的位置信息
舉一個簡單的例子,假設有如下的 mappings 屬性
mappings : AACAA;QAAQC,IADM ,
有一個分號,說明有兩行代碼,分號前 AACAA
是第一行,後面 QAAQC,IADM
是第二行
第二行有一個逗號,說明這一行分爲兩段,QAAQC
和 IADM
分號跟逗號大家應該都沒什麼疑問,主要就是英文字母這一塊的意義位置對應的原理
每一段最多有 5 個部分
-
第一部分,表示這個位置在(轉換後的代碼的)的第幾列
-
第二部分,表示這個位置屬於 sources 屬性中的哪一個文件
-
第三部分,表示這個位置屬於轉換前代碼的第幾行
-
第四部分,表示這個位置屬於轉換前代碼的第幾列
-
第五部分,表示這個位置屬於 names 屬性中的哪一個變量
那麼這五個部分是怎麼來的,我們一步一步來看
假設文件 a.js 有一行代碼 I Love SourceMap
,最終打包後輸出的文件爲 bundle.js,內容爲 Javascript is awesome
,如下
那麼怎麼表示映射關係
以 Love 爲例,它原始的位置爲 (0,2),輸出後是 awesome,位置爲 (0,14),那麼我們可以這樣來映射
像這樣寫成一種固定的格式,裏面包含了原始位置和輸出後的位置,單詞,同時還有原始文件名,因爲可能把多個文件進行處理輸出,如果不寫文件名,就不知道輸入位置來自哪個文件
我們可以優化一下,把 a.js 和最後面的單詞提出來各放到一個數組裏,用 sources 記錄所有的原始文件名,names 記錄原始文件中的所有單詞,然後用下標表示他們,以 Love 爲例,就變成
很多時候,我們輸出的文件其實是隻有一行的,所以可以把輸出文件的行號省略掉,就變成
考慮到,如果文件特別大的話,那麼行列的數值可能會特別大,所以可以考慮用相對位置來代替絕對位置來表示,只用絕對位置表示第一個單詞的位置,後面的都使用相對前一個單詞的位置
所以我們現在可以得到這麼一個初步的 map 文件
{
names: ['I', 'Love', 'SourceMap'],
sources: ['a.js'],
mappings: [11|0|0|0|0, 3|0|0|2|1, -14|0|0|5|2]
}
但是 mappings 這裏十分難看,而且還需要用|
來分隔,多佔一個位置,用 vlq 編碼就可以解決分隔數字的問題,他的核心思路是在連續的數字上做標記,我們先來理解一下,拿上面 mappings 屬性的第一個爲例,去掉|
,然後在連續的字符上加上一個標記
110000
從左往右開始讀取,數字 1 有標記,說明還有連續,再取下一個,是 1,這個 1 沒被標記,第一個數結束,所以第一個數是 11
繼續往下,0 沒被標記,說明是一個完整的數字,第二個數就是 0
依此類推。。。
最終就能得到 11,0,0,0,0
而 vlq 利用 6 位二進制數進行存儲,其中第一位就表示是否連續的標識位,最後一位表示正數還是負數(0 正數,1 負數) ,中間只有 4 位,因此一個單元表示的範圍爲 [-15,15],如果超過了就要用連續標識位了
看幾個例子來理解,每一步的變化我都用不同顏色標記了
第三步(按...5554 分割),最右邊 4 位是因爲他還需要額外多表示一位符號位,其餘的都可以用 5 位來表示數值
倒數第二步倒順序,是因爲 VLQ 表示數據字節組的順序是倒過來的
最終我們可以得到他們的 vlq 編碼
然後再把它轉成 base64 編碼,可以查下面這張表
就可以得到 5 和 - 19 的 base64 vlq 編碼了,因爲 5 的 vlq 編碼數值是 10,所以查上表可得到 K,同理 - 19 可以得到 n 和 B,最終能得到 5 和 - 19 的 base64 vlq 編碼分別是 K 和 nB
這裏有一個網站可以自己轉換驗證一下 https://www.murzwin.com/base64vlq.html
然後我們回過頭爲我們最開始那個簡單的 js 文件手動生成一下 map 文件來驗證一下
const value = 123;
console.log(value);
打包後的代碼
console.log(123);
//# sourceMappingURL=bundle.js.map
sources 和 names 是可以先確定好的
{
sources : [ a.js ],
names : [ console , log ],
}
再得到 base64 vlq 編碼
所以我們可以得到最終的 map 文件
{
sources : [ a.js ],
names : [ console , log ],
mappings : AACAA,QAAQC,IADM ,
// ...其他的
}
反過來也能根據 sourcemap 文件推出原始的位置,這裏就不再演示了
SourceMap 總結
-
映射轉換過後的代碼和源代碼之間的關係
-
代碼中引入 //# sourceMappingURL=xxx.js.map 啓用
-
source Map 解決了源代碼和運行代碼不一致所產生的問題
-
不只是 js 文件有,css 文件也有
-
核心原理是 base64 vlq 編碼
感謝巨人
https://juejin.cn/post/7023537118454480904
https://juejin.cn/post/6963076475020902436
https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.1ce2c87bpj24
https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/
http://www.qiutianaimeili.com/html/page/2019/05/89jrubx1soc.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YVySzeCNpQpijwf8kwblFw