聊一聊SourceMap
大廠技術??堅持周更??精選好文
前言
我們項目的代碼在經(jīng)過編譯打包后,會將開發(fā)時多個文件的代碼合并到同一份文件中,而且還會經(jīng)過各種壓縮,合并,代碼丑化等等操作,轉(zhuǎn)換完最終生成的代碼才會用于線上環(huán)境,所以我們線上實際運行的代碼跟我們開發(fā)時的代碼是有非常大的不同,如果此時出現(xiàn)了bug,那么我們只能定位到轉(zhuǎn)換后代碼的位置,但此時的代碼已經(jīng)面目全非了
轉(zhuǎn)換后的代碼類似下面這樣

雖然這種代碼對計算機非常友好,但是我們debug將會變得很困難,這時候就需要sourcemap了
什么是SourceMap
簡單來說,Sourcemap 就是一個信息文件,它里面存儲著代碼轉(zhuǎn)換前后的對應位置信息,也就是轉(zhuǎn)換壓縮后的代碼所對應的轉(zhuǎn)換前的源代碼位置,是源代碼和生產(chǎn)代碼的映射, Sourcemap 解決了在打包過程中,代碼經(jīng)過壓縮,去空格以及 babel 編譯轉(zhuǎn)化后,由于代碼之間差異性過大,debug 困難的問題
大家的項目在開發(fā)完進行build后,在打包文件夾里除了有js,css,圖片等資源,一定還見過 .js.map文件,這種就是sourcemap文件

點開一個打包后的js文件,拉到最后一行,可以看到 //# sourceMappingURL=main.js.map

有了這行,就可以啟用sourcemap,這個sourceMappingURL就是標記了該文件的sourcemap的地址,這個sourcemap文件可以放在本地,也可以放在網(wǎng)絡上
再點開一個 .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版本的規(guī)范(下面會淺提一下)
sources:轉(zhuǎn)換前的源文件url數(shù)組(數(shù)組是因為存在多個文件合并的情況)
names:在mappings中引用的標識符數(shù)組(可以理解為轉(zhuǎn)換前代碼的所有變量名和屬性名)
sourceRoot:源文件的根路徑
sourcesContent:轉(zhuǎn)換前源文件的原始內(nèi)容,也是一個數(shù)組
mappings:記錄源碼和編譯后代碼的位置信息的base64 VLQ 字符串,是最重要的內(nèi)容
file:生成的與該sourcemap文件關聯(lián)的文件名,也就是打包編譯后的文件名
SourceMap的版本
關于sourcemap的版本
2009年,google介紹他的一個編譯器Cloure Compiler時,也順便推出了一個調(diào)試插件Closure Inspector,可以方便調(diào)試編譯后的代碼,這個就是 sourcemap 的雛形
2010年,Closure Compiler Source Map 2.0中,共同制定了一些標準,已決定使用base64編碼,但是生成的map文件要比現(xiàn)在大很多
2011年,第三代出爐, Source Map Revision 3 Proposal,也就是我們現(xiàn)在用的 sourcemap 的版本,這也就是為什么我們上面map文件的 version=3 了,這一版對算法進行了優(yōu)化,大大縮小了map文件的體積
第一版生成的map文件大概有轉(zhuǎn)化后文件的10倍大,第二版則將體積減少了20%~30%,第三版又在v2的基礎上體積減少了一半
正是因為有了第三代 Source Map Revision 3 Proposal 這個標準,不同的打包工具和瀏覽器才能使用sourcemap,github上的一個根據(jù)這個標準生成sourcemap的庫 https://github.com/mozilla/source-map
SourceMap的原理
這里主要關注mappings和names屬性,mappings屬性是一個很長的字符串,它分成三個部分

分號(;),表示行對應,生成的文件的每一行用分號(;)分隔,一個分號代表轉(zhuǎn)換后源碼的一行
逗號(,),位置對應,每一段用逗號(,)分隔,一個逗號對應轉(zhuǎn)換后源碼的一個位置
英文字母,每一段由1,4或5塊可變長度的字段組成,記錄原始代碼的位置信息
舉一個簡單的例子,假設有如下的mappings屬性
?mappings?:??AACAA;QAAQC,IADM?,
有一個分號,說明有兩行代碼,分號前 AACAA 是第一行,后面 QAAQC,IADM 是第二行
第二行有一個逗號,說明這一行分為兩段,QAAQC 和 IADM
分號跟逗號大家應該都沒什么疑問,主要就是英文字母這一塊的意義位置對應的原理
每一段最多有5個部分
第一部分,表示這個位置在(轉(zhuǎn)換后的代碼的)的第幾列
第二部分,表示這個位置屬于sources屬性中的哪一個文件
第三部分,表示這個位置屬于轉(zhuǎn)換前代碼的第幾行
第四部分,表示這個位置屬于轉(zhuǎn)換前代碼的第幾列
第五部分,表示這個位置屬于names屬性中的哪一個變量
那么這五個部分是怎么來的,我們一步一步來看
假設文件a.js有一行代碼 I Love SourceMap,最終打包后輸出的文件為bundle.js,內(nèi)容為 Javascript is awesome,如下

那么怎么表示映射關系
以 Love 為例,它原始的位置為(0,2),輸出后是awesome,位置為(0,14),那么我們可以這樣來映射

像這樣寫成一種固定的格式,里面包含了原始位置和輸出后的位置,單詞,同時還有原始文件名,因為可能把多個文件進行處理輸出,如果不寫文件名,就不知道輸入位置來自哪個文件
| 輸出后的單詞 | 映射關系 |
|---|---|
| Javascript | 0|0|a.js|0|7|SourceMap |
| is | 0|11|a.js|0|0|I |
| awesome | 0|14|a.js|0|2|Love |
我們可以優(yōu)化一下,把a.js和最后面的單詞提出來各放到一個數(shù)組里,用sources記錄所有的原始文件名,names記錄原始文件中的所有單詞,然后用下標表示他們,以Love為例,就變成

很多時候,我們輸出的文件其實是只有一行的,所以可以把輸出文件的行號省略掉,就變成

考慮到,如果文件特別大的話,那么行列的數(shù)值可能會特別大,所以可以考慮用相對位置來代替絕對位置來表示,只用絕對位置表示第一個單詞的位置,后面的都使用相對前一個單詞的位置

| 原始單詞 | 輸入位置 | 輸出單詞 | 輸出位置 | 映射 |
|---|---|---|---|---|
| I | (0,0) 絕對位置 | is | (0,11)絕對位置 | 11|0|0|0|0 |
| Love | (0,2) 相對I的位置 | awesome | (0,3)相對is的位置 | 3|0|0|2|1 |
| SourceMap | (0,5) 相對Love的位置 | Javascript | (0,-14) 相對awesome的位置 | -14|0|0|5|2 |
所以我們現(xiàn)在可以得到這么一個初步的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 編碼就可以解決分隔數(shù)字的問題,他的核心思路是在連續(xù)的數(shù)字上做標記,我們先來理解一下,拿上面 mappings 屬性的第一個為例,去掉|,然后在連續(xù)的字符上加上一個標記
110000
從左往右開始讀取,數(shù)字1有標記,說明還有連續(xù),再取下一個,是1,這個1沒被標記,第一個數(shù)結束,所以第一個數(shù)是11
繼續(xù)往下,0沒被標記,說明是一個完整的數(shù)字,第二個數(shù)就是0
依此類推。。。
最終就能得到11,0,0,0,0
而 vlq 利用6位二進制數(shù)進行存儲,其中第一位就表示是否連續(xù)的標識位,最后一位表示正數(shù)還是負數(shù)(0正數(shù),1負數(shù)) ,中間只有4位,因此一個單元表示的范圍為[-15,15],如果超過了就要用連續(xù)標識位了
看幾個例子來理解,每一步的變化我都用不同顏色標記了

第三步(按...5554分割),最右邊4位是因為他還需要額外多表示一位符號位,其余的都可以用5位來表示數(shù)值
倒數(shù)第二步倒順序,是因為VLQ表示數(shù)據(jù)字節(jié)組的順序是倒過來的
最終我們可以得到他們的 vlq 編碼
| 十進制數(shù)值 | vlq編碼 | vlq編碼每一段對應的數(shù)值 |
|---|---|---|
| 5 | 001010 | 10 |
| -19 | 100111 000001 | 39和1 |
然后再把它轉(zhuǎn)成base64編碼,可以查下面這張表

就可以得到5和-19的base64 vlq編碼了,因為5的 vlq 編碼數(shù)值是10,所以查上表可得到K,同理-19可以得到n和B,最終能得到5和-19的 base64 vlq 編碼分別是K和nB
這里有一個網(wǎng)站可以自己轉(zhuǎn)換驗證一下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 編碼
| 原始位置 | 輸出位置 | sources索引 | names索引 | 映射 | 每一部分的vlq編碼 | base64 vlq編碼 | |
|---|---|---|---|---|---|---|---|
| console | (1,0)絕對 | (0,0)絕對 | 0 | 0 | 0 | 0 | 1 |
| log | (0,8)相對 | (0,8)相對 | 0 | 1 | 8 | 0 | 0 |
| 123 | (-1,6)相對 | (0,4)相對 | 0 | 無 | 4 | 0 | -1 |
所以我們可以得到最終的map文件
{
?????sources?:?[?a.js?],
?????names?:?[??console?,??log?],
?????mappings?:??AACAA,QAAQC,IADM?,
????//?...其他的
}
反過來也能根據(jù)sourcemap文件推出原始的位置,這里就不再演示了
SourceMap總結
映射轉(zhuǎn)換過后的代碼和源代碼之間的關系
代碼中引入 //# sourceMappingURL=xxx.js.map 啟用
source Map 解決了源代碼和運行代碼不一致所產(chǎn)生的問題
不只是js文件有,css文件也有
核心原理是 base64 vlq 編碼
???H5-Dooring,讓H5制作更簡單
感謝巨人
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
???謝謝支持
以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^
喜歡的話別忘了?分享、點贊、收藏?三連哦~。
