SourceMap知多少:介紹與實踐
(給全棧前端精選加星標,提升前端技能)
作者:騰訊IMWeb前端團隊
說起sourceMap大家肯定都不陌生,隨著前端工程化的演進,我們打包出來的代碼都是混淆壓縮過的,當源代碼經(jīng)過轉(zhuǎn)換后,調(diào)試就成了一個問題。在瀏覽器中調(diào)試時,如何判斷原始代碼的位置?
為了解決這個問題,google 提出了sourceMap 的想法,并在chorme上最先支持sourceMap的使用。sourceMap 由于包含許多信息,前期也經(jīng)過多版的編碼算法優(yōu)化,最后在2011年探索出了Source Map Revision 3.0 ,這個版本也就是我們現(xiàn)在一直在使用的sourceMap版本。這一版本的mapping信息使用Base64 VLQ 編碼,大大縮小了.map文件的體積。
sourceMap可以幫我們直接定位到編譯前代碼的特定位置,接下來我們直接拿個sourceMap文件來看看它包含了一些什么信息:

上面可以看到,sourceMap其實就是就是一段維護了前后代碼映射關系的json描述文件,包含了以下一些信息:
version:sourcemap版本(現(xiàn)在都是v3)
file:轉(zhuǎn)換后的文件名。
sourceRoot:轉(zhuǎn)換前的文件所在的目錄。如果與轉(zhuǎn)換前的文件在同一目錄,該項為空。
sources:轉(zhuǎn)換前的文件。該項是一個數(shù)組,表示可能存在多個文件合并。
names:轉(zhuǎn)換前的所有變量名和屬性名。
mappings:記錄位置信息的字符串。
mappings 信息是關鍵,它使用Base64 VLQ 編碼,包含了源代碼與生成代碼的位置映射信息。mappings的編碼原理詳解可見:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html,這里就不再詳述。
?webpack 給出了多種sourceMap配置方式,相信很多人第一眼看到的時候和我一樣,疑惑這些都有啥區(qū)別

其實不難發(fā)現(xiàn)這么多配置,這些就是source-map和eval、inline、cheap、module 的自由組合。所以我們來拆開看下每項配置。
為了方便演示,這里的源代碼只包含了一行代碼
console.log('hello world');
先給大家展示,最原始的只設置’source-map’配置,可以看到輸出了兩個文件,其中包含一個map文件

main.js文件內(nèi)容如下,map文件上面展示過了,就不再展示內(nèi)容了

1
eval
每個模塊用eval()包裹執(zhí)行。
1)devtool: eval
我們先看看單獨的eval配置,這個配置相對于其他會特殊一點 。因為配置里沒有sourceMap,實際上它也會生出map,只是它映射的是轉(zhuǎn)換后的代碼,而不是映射到原始代碼。

2)devtool: eval-source-map
所以eval-source-map就會帶上源碼的sourceMap,打包結(jié)果如下:

對于eval的構建模式,我們可以看看官方的描述
devtool: “eval-source-map” is really as good as devtool: “source-map”, but can cache SourceMaps for modules. It’s much faster for rebuilds.
可以看出官方是比較推薦開發(fā)場景下使用的,因為它能cache sourceMap,從而rebuild的速度會比較快。
2
inline
inline配置想必大家肯定已經(jīng)能猜到了,就是將map作為DataURI嵌入,不單獨生成.map文件。devtool: inline-source-map構建出來的文件如下, 這個比較好理解,就不多說了

4
cheap
這是 “cheap(低開銷)” 的 source map,因為它沒有生成列映射(column mapping),只是映射行數(shù) 。
為了方便演示,我們在代碼加一行錯誤拋出:


5
module
Webpack會利用loader將所有非js模塊轉(zhuǎn)化為webpack可處理的js模塊,而增加上面的cheap配置后也不會有l(wèi)oader模塊之間對應的sourceMap。
什么是模塊之間的sourceMap呢?比如jsx文件會經(jīng)歷loader處理成js文件再混淆壓縮, 如果沒有l(wèi)oader之間的sourceMap,那么在debug的時候定義到上圖中的壓縮前的js處,而不能追蹤到jsx中。
所以為了映射到loader處理前的代碼,我們一般也會加上module配置
6
總結(jié)
1、開發(fā)環(huán)境
綜上所述,考慮到我們在開發(fā)環(huán)境對sourceMap的要求是:快(eval),信息全(module),且由于此時代碼未壓縮,我們并不那么在意代碼列信息(cheap),所以開發(fā)環(huán)境比較推薦配置:devtool: cheap-module-eval-source-map
2、生產(chǎn)環(huán)境
一般情況下,我們并不希望任何人都可以在瀏覽器直接看到我們未編譯的源碼,所以我們不應該直接提供sourceMap給瀏覽器。但我們又需要sourceMap來定位我們的錯誤信息, 這時我們可以設置hidden-source-map:
一方面webpack會生成sourcemap文件以提供給錯誤收集工具比如sentry,另一方面又不會為 bundle 添加引用注釋,以避免瀏覽器使用。
當然如果沒有這一類的錯誤處理工具,可以看看webpack推薦的其他配置:
https://www.webpackjs.com/configuration/devtool/
說起sourceMap我們第一反應通常是JavaScript的sourceMap,實際上現(xiàn)在css也可以使用sourceMap。因為sourceMap本質(zhì)只是一個json,里面包含了源碼的映射信息。所以其實只要了解sourcemap的編碼規(guī)范,我們可以對任何我們想要的資源生成sourceMap,當然sourceMap 的支持也還是要取決于瀏覽器的支持。
現(xiàn)在,對于css我們也有同樣訴求,比如我現(xiàn)在打開調(diào)試器看到的樣式配置沒有任何源信息。如果想像js一樣,知道這個css樣式是在哪個文件需要怎么弄呢?

上面講解的配置其實都是針對js的sourceMap,配置后webpack會自動幫我們生成各類js sourceMap。因為本質(zhì)上webpack只處理js,對于webpack來說,css是否有sourceMap依賴于對css處理的loader是否有sourceMap輸出,所以loader需要開啟并傳遞sourceMap,這樣最后生成的css才會帶上sourceMap 。
目前使用的css-loader,sass-loader都已經(jīng)提供了生成sourceMap的能力,只需要我們加上配置即可。
需要注意的是,這里如果要拿到sass編譯前的源碼信息,那么sourceMap一定要從sass-loader一直傳遞到css-loader,中間如有其他loader處理,也要透傳sourceMap

我們可以看到,加了sourceMap 配置后,sourceMap會被內(nèi)聯(lián)在css代碼里(這一層是css-loader處理的,與你是否使用min-extract-css-plugin抽出css無關)

加了css sourceMap后,我們可以很輕松的定位到sass編譯前的源碼路徑了。

通過debug,打印出生成的css sourceMap,和js sourceMap對比并無他樣:

如果大家用了sass的話,很可能會遇到一個css url resolve的問題,在之前的一篇講webpack 配置的文章里我也提到過:

實際上,利用css sourceMap這個問題便可以在不改變源碼的情況下就可以完美解決。
這里會增加一個loader去處理,loader處理流程主要分為二步:
1、根據(jù)sourceMap的sourcesContent和url內(nèi)容進行匹配,然后從sources定位到原有的css資源路徑
2、將傳遞給下個loader的url內(nèi)容替換成絕對路徑
代碼如下:
module.exports = function (content, map) {
? ?const res = content.replace(/url\((?:\'|")?((\.\/|\.\.\/)+([^\'"\)]*))(\'|")?\)/g, (str, img, p2, imgPath) => {
? ? ? ?let index = -1;
? ? ? ?const {sourcesContent = [], sources = [], sourceRoot = []} = map || {};
? ? ? ?sourcesContent.some((item, i)=> {
? ? ? ? ? ?if (item.indexOf(img) !== -1) {
? ? ? ? ? ? ? ?index = i;
? ? ? ? ? ? ? ?return true;
? ? ? ? ? ?}
? ? ? ?});
? ? ? ?if (index !== -1) {
? ? ? ? ? ?const dir = path.dirname(sources[index]); // 獲取文件所在目錄
? ? ? ? ? ?str = str.replace(img, `~${path.join(dir, img)}`);
? ? ? ?}
? ? ? ?return str;
? ?});
? ?this.callback(null, res, map);
? ?return;
}
因為依賴sass-loader 處理之后的sourceMap, 所以@tencent/im-resolve-url-loader應配置在sass-loader 前面,配置如下:

分享前端好文,點亮?在看?
