【總結(jié)】1199- 弄懂 SourceMap,前端開發(fā)提效 100%

一、什么是 Source Map
通俗的來說, Source Map 就是一個(gè)信息文件,里面存儲(chǔ)了代碼打包轉(zhuǎn)換后的位置信息,實(shí)質(zhì)是一個(gè) json 描述文件,維護(hù)了打包前后的代碼映射關(guān)系。關(guān)于 Source Map 的解釋可以看下 Introduction to JavaScript Source Maps[7]。
我們線上的代碼一般都是經(jīng)過打包的,如果線上代碼報(bào)錯(cuò)了,想要調(diào)試起來,那真是很費(fèi)勁了,比如下面這個(gè)例子:
使用打包工具 Webpack ,編譯這一段代碼
console.log('source?map!!!')
console.log(a);?//這一行肯定會(huì)報(bào)錯(cuò)
瀏覽器打開后的效果:

點(diǎn)擊進(jìn)入報(bào)錯(cuò)文件之后:

這根本沒法找到具體位置以及原因,所以這個(gè)時(shí)候, Source Map 的作用就來了, Webpack 構(gòu)建代碼中,開啟 Source Map :

然后重新執(zhí)行構(gòu)建,再次打開瀏覽器:

可以發(fā)現(xiàn),可以成功定位到具體的報(bào)錯(cuò)位置了,這就是 Source Map 的作用。需要注意一點(diǎn)的是, Source Map 并不是 Webpack 特有的,其他打包工具同樣支持 Source Map ,打包工具只是將 Source Map 這項(xiàng)技術(shù)通過配置化的方式引入進(jìn)來。關(guān)于打包工具,下文會(huì)有介紹。
二、Source Map 的作用
上面的案例只是 Source Map 的初體驗(yàn),現(xiàn)在來說一下它的作用,我們?yōu)槭裁葱枰?Source Map ?
阮一峰老師的JavaScript Source Map 詳解[8]指出,JavaScript 腳本正變得越來越復(fù)雜。大部分源碼(尤其是各種函數(shù)庫和框架)都要經(jīng)過轉(zhuǎn)換,才能投入生產(chǎn)環(huán)境。
常見的源碼轉(zhuǎn)換,主要是以下三種情況:
壓縮,減小體積 多個(gè)文件合并,減少 HTTP 請(qǐng)求數(shù) 其他語言編譯成 JavaScript
這三種情況,都使得實(shí)際運(yùn)行的代碼不同于開發(fā)代碼,除錯(cuò)( debug )變得困難重重,所以才需要 Source Map 。結(jié)合上面的例子,即使打包過后的代碼,也可以找到具體的報(bào)錯(cuò)位置,這使得我們 debug 代碼變得輕松簡單,這就是 Source Map 想要解決的問題。
三、如何生成 Source Map
各種主流前端任務(wù)管理工具,打包工具都支持生成 Source Map 。
3.1 UglifyJS
UglifyJS 是命令行工具,用于壓縮 JavaScript 代碼
安裝 UglifyJS :
npm?install?uglify?-?js?-?g
復(fù)制代碼
壓縮代碼的同時(shí)生成 Source Map :
uglifyjs?app.js?-?o?app.min.js--source?-?map?app.min.js.map
復(fù)制代碼
Source Map 相關(guān)選項(xiàng):
--source?-?map?Source?Map的文件的路徑和名稱
????--source?-?map?-?root?源文件的路徑
????--source?-?map?-?url?//#sourceMappingURL的路徑。?默認(rèn)為--source-map指定的值。
????--source?-?map?-?include?-?sources?是否將源代碼的內(nèi)容添加到sourcesContent數(shù)組
????--source?-?map?-?inline?是否將Source?Map寫到壓縮代碼的最后一行
????--?in?-source?-?map?輸入Source?Map,?當(dāng)源文件已經(jīng)經(jīng)過變換時(shí)使用
復(fù)制代碼
3.2 Grunt
Grunt 是 JavaScript 項(xiàng)目構(gòu)建工具
配置 grunt-contrib-uglify 插件以生成 Source Map :
grunt.initConfig({
????uglify:?{
????????options:?{
????????????sourceMap:?true
????????}
????}
});
復(fù)制代碼
使用 grunt-usemin 打包源碼時(shí), grunt-usemin 會(huì)依次調(diào)用grunt-contrib-concat[9]與grunt-contrib-uglify[10]對(duì)源碼進(jìn)行打包和壓縮。因此都需要進(jìn)行配置:
grunt.initConfig({
????concat:?{
????????options:?{
????????????sourceMap:?true
????????}
????},
????uglify:?{
????????options:?{
????????????sourceMap:?true,
????????????sourceMapIn:?function(uglifySource)?{
????????????????return?uglifySource?+?'.map';
????????????},
????????}
????}
});
復(fù)制代碼
3.3 Gulp
Gulp 是 JavaScript 項(xiàng)目構(gòu)建工具
使用gulp-sourcemaps[11]生成 Source Map :
var?gulp?=?require('gulp');
var?plugin1?=?require('gulp-plugin1');
var?plugin2?=?require('gulp-plugin2');
var?sourcemaps?=?require('gulp-sourcemaps');
gulp.task('javascript',?function()?{
????gulp.src('src/**/*.js')
????????.pipe(sourcemaps.init())
????????.pipe(plugin1())
????????.pipe(plugin2())
????????.pipe(sourcemaps.write('../maps'))
????????.pipe(gulp.dest('dist'));
});
復(fù)制代碼
3.4 SystemJS
SystemJS 是模塊加載器
使用SystemJS Build Tool[12]生成 Source Map :
builder.bundle('myModule.js',?'outfile.js',?{
????minify:?true,
????sourceMaps:?true
});
復(fù)制代碼
sourceMapContents選項(xiàng)可以指定是否將源碼寫入Source Map文件
3.5 Webpack
Webpack 是前端打包工具(本文案例都會(huì)使用該打包工具)。在其配置文件 webpack.config.js 中設(shè)置devtool[13]即可生成 Source Map 文件:
const?path?=?require('path');
module.exports?=?{
????entry:?'./src/index.js',
????output:?{
????????filename:?'bundle.js',
????????path:?path.resolve(__dirname,?'dist')
????},
????devtool:?"source-map"
};
復(fù)制代碼
devtool有 20 多種不同取值,分別生成不同類型的 Source Map,可以根據(jù)需要進(jìn)行配置。下文會(huì)詳細(xì)介紹,這里不再贅述。
3.6 Closure Compiler
利用 Closure Compiler[14] 生成
四、如何使用 Source Map
生成 Source Map 之后,一般在瀏覽器中調(diào)試使用,前提是需要開啟該功能,以 Chrome 為例:
打開開發(fā)者工具,找到 Settins :

勾選以下兩個(gè)選項(xiàng):

再回到上面的案例中,源代碼文件變成了 index.js ,點(diǎn)擊進(jìn)入后顯示真實(shí)的源代碼,即說明成功開啟并使用了 Source Map

五、Source Map 的工作原理
還是上面這個(gè)案例,執(zhí)行打包后,生成 dist 文件夾,打開 dist/bundld.js :

可以看到尾部有這句注釋:
//#?sourceMappingURL=bundle.js.map
復(fù)制代碼
正是因?yàn)檫@句注釋,標(biāo)記了該文件的 Source Map 地址,瀏覽器才可以正確的找到源代碼的位置。sourceMappingURL 指向 Source Map 文件的 URL 。
除了這種方式之外,MDN[15]中指出,可以通過 response header 的 SourceMap: 字段來表明。
>?SourceMap:?/path/to/file.js.map
>?```
`dist`?文件夾中,除了?`bundle.js`?還有?`bundle.js.map`?,這個(gè)文件才是?`Source?Map`?文件,也是?`sourceMappingURL`?指向的?`URL`

*?`version`:`Source map`的版本,目前為`v3`。
*?`sources`:轉(zhuǎn)換前的文件。該項(xiàng)是一個(gè)數(shù)組,表示可能存在多個(gè)文件合并。
*?`names`:轉(zhuǎn)換前的所有變量名和屬性名。
*?`mappings`:記錄位置信息的字符串,下文會(huì)介紹。
*?`file`:轉(zhuǎn)換后的文件名。
*?`sourceRoot`:轉(zhuǎn)換前的文件所在的目錄。如果與轉(zhuǎn)換前的文件在同一目錄,該項(xiàng)為空。
*?`sourcesContent`:轉(zhuǎn)換前文件的原始內(nèi)容。
#####?5.1?關(guān)于Source?map的版本
在2009年?`Google`?的一篇文章中,在介紹?`Cloure Compiler`?時(shí),?`Google`?也趁便推出了一款調(diào)試東西:?`Firefox`?插件?`Closure Inspector`?,以便利調(diào)試編譯后代碼。這便是?`Source Map`?的初步代啦!
>?You?can?use?the?compiler?with?Closure?Inspector?,?a?Firebug?extension?that?makes?debugging?the?obfuscated?code?almost?as?easy?as?debugging?the?human-readable?source.
2010年,在第二代即?`Closure Compiler Source Map 2.0`?中,?`Source Map`?招認(rèn)了共同的?`JSON`?格式及其他標(biāo)準(zhǔn),已幾乎具有現(xiàn)在的雛形。最大的差異在于?`mapping`?算法,也是?`Source Map`?的要害地址。第二代中的?`mapping`?已決定運(yùn)用?`base 64`?編碼,可是算法同現(xiàn)在有收支,所以生成的?`.map`?比較現(xiàn)在要大許多。
2011年,第三代即[**Source?Map?Revision?3?Proposal**](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#)出爐了,這也是咱們現(xiàn)在運(yùn)用的?`Source Map`?版別。從文檔的命名看來,此刻的?`Source Map`?已脫離?`Clousre Compiler`?,演化成了一款獨(dú)立東西,也得到了瀏覽器的支撐。這一版相較于二代最大的改動(dòng)是?`mapping`?算法的緊縮換代,運(yùn)用[VLQ](https://en.wikipedia.org/wiki/Variable-length_quantity)編碼生成[base64](https://zh.wikipedia.org/zh-cn/Base64)前的?`mapping`?,大大縮小了?`.map`?文件的體積。
`Source Map`?發(fā)展史的詼諧之處在于,它作為一款輔佐東西被開發(fā)出來。畢竟它輔佐的方針日漸式微,而它卻成為了技能主體,被寫進(jìn)了瀏覽器中。
> Source Map V1最初步生成的Source Map文件大概有轉(zhuǎn)化后文件的10倍大。Source Map V2將之減少了50%,V3又在V2的基礎(chǔ)上減少了50%。所以現(xiàn)在133k的文件對(duì)應(yīng)的Source Map文件巨細(xì)大概在300k左右。
#####?5.2?關(guān)于mappings屬性
為了避免干擾,將案例改成如下不報(bào)錯(cuò)的情況:
```js
var?a?=?1;
console.log(a);
`
復(fù)制代碼
打包編譯的后 bundle.js 文件:
/******/
(()?=>?{?//?webpackBootstrap
????var?__webpack_exports__?=?{};
????/*!**********************!*\
??????!***?./src/index.js?***!
??????\**********************/
????var?a?=?1;
????console.log(a);
????/******/
})();
//#?sourceMappingURL=bundle.js.map
復(fù)制代碼
打包編譯后的 bundle.js.map 文件:
{
????"version":?3,
????"sources":?[
????????"webpack://learn-source-map/./src/index.js"
????],
????"names":?[],
????"mappings":?"AAAA;AACA,c",
????"file":?"bundle.js",
????"sourcesContent":?[
????????"var?a?=?1;\r\nconsole.log(a);"
????],
????"sourceRoot":?""
}
復(fù)制代碼
可以看到 mappings 屬性的值是:AAAA; AACA, c ,要想說清楚這個(gè)東西,需要先解釋一下它的組成結(jié)構(gòu)。這是一個(gè)字符串,它分成三層:
第一層是行對(duì)應(yīng),以分號(hào)(; )表示,每個(gè)分號(hào)對(duì)應(yīng)轉(zhuǎn)換后源碼的一行。所以,第一個(gè)分號(hào)前的內(nèi)容,就對(duì)應(yīng)源碼的第一行,以此類推。 第二層是位置對(duì)應(yīng),以逗號(hào)(, )表示,每個(gè)逗號(hào)對(duì)應(yīng)轉(zhuǎn)換后源碼的一個(gè)位置。所以,第一個(gè)逗號(hào)前的內(nèi)容,就對(duì)應(yīng)該行源碼的第一個(gè)位置,以此類推。 第三層是位置轉(zhuǎn)換,以VLQ 編碼[16]表示,代表該位置對(duì)應(yīng)的轉(zhuǎn)換前的源碼位置。
在回到源代碼,就可以分析出:
因?yàn)樵创a中有兩行,所以有一個(gè)分號(hào),分號(hào)前后表示了第一行和第二行。即 mappings中的AAAA和AACA,c。分號(hào)后面表示第二行,也就是代碼 console.log(a);可以拆分出兩個(gè)位置,分別是console和log(a),所以存在一個(gè)逗號(hào)。即AACA,c中的AACA和c。
總結(jié),就是轉(zhuǎn)換后的源碼分成兩行,第一行有一個(gè)位置,第二行有兩個(gè)位置。
至于這個(gè) AAAA , AAcA 等字母是怎么來的,可以參考阮一峰老師的JavaScript Source Map 詳解[17]有作詳細(xì)的介紹。筆者自己的理解是:
AAAA 和 AAcA 以及 c 都是代表了位置,正常來說,每個(gè)位置最多由 5 個(gè)字母組成,5 個(gè)字母的含義分別是:
第一位,表示這個(gè)位置在(轉(zhuǎn)換后的代碼的)的第幾列。 第二位,表示這個(gè)位置屬于 sources 屬性中的哪一個(gè)文件。 第三位,表示這個(gè)位置屬于轉(zhuǎn)換前代碼的第幾行。 第四位,表示這個(gè)位置屬于轉(zhuǎn)換前代碼的第幾列。 第五位,表示這個(gè)位置屬于 names 屬性中的哪一個(gè)變量。
這里轉(zhuǎn)換后最多只有 4 個(gè)字母,是因?yàn)闆]有 names 屬性。
每一個(gè)位置都可以用VLQ 編碼[18]轉(zhuǎn)換,形成一種映射關(guān)系??梢栽?span style="font-weight: bold;color: #ffb11b;padding: 3px;">這個(gè)網(wǎng)站[19]自己轉(zhuǎn)換測(cè)試,將 AAAA; AACA, c 轉(zhuǎn)換后的結(jié)果:

可以得到兩組數(shù)據(jù):
[0,?0,?0,?0]
[0,?0,?1,?0],?[14]
復(fù)制代碼
數(shù)字都是從 0 開始的,拿位置 AAAA 舉例,轉(zhuǎn)換后得到 [0, 0, 0, 0] ,所以代表的含義分別是;
壓縮代碼的第一列。 第一個(gè)源代碼文件,即 index.js。源代碼的第一行。 源代碼第一列
通過以上解析,我們就能知道源代碼中 var a = 1; 在打包后文件中,即 bundle.js 的具體位置了。
六、Webpack 中的 Source Map
上文介紹了 Source Map 的作用,原理等?,F(xiàn)在說一下打包工具 WebPack 中對(duì) Source Map 的應(yīng)用,畢竟我們?cè)陂_發(fā)中,都離不開它。
上文有說道,只需要在 webpack.config.js 文件中配置 devtool 就可以使用 Source Map ,這個(gè) devtool 具體的值有哪些,可以參考webpack devtool[20]
的介紹,官方羅列了 20 幾種類型,我們當(dāng)然不能全部都記住,可以記住幾個(gè)關(guān)鍵的:

建議以下 7 種可選方案:
source-map:外部。可以查看錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置。 inline-source-map:內(nèi)聯(lián)。只生成一個(gè)內(nèi)聯(lián) Source Map,可以查看錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置hidden-source-map:外部??梢圆榭村e(cuò)誤代碼準(zhǔn)確信息,但不能追蹤源代碼錯(cuò)誤,只能提示到構(gòu)建后代碼的錯(cuò)誤位置。 eval-source-map:內(nèi)聯(lián)。每一個(gè)文件都生成對(duì)應(yīng)的 Source Map,都在eval中,可以查看錯(cuò)誤代碼準(zhǔn)確信息 和 源代碼的錯(cuò)誤位置。nosources-source-map:外部??梢圆榭村e(cuò)誤代碼錯(cuò)誤原因,但不能查看錯(cuò)誤代碼準(zhǔn)確信息,并且沒有任何源代碼信息。 cheap-source-map:外部??梢圆榭村e(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置,只能把錯(cuò)誤精確到整行,忽略列。 cheap-module-source-map:外部。可以錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置, module會(huì)加入loader的Source Map。
內(nèi)聯(lián)和外部的區(qū)別:
外部生成了文件( .map),內(nèi)聯(lián)沒有。內(nèi)聯(lián)構(gòu)建速度更快。
以下通過具體的案例演示上面的 7 種類型:
首先,將案例改成報(bào)錯(cuò)狀態(tài),為了體現(xiàn)列的情況,將源代碼修改成如下:
console.log('source?map!!!')
var?a?=?1;
console.log(a,?b);?//這一行肯定會(huì)報(bào)錯(cuò)
復(fù)制代碼
6.1 source-map
devtool:?'source-map'
復(fù)制代碼
編譯后,可以查看錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置:

生成了 .map 文件:

6.2 inline-source-map
devtool:?'inline-source-map'
復(fù)制代碼
編譯后,可以查看錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置:

但是沒有生成 .map文件 ,而是以 base64 的形式插入到 sourceMappingURL 中:

6.3 hidden-source-map
devtool:?'hidden-source-map'
復(fù)制代碼
編譯后,可以查看錯(cuò)誤代碼準(zhǔn)確信息,但是無法查看源代碼的位置:

生成了 .map 文件:

6.4 eval-source-map
devtool:?'eval-source-map'
復(fù)制代碼
編譯后,可以查看錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置:

但是沒有生成 .map文件 ,而是在 eval函數(shù) 中,包括 sourceMappingURL :


6.5 nosources-source-map
devtool:?'nosources-source-map'
復(fù)制代碼
編譯后,可以查看無法查看錯(cuò)誤代碼的準(zhǔn)確位置和源代碼的錯(cuò)誤位置,只能提示錯(cuò)誤原因:

生成了 .map 文件:

6.6 cheap-source-map
devtool:?'cheap-source-map'
復(fù)制代碼
編譯后,可以查看錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置,但是忽略了具體的列( 因?yàn)槭莃導(dǎo)致報(bào)錯(cuò) ):

生成了 .map 文件:

6.7 cheap-module-source-map
因?yàn)樾枰?module ,所以案例中增加 loader :
module:?{
????rules:?[{
????????test:?/\.css$/,
????????use:?[
????????????// style-loader:創(chuàng)建style標(biāo)簽,將js中的樣式資源插入進(jìn)去,添加到head中生效
????????????'style-loader',
????????????// css-loader:將css文件變成commonjs模塊加載到j(luò)s中,里面內(nèi)容是樣式字符串
????????????'css-loader'
????????]
????}]
}
復(fù)制代碼
在 src 目錄下新建 index.css 文件,添加樣式代碼:
body?{
????margin:?0;
????padding:?0;
????height:?100%;
????background-color:?pink;
}
復(fù)制代碼
然后在 src/index.js 中引入 index.css :
//引入index.css
import?'./index.css';
console.log('source?map!!!')
var?a?=?1;
console.log(a,?b);?//這一行肯定會(huì)報(bào)錯(cuò)
復(fù)制代碼
修改 devtool :
devtool:?'cheap-module-source-map'
復(fù)制代碼
打包后,打開瀏覽器,樣式生效,說明 loader 引入成功??梢圆榭?strong style="color: black;">錯(cuò)誤代碼準(zhǔn)確信息和源代碼的錯(cuò)誤位置,但是忽略了具體的列( 因?yàn)槭莃導(dǎo)致報(bào)錯(cuò) ):

生成了 .map 文件,同時(shí),將 loader 的信息也一起打包進(jìn)來:


6.8 總結(jié)
(1)開發(fā)環(huán)境:需要考慮速度快,調(diào)試更友好
速度快( eval>inline>cheap>... )eval-cheap-souce-mapeval-source-map調(diào)試更友好 souce-mapcheap-module-souce-mapcheap-souce-map
最終得出最好的兩種方案 --> eval-source-map(完整度高,內(nèi)聯(lián)速度快) / eval-cheap-module-souce-map(錯(cuò)誤提示忽略列但是包含其他信息,內(nèi)聯(lián)速度快)
(2)生產(chǎn)環(huán)境:需要考慮源代碼要不要隱藏,調(diào)試要不要更友好
內(nèi)聯(lián)會(huì)讓代碼體積變大,所以在生產(chǎn)環(huán)境不用內(nèi)聯(lián) 隱藏源代碼 nosources-source-map全部隱藏(打包后的代碼與源代碼)hidden-source-map只隱藏源代碼,會(huì)提示構(gòu)建后代碼錯(cuò)誤信息
最終得出最好的兩種方案 --> source-map(最完整) / cheap-module-souce-map(錯(cuò)誤提示一整行忽略列)
七、總結(jié)
Source Map 是我們?nèi)粘i_發(fā)過程中必不可少的,它可以幫助我們調(diào)試,定位錯(cuò)誤。盡管它涉及非常多的知識(shí)點(diǎn),例如:VLQ[21]、base64[22]等,但是我們核心關(guān)注的是它的工作原理,以及在打包工具中,如 webpack 等對(duì) Source Map 的應(yīng)用。
Source Map 非常強(qiáng)大,不僅在應(yīng)用于日常開發(fā),還可以做更多的事情,如 性能異常監(jiān)控平臺(tái) 。比如FunDebug[23]這個(gè)網(wǎng)站就是通過 Source Map 還原生產(chǎn)環(huán)境中的壓縮代碼,提供完整的堆棧信息,準(zhǔn)確定位出錯(cuò)誤源碼,幫助用戶快速修復(fù) Bug ,像這樣的案例還有許多。
總之,學(xué)習(xí) Source Map 是非常有必要的。
八、參考
Introduction to JavaScript Source Maps[24] MDN[25] JavaScript Source Map 詳解[26] VLQ[27] base64[28] base64vlq[29] FunDebug[30] 絕了,沒想到一個(gè) source map 居然涉及到那么多知識(shí)盲區(qū)[31] 談?wù)勎沂侨绾潍@得知乎的前端源碼的[32]
關(guān)于本文
來源:IDuxFE
https://juejin.cn/post/7023537118454480904

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
