深入淺出 Source Map
作者:IDuxFE
https://juejin.cn/post/7023537118454480904
一、什麼是 Source Map
通俗的來說, Source Map
就是一個信息文件,裏面存儲了代碼打包轉換後的位置信息,實質是一個 json
描述文件,維護了打包前後的代碼映射關係。關於 Source Map
的解釋可以看下 Introduction to JavaScript Source Maps[7]。
我們線上的代碼一般都是經過打包的,如果線上代碼報錯了,想要調試起來,那真是很費勁了,比如下面這個例子:
使用打包工具 Webpack
,編譯這一段代碼
console.log('source map!!!')
console.log(a); //這一行肯定會報錯
瀏覽器打開後的效果:
點擊進入報錯文件之後:
這根本沒法找到具體位置以及原因,所以這個時候, Source Map
的作用就來了, Webpack
構建代碼中,開啓 Source Map
:
然後重新執行構建,再次打開瀏覽器:
可以發現,可以成功定位到具體的報錯位置了,這就是 Source Map
的作用。需要注意一點的是, Source Map
並不是 Webpack
特有的,其他打包工具同樣支持 Source Map
,打包工具只是將 Source Map
這項技術通過配置化的方式引入進來。關於打包工具,下文會有介紹。
二、Source Map 的作用
上面的案例只是 Source Map
的初體驗,現在來說一下它的作用,我們爲什麼需要 Source Map
?
阮一峯老師的 JavaScript Source Map 詳解 [8] 指出,JavaScript 腳本正變得越來越複雜。大部分源碼(尤其是各種函數庫和框架)都要經過轉換,才能投入生產環境。
常見的源碼轉換,主要是以下三種情況:
-
壓縮,減小體積
-
多個文件合併,減少 HTTP 請求數
-
其他語言編譯成 JavaScript
這三種情況,都使得實際運行的代碼不同於開發代碼,除錯( debug
)變得困難重重,所以才需要 Source Map
。結合上面的例子,即使打包過後的代碼,也可以找到具體的報錯位置,這使得我們 debug
代碼變得輕鬆簡單,這就是 Source Map
想要解決的問題。
三、如何生成 Source Map
各種主流前端任務管理工具,打包工具都支持生成 Source Map
。
3.1 UglifyJS
UglifyJS
是命令行工具,用於壓縮 JavaScript
代碼
安裝 UglifyJS
:
npm install uglify - js - g
壓縮代碼的同時生成 Source Map
:
uglifyjs app.js - o app.min.js--source - map app.min.js.map
Source Map
相關選項:
--source - map Source Map的文件的路徑和名稱
--source - map - root 源文件的路徑
--source - map - url //#sourceMappingURL的路徑。 默認爲--source-map指定的值。
--source - map - include - sources 是否將源代碼的內容添加到sourcesContent數組
--source - map - inline 是否將Source Map寫到壓縮代碼的最後一行
-- in -source - map 輸入Source Map, 當源文件已經經過變換時使用
3.2 Grunt
Grunt
是 JavaScript
項目構建工具
配置 grunt-contrib-uglify
插件以生成 Source Map
:
grunt.initConfig({
uglify: {
options: {
sourceMap: true
}
}
});
使用 grunt-usemin
打包源碼時, grunt-usemin
會依次調用 grunt-contrib-concat[9] 與 grunt-contrib-uglify[10] 對源碼進行打包和壓縮。因此都需要進行配置:
grunt.initConfig({
concat: {
options: {
sourceMap: true
}
},
uglify: {
options: {
sourceMap: true,
sourceMapIn: function(uglifySource) {
return uglifySource + '.map';
},
}
}
});
3.3 Gulp
Gulp
是 JavaScript
項目構建工具
使用 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'));
});
3.4 SystemJS
SystemJS
是模塊加載器
使用 SystemJS Build Tool[12] 生成 Source Map
:
builder.bundle('myModule.js', 'outfile.js', {
minify: true,
sourceMaps: true
});
sourceMapContents
選項可以指定是否將源碼寫入Source Map
文件
3.5 Webpack
Webpack
是前端打包工具(本文案例都會使用該打包工具)。在其配置文件 webpack.config.js
中設置 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"
};
- devtool 有 20 多種不同取值,分別生成不同類型的
Source Map
,可以根據需要進行配置。下文會詳細介紹,這裏不再贅述。
3.6 Closure Compiler
利用 Closure Compiler[14] 生成
四、如何使用 Source Map
生成 Source Map
之後,一般在瀏覽器中調試使用,前提是需要開啓該功能,以 Chrome
爲例:
打開開發者工具,找到 Settins
:
勾選以下兩個選項:
再回到上面的案例中,源代碼文件變成了 index.js
,點擊進入後顯示真實的源代碼,即說明成功開啓並使用了 Source Map
五、Source Map 的工作原理
還是上面這個案例,執行打包後,生成 dist
文件夾,打開 dist/bundld.js
:
可以看到尾部有這句註釋:
//# sourceMappingURL=bundle.js.map
正是因爲這句註釋,標記了該文件的 Source Map
地址,瀏覽器纔可以正確的找到源代碼的位置。sourceMappingURL
指向 Source Map
文件的 URL
。
除了這種方式之外,MDN[15] 中指出,可以通過 response header
的 SourceMap: <url>
字段來表明。
> SourceMap: /path/to/file.js.map
dist
文件夾中,除了 bundle.js
還有 bundle.js.map
,這個文件纔是 Source Map
文件,也是 sourceMappingURL
指向的 URL
-
version
:Source map
的版本,目前爲v3
。 -
sources
:轉換前的文件。該項是一個數組,表示可能存在多個文件合併。 -
names
:轉換前的所有變量名和屬性名。 -
mappings
:記錄位置信息的字符串,下文會介紹。 -
file
:轉換後的文件名。 -
sourceRoot
:轉換前的文件所在的目錄。如果與轉換前的文件在同一目錄,該項爲空。 -
sourcesContent
:轉換前文件的原始內容。
5.1 關於 Source map 的版本
在 2009 年 Google
的一篇文章中,在介紹 Cloure Compiler
時, Google
也趁便推出了一款調試東西:Firefox
插件 Closure Inspector
,以便利調試編譯後代碼。這便是 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
招認了共同的 JSON
格式及其他標準,已幾乎具有現在的雛形。最大的差異在於 mapping
算法,也是 Source Map
的要害地址。第二代中的 mapping
已決定運用 base 64
編碼,可是算法同現在有收支,所以生成的 .map
比較現在要大許多。2011 年,第三代即 Source Map Revision 3 Proposal[16] 出爐了,這也是咱們現在運用的 Source Map
版別。從文檔的命名看來,此刻的 Source Map
已脫離 Clousre Compiler
,演化成了一款獨立東西,也得到了瀏覽器的支撐。這一版相較於二代最大的改動是 mapping
算法的緊縮換代,運用 VLQ[17] 編碼生成 base64[18] 前的 mapping
,大大縮小了 .map
文件的體積。
Source Map
發展史的詼諧之處在於,它作爲一款輔佐東西被開發出來。畢竟它輔佐的方針日漸式微,而它卻成爲了技能主體,被寫進了瀏覽器中。
Source Map V1 最初步生成的 Source Map 文件大概有轉化後文件的 10 倍大。Source Map V2 將之減少了 50%,V3 又在 V2 的基礎上減少了 50%。所以現在 133k 的文件對應的 Source Map 文件鉅細大概在 300k 左右。
5.2 關於 mappings 屬性
爲了避免干擾,將案例改成如下不報錯的情況:
var a = 1;
console.log(a);
打包編譯的後 bundle.js
文件:
/******/
(() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
var a = 1;
console.log(a);
/******/
})();
//# sourceMappingURL=bundle.js.map
打包編譯後的 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": ""
}
可以看到 mappings
屬性的值是:AAAA; AACA, c
,要想說清楚這個東西,需要先解釋一下它的組成結構。這是一個字符串,它分成三層:
-
第一層是行對應,以分號(; )表示,每個分號對應轉換後源碼的一行。所以,第一個分號前的內容,就對應源碼的第一行,以此類推。
-
第二層是位置對應,以逗號(, )表示,每個逗號對應轉換後源碼的一個位置。所以,第一個逗號前的內容,就對應該行源碼的第一個位置,以此類推。
-
第三層是位置轉換,以 VLQ 編碼 [19] 表示,代表該位置對應的轉換前的源碼位置。
在回到源代碼,就可以分析出:
-
因爲源代碼中有兩行,所以有一個分號,分號前後表示了第一行和第二行。即
mappings
中的AAAA
和AACA,c
。 -
分號後面表示第二行,也就是代碼
console.log(a);
可以拆分出兩個位置,分別是console
和log(a)
,所以存在一個逗號。即AACA,c
中的AACA
和c
。
總結,就是轉換後的源碼分成兩行,第一行有一個位置,第二行有兩個位置。
至於這個 AAAA
, AAcA
等字母是怎麼來的,可以參考阮一峯老師的 JavaScript Source Map 詳解 [20] 有作詳細的介紹。筆者自己的理解是:
AAAA
和 AAcA
以及 c
都是代表了位置,正常來說,每個位置最多由 5 個字母組成,5 個字母的含義分別是:
-
第一位,表示這個位置在(轉換後的代碼的)的第幾列。
-
第二位,表示這個位置屬於 sources 屬性中的哪一個文件。
-
第三位,表示這個位置屬於轉換前代碼的第幾行。
-
第四位,表示這個位置屬於轉換前代碼的第幾列。
-
第五位,表示這個位置屬於 names 屬性中的哪一個變量。
這裏轉換後最多隻有 4 個字母,是因爲沒有 names
屬性。
每一個位置都可以用 VLQ 編碼 [21] 轉換,形成一種映射關係。可以在這個網站 [22] 自己轉換測試,將 AAAA; AACA, c
轉換後的結果:
可以得到兩組數據:
[0, 0, 0, 0]
[0, 0, 1, 0], [14]
數字都是從 0
開始的,拿位置 AAAA
舉例,轉換後得到 [0, 0, 0, 0]
,所以代表的含義分別是;
-
壓縮代碼的第一列。
-
第一個源代碼文件,即
index.js
。 -
源代碼的第一行。
-
源代碼第一列
通過以上解析,我們就能知道源代碼中 var a = 1;
在打包後文件中,即 bundle.js
的具體位置了。
六、Webpack 中的 Source Map
上文介紹了 Source Map
的作用,原理等。現在說一下打包工具 WebPack
中對 Source Map
的應用,畢竟我們在開發中,都離不開它。
上文有說道,只需要在 webpack.config.js
文件中配置 devtool
就可以使用 Source Map
,這個 devtool
具體的值有哪些,可以參考 webpack devtool[23]
的介紹,官方羅列了 20 幾種類型,我們當然不能全部都記住,可以記住幾個關鍵的:
建議以下 7 種可選方案:
-
source-map:外部。可以查看錯誤代碼準確信息和源代碼的錯誤位置。
-
inline-source-map:內聯。只生成一個內聯
Source Map
,可以查看錯誤代碼準確信息和源代碼的錯誤位置 -
hidden-source-map:外部。可以查看錯誤代碼準確信息,但不能追蹤源代碼錯誤,只能提示到構建後代碼的錯誤位置。
-
eval-source-map:內聯。每一個文件都生成對應的
Source Map
,都在eval
中,可以查看錯誤代碼準確信息 和 源代碼的錯誤位置。 -
nosources-source-map:外部。可以查看錯誤代碼錯誤原因,但不能查看錯誤代碼準確信息,並且沒有任何源代碼信息。
-
cheap-source-map:外部。可以查看錯誤代碼準確信息和源代碼的錯誤位置,只能把錯誤精確到整行,忽略列。
-
cheap-module-source-map:外部。可以錯誤代碼準確信息和源代碼的錯誤位置,
module
會加入loader
的Source Map
。
內聯和外部的區別:
-
外部生成了文件(
.map
),內聯沒有。 -
內聯構建速度更快。
以下通過具體的案例演示上面的 7 種類型:
首先,將案例改成報錯狀態,爲了體現列的情況,將源代碼修改成如下:
console.log('source map!!!')
var a = 1;
console.log(a, b); //這一行肯定會報錯
6.1 source-map
devtool: 'source-map'
編譯後,可以查看錯誤代碼準確信息和源代碼的錯誤位置:
生成了 .map
文件:
6.2 inline-source-map
devtool: 'inline-source-map'
編譯後,可以查看錯誤代碼準確信息和源代碼的錯誤位置:
但是沒有生成 .map文件
,而是以 base64
的形式插入到 sourceMappingURL
中:
6.3 hidden-source-map
devtool: 'hidden-source-map'
編譯後,可以查看錯誤代碼準確信息,但是無法查看源代碼的位置:
生成了 .map
文件:
6.4 eval-source-map
devtool: 'eval-source-map'
編譯後,可以查看錯誤代碼準確信息和源代碼的錯誤位置:
但是沒有生成 .map文件
,而是在 eval函數
中,包括 sourceMappingURL
:
6.5 nosources-source-map
devtool: 'nosources-source-map'
編譯後,可以查看無法查看錯誤代碼的準確位置和源代碼的錯誤位置,只能提示錯誤原因:
生成了 .map
文件:
6.6 cheap-source-map
devtool: 'cheap-source-map'
編譯後,可以查看錯誤代碼準確信息和源代碼的錯誤位置,但是忽略了具體的列( 因爲是b導致報錯
):
生成了 .map
文件:
6.7 cheap-module-source-map
因爲需要 module
,所以案例中增加 loader
:
module: {
rules: [{
test: /\.css$/,
use: [
// style-loader:創建style標籤,將js中的樣式資源插入進去,添加到head中生效
'style-loader',
// css-loader:將css文件變成commonjs模塊加載到js中,裏面內容是樣式字符串
'css-loader'
]
}]
}
在 src
目錄下新建 index.css
文件,添加樣式代碼:
body {
margin: 0;
padding: 0;
height: 100%;
background-color: pink;
}
然後在 src/index.js
中引入 index.css
:
//引入index.css
import './index.css';
console.log('source map!!!')
var a = 1;
console.log(a, b); //這一行肯定會報錯
修改 devtool
:
devtool: 'cheap-module-source-map'
打包後,打開瀏覽器,樣式生效,說明 loader
引入成功。可以查看錯誤代碼準確信息和源代碼的錯誤位置,但是忽略了具體的列( 因爲是b導致報錯
):
生成了 .map
文件,同時,將 loader
的信息也一起打包進來:
6.8 總結
(1)開發環境:需要考慮速度快,調試更友好
- 速度快 (
eval
>inline
>cheap
>... )
-
eval-cheap-souce-map
-
eval-source-map
- 調試更友好
-
souce-map
-
cheap-module-souce-map
-
cheap-souce-map
最終得出最好的兩種方案 --> eval-source-map(完整度高,內聯速度快) / eval-cheap-module-souce-map(錯誤提示忽略列但是包含其他信息,內聯速度快)
(2)生產環境:需要考慮源代碼要不要隱藏,調試要不要更友好
-
內聯會讓代碼體積變大,所以在生產環境不用內聯
-
隱藏源代碼
-
nosources-source-map
全部隱藏(打包後的代碼與源代碼) -
hidden-source-map
只隱藏源代碼,會提示構建後代碼錯誤信息
最終得出最好的兩種方案 --> source-map(最完整) / cheap-module-souce-map(錯誤提示一整行忽略列)
七、總結
Source Map
是我們日常開發過程中必不可少的,它可以幫助我們調試,定位錯誤。儘管它涉及非常多的知識點,例如:VLQ[24]、base64[25] 等,但是我們核心關注的是它的工作原理,以及在打包工具中,如 webpack
等對 Source Map
的應用。
Source Map
非常強大,不僅在應用於日常開發,還可以做更多的事情,如 性能異常監控平臺
。比如 FunDebug[26] 這個網站就是通過 Source Map
還原生產環境中的壓縮代碼,提供完整的堆棧信息,準確定位出錯誤源碼,幫助用戶快速修復 Bug
,像這樣的案例還有許多。
總之,學習 Source Map
是非常有必要的。
向下滑動查看
參考
-
Introduction to JavaScript Source Maps[27]
-
MDN[28]
-
JavaScript Source Map 詳解 [29]
-
VLQ[30]
-
base64[31]
-
base64vlq[32]
-
FunDebug[33]
-
絕了,沒想到一個 source map 居然涉及到那麼多知識盲區 [34]
-
談談我是如何獲得知乎的前端源碼的 [35]
參考資料
[1]
https://juejin.cn/column/6992030342987120677: https://juejin.cn/column/6992030342987120677
[2]
https://juejin.cn/post/6992371845349507108: https://juejin.cn/post/6992371845349507108
[3]
https://juejin.cn/post/7005351791671902244: https://juejin.cn/post/7005351791671902244
[4]
https://juejin.cn/post/7013149595068792845: https://juejin.cn/post/7013149595068792845
[5]
https://juejin.cn/post/7021687704999952415: https://juejin.cn/post/7021687704999952415
[6]
https://juejin.cn/user/932815872994359: https://juejin.cn/user/932815872994359
[7]
https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/: https://link.juejin.cn?target=https%3A%2F%2Fwww.html5rocks.com%2Fen%2Ftutorials%2Fdevelopertools%2Fsourcemaps%2F
[8]
http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html: https://link.juejin.cn?target=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2013%2F01%2Fjavascript_source_map.html
[9]
https://github.com/gruntjs/grunt-contrib-concat: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fgruntjs%2Fgrunt-contrib-concat
[10]
https://github.com/gruntjs/grunt-contrib-uglify: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fgruntjs%2Fgrunt-contrib-uglify
[11]
https://github.com/floridoo/gulp-sourcemaps: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Ffloridoo%2Fgulp-sourcemaps
[12]
https://github.com/systemjs/builder: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fsystemjs%2Fbuilder
[13]
https://webpack.js.org/configuration/devtool/: https://link.juejin.cn?target=https%3A%2F%2Fwebpack.js.org%2Fconfiguration%2Fdevtool%2F
[14]
https://github.com/google/closure-compiler: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fgoogle%2Fclosure-compiler
[15]
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/SourceMap: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FHTTP%2FHeaders%2FSourceMap
[16]
https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#: https://link.juejin.cn?target=https%3A%2F%2Fdocs.google.com%2Fdocument%2Fd%2F1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k%2Fedit%23
[17]
https://en.wikipedia.org/wiki/Variable-length_quantity: https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FVariable-length_quantity
[18]
https://zh.wikipedia.org/zh-cn/Base64: https://link.juejin.cn?target=https%3A%2F%2Fzh.wikipedia.org%2Fzh-cn%2FBase64
[19]
https://en.wikipedia.org/wiki/Variable-length_quantity: https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FVariable-length_quantity
[20]
http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html: https://link.juejin.cn?target=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2013%2F01%2Fjavascript_source_map.html
[21]
https://en.wikipedia.org/wiki/Variable-length_quantity: https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FVariable-length_quantity
[22]
https://www.murzwin.com/base64vlq.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.murzwin.com%2Fbase64vlq.html
[23]
https://webpack.docschina.org/configuration/devtool/#root: https://link.juejin.cn?target=https%3A%2F%2Fwebpack.docschina.org%2Fconfiguration%2Fdevtool%2F%23root
[24]
https://en.wikipedia.org/wiki/Variable-length_quantity: https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FVariable-length_quantity
[25]
https://zh.wikipedia.org/zh-cn/Base64: https://link.juejin.cn?target=https%3A%2F%2Fzh.wikipedia.org%2Fzh-cn%2FBase64
[26]
https://www.fundebug.com/: https://link.juejin.cn?target=https%3A%2F%2Fwww.fundebug.com%2F
[27]
https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/: https://link.juejin.cn?target=https%3A%2F%2Fwww.html5rocks.com%2Fen%2Ftutorials%2Fdevelopertools%2Fsourcemaps%2F
[28]
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/SourceMap: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FHTTP%2FHeaders%2FSourceMap
[29]
http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html: https://link.juejin.cn?target=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2013%2F01%2Fjavascript_source_map.html
[30]
https://en.wikipedia.org/wiki/Variable-length_quantity: https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FVariable-length_quantity
[31]
https://zh.wikipedia.org/zh-cn/Base64: https://link.juejin.cn?target=https%3A%2F%2Fzh.wikipedia.org%2Fzh-cn%2FBase64
[32]
https://www.murzwin.com/base64vlq.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.murzwin.com%2Fbase64vlq.html
[33]
https://www.fundebug.com/: https://link.juejin.cn?target=https%3A%2F%2Fwww.fundebug.com%2F
[34]
https://juejin.cn/post/6963076475020902436: https://juejin.cn/post/6963076475020902436
[35]
https://zhuanlan.zhihu.com/p/26033573: https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F26033573
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4tQAy9U53zlSbSEuQB-SNw