來自公眾號:前端真好玩
Source map 想必大家都不陌生。線上的代碼多是壓縮后的,如果線上有報錯卻只能調試那個代碼多半是個噩夢。因此我們需要有一個橋梁幫助我們搭建起源代碼及壓縮后代碼的聯系,source map 就是起了這個作用。
以下是 MDN 對於 source map 的解釋:
調試原始源代碼會比瀏覽器下載的轉換后的代碼更加容易。 source map[1] 是從已轉換的代碼映射到原始源的文件,使瀏覽器能夠重構原始源並在調試器中顯示重建的原始源。
但是不知道各位讀者有沒有對 source map 的原理產生過疑問?筆者列出了四個疑問,不知道各位是不是也存在過這樣的問題:
Source map 四問
接下來的內容會逐步為讀者解答這四問。
source map 文件是否影響網頁性能
這個答案肯定是不會影響,否則構建相關的優化就肯定會涉及到對於 source map 的處理了,畢竟 source map 文件也不小。
其實 source map 只有在打開 dev tools 的情況下才會開始下載,相信大部分用戶都不會去打開這個面板,所以這也就不是問題了。
這時可能會有讀者想說:哎,但是我好像從來沒有在 Network 里看到 source map 文件的加載呀?其實這只是瀏覽器隱藏了而已,如果大家使用抓包工具的話就能發現在打開 dev tools 的時候開始下載 source map 了。
source map 存在標准嘛?
source map 是存在一個標准的,為 Google 及 Mozilla 的工程師制定,文檔地址[2]。正是因為存在這份標准,各個打包器及瀏覽器才能生成及使用 source map,否則就亂套了。
各個打包器基本都基於該庫[3]來生成 source map,當然也存在一些魔改的方案,但是標准都是統一的。
通過上面的庫生成出來的 source map 格式大致如下,大家也可以對比各個打包器的產物,格式及內容大部分都是一致的:
{ version: 3, file: "min.js", names: ["bar", "baz", "n"], sources: ["one.js", "two.js"], sourceRoot: "http://example.com/www/js/", mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA" }
接下來筆者介紹下重要字段的作用:
- version:顧名思義,指代了版本號,目前 source map 標准的版本為 3,也就是說這份 source map 使用的是第三版標准產出的
- file:編譯后的文件名
- names:一個優化用的字段,后續會在 mappings 中用到
- sources:多個源文件名
- mappings:這是最重要的內容,表示了源代碼及編譯后代碼的關系,但是先略過這塊,下文中會詳細解釋
另外大部分應用都是由 webpack 來打包的,可能有些讀者會發現 webpack 的 source map 產出的字段於上面的略微有些不一致。
這是因為 webpack 魔改了一些東西,但是底下還是基於這個庫實現的,只是變動了一些不涉及核心的字段,具體代碼[4]。
瀏覽器怎么知道源文件和 source map 的關系?
這里我們以 webpack 做個實驗,通過 webpack5 對於以下代碼進行打包:
// index.jsconst a = 1console.log(a);
當我們開啟 source map 選項以后,產物應該為兩個文件,分別為 bundle.js
以及 bundle.js.map
。
查看 bundle.js
文件以后我們會發現代碼中存在這一一段注釋:
console.log(1);//# sourceMappingURL=bundle.js.map
sourceMappingURL
就是標記了該文件的 source map 地址。
當然除此之外還有別的方式,通過查閱 MDN 文檔[5] 發現還可以通過 response header 的 SourceMap: <url>
字段來表明。
source map 是如何對應到源代碼的?
這是 source map 最核心的功能,也是最涉及知識盲區的一塊內容。
大家應該還記得上文中沒介紹的 mapping
字段吧,接下來我們就來詳細了解這個字段的用處。
我們還是以剛才打包的文件為例,來看看產出的 source map 長啥樣(去掉了無關緊要的):
{ sources:["webpack://webpack-source-demo/./src/index.js"], names: ['console', 'log'], mappings: 'AACAA,QAAQC,IADE', }
首先 mappings
的內容其實是 Base64 VLQ 的編碼表示。
內容由三部分組成,分別為:
- 英文,表示源碼及壓縮代碼的位置關聯
- 逗號,分隔一行代碼中的內容。比如說
console.log(a)
就由console
、log
及a
三部分組成,所以存在兩個逗號。 - 分號,代表換行
逗號和分號想必大家沒啥疑問,但是對於這幾個英文內容應該會很困惑。
其實這就是一種壓縮數字內容的編碼方式,畢竟源代碼可能很龐大,用數字表示行數及列數的話 source map 文件將也會很龐大,因此選用 Base 64 來代表數字用以減少文件體積。
比如說 A
代表了數字 0,C
代表了數字 1 等等,有興趣的讀者可以通過該網站[6]了解映射關系。
了解了這層編碼的映射關系,我們再來聊聊這一串串英文到底代表了什么。
其實這每串英文中的字母都代表了一個位置:
1.壓縮代碼的第幾列2.哪個源代碼文件,畢竟可以多個文件打包成一個,對應 sources
字段3.源代碼第幾行4.源代碼第幾列5.names
字段里的索引
這時讀者可能有個疑惑,為啥沒有壓縮代碼的第幾行表示?這是因為壓縮后的代碼就一行,所以只需要表示第幾列就行了。
更新:有讀者詢問 Base64 表達的數字是有上限的,如果需要表示的數字很大的話該怎么辦。實際上除了每個分號中的第一串英文是用來表示代碼的第幾行第幾列的絕對位置之外,后面的都是相對於之前的位置來做加減法的。
了解完以上知識以后,我們就來根據上文的內容解析下 AACAA
的具體含義吧,通過該網站[7]我們可以知道 AACAA
對應了 [0,0,1,0,0]
,這里需要注意的是數字都從 0 開始,筆者表述的時候會自動加一,畢竟代碼第零行聽起來怪怪的。
1.壓縮代碼的第一列2.第一個源代碼文件,也就是 index.js
文件了3.源代碼第二行了4.源代碼的第一列5.names
數組中的第一個索引,也就是 console
通過以上的解析,我們就能知道 console
在源代碼及壓縮文件中的具體位置了。
但是為什么 source map 會知道編譯后的代碼具體在什么位置呢?這里就要用到 AST 了。讓我們打開網站[8]輸入 console.log(a)
后觀察右邊的內容,你應該會發現如圖所示的數據:
因為 source map 是由 AST 產出的,所以我們能用上 AST 中的這個數據。
source map 的應用
一般來說 source map 的應用都是在監控系統中,開發者構建完應用后,通過插件將源代碼及 source map 上傳至平台中。一旦客戶端上報錯誤后,我們就可以通過該庫[9]來還原源代碼的報錯位置(具體 API 看文檔即可),方便開發者快速定位線上問題。
最后
source map 是我們日常中經常用到的東西,但是直到學習這塊內容的時候才知道居然涉及到了那么多的知識盲區。
大家如果有什么疑問歡迎在評論區交流。
引用鏈接
[1]
source map: https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/[2]
文檔地址: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit[3]
該庫: https://github.com/mozilla/source-map[4]
具體代碼: https://github.com/webpack/webpack-sources/blob/master/lib/SourceMapSource.js[5]
MDN 文檔: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/SourceMap[6]
該網站: https://www.murzwin.com/base64vlq.html[7]
該網站: https://www.murzwin.com/base64vlq.html[8]
網站: https://astexplorer.net/[9]
該庫: https://github.com/mozilla/source-map
原文出自https://mp.weixin.qq.com/s/woHjjQFpjtsaDfRXw-Cvug