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

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

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

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

1
eval
每個(gè)模塊用eval()包裹執(zhí)行。
1)devtool: eval
我們先看看單獨(dú)的eval配置,這個(gè)配置相對(duì)于其他會(huì)特殊一點(diǎn) 。因?yàn)榕渲美餂](méi)有sourceMap,實(shí)際上它也會(huì)生出map,只是它映射的是轉(zhuǎn)換后的代碼,而不是映射到原始代碼。

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

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

4
cheap
這是 “cheap(低開(kāi)銷)” 的 source map,因?yàn)樗鼪](méi)有生成列映射(column mapping),只是映射行數(shù) 。
為了方便演示,我們?cè)诖a加一行錯(cuò)誤拋出:


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

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

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

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

通過(guò)debug,打印出生成的css sourceMap,和js sourceMap對(duì)比并無(wú)他樣:

如果大家用了sass的話,很可能會(huì)遇到一個(gè)css url resolve的問(wèn)題,在之前的一篇講webpack 配置的文章里我也提到過(guò):

實(shí)際上,利用css sourceMap這個(gè)問(wèn)題便可以在不改變?cè)创a的情況下就可以完美解決。
這里會(huì)增加一個(gè)loader去處理,loader處理流程主要分為二步:
1、根據(jù)sourceMap的sourcesContent和url內(nèi)容進(jìn)行匹配,然后從sources定位到原有的css資源路徑
2、將傳遞給下個(gè)loader的url內(nèi)容替換成絕對(duì)路徑
代碼如下:
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;
? ? ? ? ? ? ? ?returntrue;
? ? ? ? ? ?}
? ? ? ?});
? ? ? ?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;
}
因?yàn)橐蕾噑ass-loader 處理之后的sourceMap, 所以@tencent/im-resolve-url-loader應(yīng)配置在sass-loader 前面,配置如下:

IMWeb 團(tuán)隊(duì)隸屬騰訊公司,是國(guó)內(nèi)最專業(yè)的前端團(tuán)隊(duì)之一。
我們專注前端領(lǐng)域多年,負(fù)責(zé)過(guò) QQ 資料、QQ 注冊(cè)、QQ 群等億級(jí)業(yè)務(wù)。目前聚焦于在線教育領(lǐng)域,精心打磨?騰訊課堂、企鵝輔導(dǎo)?及 ABCMouse 三大產(chǎn)品。
