深入淺出 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 腳本正變得越來越複雜。大部分源碼(尤其是各種函數庫和框架)都要經過轉換,才能投入生產環境。

常見的源碼轉換,主要是以下三種情況:

這三種情況,都使得實際運行的代碼不同於開發代碼,除錯( 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

GruntJavaScript 項目構建工具

配置 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

GulpJavaScript 項目構建工具

使用 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
});
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"
};
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 headerSourceMap: <url> 字段來表明。

> SourceMap: /path/to/file.js.map

dist 文件夾中,除了 bundle.js 還有 bundle.js.map ,這個文件纔是 Source Map 文件,也是 sourceMappingURL 指向的 URL

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 ,要想說清楚這個東西,需要先解釋一下它的組成結構。這是一個字符串,它分成三層:

在回到源代碼,就可以分析出:

  1. 因爲源代碼中有兩行,所以有一個分號,分號前後表示了第一行和第二行。即mappings中的AAAAAACA,c

  2. 分號後面表示第二行,也就是代碼console.log(a);可以拆分出兩個位置,分別是consolelog(a),所以存在一個逗號。即AACA,c中的AACAc

總結,就是轉換後的源碼分成兩行,第一行有一個位置,第二行有兩個位置

至於這個 AAAAAAcA 等字母是怎麼來的,可以參考阮一峯老師的 JavaScript Source Map 詳解 [20] 有作詳細的介紹。筆者自己的理解是:

AAAAAAcA 以及 c 都是代表了位置,正常來說,每個位置最多由 5 個字母組成,5 個字母的含義分別是:

這裏轉換後最多隻有 4 個字母,是因爲沒有 names 屬性。

每一個位置都可以用 VLQ 編碼 [21] 轉換,形成一種映射關係。可以在這個網站 [22] 自己轉換測試,將 AAAA; AACA, c 轉換後的結果:

可以得到兩組數據:

[0, 0, 0, 0]
[0, 0, 1, 0][14]

數字都是從 0 開始的,拿位置 AAAA 舉例,轉換後得到 [0, 0, 0, 0] ,所以代表的含義分別是;

  1. 壓縮代碼的第一列。

  2. 第一個源代碼文件,即index.js

  3. 源代碼的第一行。

  4. 源代碼第一列

通過以上解析,我們就能知道源代碼中 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 種可選方案:

內聯和外部的區別:

  1. 外部生成了文件(.map),內聯沒有。

  2. 內聯構建速度更快。

以下通過具體的案例演示上面的 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)開發環境:需要考慮速度快,調試更友好

  1. eval-cheap-souce-map

  2. eval-source-map

  1. souce-map

  2. cheap-module-souce-map

  3. cheap-souce-map

最終得出最好的兩種方案 --> eval-source-map(完整度高,內聯速度快) / eval-cheap-module-souce-map(錯誤提示忽略列但是包含其他信息,內聯速度快)

(2)生產環境:需要考慮源代碼要不要隱藏,調試要不要更友好

  1. nosources-source-map 全部隱藏(打包後的代碼與源代碼)

  2. 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 是非常有必要的。

向下滑動查看

參考

參考資料

[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