源映射(Source Map)詳解

1、什麼是源映射

爲了提升性能,不少站點都會先壓縮 JavaScript 代碼而後上線,javascript

但若是代碼運行時出現錯誤,瀏覽器只會顯示在已壓縮的代碼中的位置,很難肯定真正的源碼錯誤位置。html

這時源映射就登場了。html5

 

源映射(Source Map)是一種數據格式,它存儲了源代碼和生成代碼之間的位置映射關係。java

源映射通常使用 .map 擴展名,源映射本質是一個 JSON 文本文檔,其 MIME 類型也通常設爲 application/json。git

 

2、如何使用源映射

在 JavaScript 代碼中添加註釋:github

//# sourceMappingURL=file.js.map

瀏覽器(最新版 Chrome、Firefox 和 Edge 均支持)就會加載 file.js.map 並自動計算代碼的實際位置。npm

在 Chrome 開發面板(按F12打開)的設置(按F1打開)中,能夠經過勾選 "Enable Source Maps" 選項來設置是否須要加載源映射。json

源映射自己並不會影響代碼的執行,只會在定位錯誤位置時被使用。數組

 

最先瀏覽器是經過 "@ sourceMappingURL" 標記地址的,但這引起了一些引擎和工具的問題(和 IE 的 @cc_on 衝突),因此如今改爲了 "# sourceMappingURL"。瀏覽器

 

NodeJS 中的源映射

NodeJS 在顯示錯誤堆棧時,並不會加載源映射,能夠藉助 source-map-support 這個包實現。

$ npm install source-map-support

而後在代碼頂部加上:

require('source-map-support/register');

這時全部堆棧位置就會被更新成真正的源碼位置。

VSCode 中的源映射

VSCode 支持在調試時使用源映射,在 .vscode/launch.json 中添加:

{
    "configurations": [
        {
            "sourceMaps": true,
            "outDir": "${workspaceRoot}/build"
        }
    ]
}

注意必須設置 outDir,不然可能出現沒法添加斷點的問題。

3、如何生成源映射

如今不少生成工具都支持生成源映射,如 Uglify, Grunt, Gulp,能夠參考生成工具的文檔。

4、源映射格式詳解

源映射本質是一個 JSON,格式如:

{
  version: 3,
  file: 'min.js',
  names: ['bar', 'baz', 'n'], 
sourceRoot: 'http://example.com/www/js/',
sources: [
'one.js', 'two.js'],
sourcesContent: ['', ''],
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' }

主要包括如下字段:

  • version: Source Map(源映射)的版本號,目前統一使用版本 3。
  • file: (可選)生成文件的路徑(相對於 Source Map(源映射) 自己路徑)。
  • names: (可選)全部名稱,如變量名、函數名,下文詳細介紹。
  • sourceRoot: (可選)全部源文件的根路徑(相對於 Source Map(源映射) 自己路徑)
  • sources: 全部源文件的路徑(相對於 sourceRoot)
  • sourcesContent: (可選)全部源文件的內容。
  • mappings: 全部映射點,下文詳細介紹。

其中,全部相對路徑的計算方式和網頁中的相對地址相同。

全部地址能夠是 http:// 開頭的網址或者是本地文件地址。

sources

sources 是一個數組,這意味着一個文件能夠從多個文件生成過來。

不少讀者會以爲這裏出現的路徑太多,幫你們捋一捋:

假如源文件是 xld.js ,經過壓縮生成了 xld.min.js 和 xld.min.js.map ,那麼

在 xld.min.js 中須要經過 // #sourceMappingURL=xld.min.js.map 指定它的源映射。

xld.min.js.map 中須要經過 file: xld.min.js 指定它生效的文件。file 並非必須的字段,該字段只用於檢驗。

xld.min.js.map 中須要經過 sources: ["xld.js"] 指定真正的源文件地址。

sourceRoot

若是 sources 有不少且有相同的前綴,則能夠統一提取到 sourceRoot 中。因此如下是等價的:

{
  sources: ["a/foo.js", "a/bar.js"],
}
{
  sourceRoot : "a",
  sources: ["foo.js", "bar.js"],
}

mappings

mappings 是記錄映射關係的核心。

從表面看,mappings 是一個字符串,裏面由不少看似亂碼的字符組成。

其實 mappings 是一個數組經過必定的方式編碼獲得的,這個數組包含了生成的文件中每行的映射點列表:

 

mappings = [ 
第 1 行的映射點列表,
第 2 行的映射點列表,
...
]

 

每行的映射點列表又是一個數組,包含了該行中全部列的映射點。

 

 

mappings = [ 
    [ 第 1 行第 1 個映射點, 第 1 行第 2 個映射點, ... ] // 第 1 行的映射點列表
    [ 第 2 行第 1 個映射點, 第 2 行第 2 個映射點, ... ] // 第 2 行的映射點列表
    ... 
]

 

每一個映射點又是一個數組,數組中包含了 5 個數字:

[ 生成文件的列, 源文件索引, 源文件行號, 源文件列號, 名稱索引 ]

其中,名稱索引可省略。源文件索引, 源文件行號, 源文件列號也可同時省略,

這表示映射點的數組長度多是 一、4 或 5。

源映射全部行列號都是從 0 開始計數的,本文中所使用的行列號也都是從 0 開始計數的。

舉個例子,好比如今有一個源映射以下:

 1 {
 2   version: 3,
 3   file: 'min.js',
 4   names: ['bar', 'baz', 'n'], 
 5   sourceRoot: 'http://example.com/www/js/',
 6   sources: ['one.js', 'two.js'], 
 7   sourcesContent: ['', ''],
 8   mappings: [
 9     [],
10     [],
11     [
12       [1, 0, 2, 5, 1],
13       [4, 0, 3, 6, 0]   // #13 行
14     ]
15   ]
16 }

以 #13 行數據爲例:#13 行出如今 mappings[2] 裏面,所以它表示生成的文件第 2 行的信息。

#13 行包含了 5 個數字,分別表示生成文件的列 = 4, 源文件索引 = 0, 源文件行號 = 3, 源文件列號 = 6, 名稱索引 = 0。

最終獲得:生成的文件(即 min.js)中,行 2 列 4 的位置是從第 0 個源碼(即 http://example.com/www/js/one.js)中行 3 列 6 的位置生成的,源碼中相關的名稱是 0(即 bar)。

 

經過多個映射點,能夠一必定義生成的文件中每一個位置對應的實際源碼位置。

注意即便指定了某一行列的源碼位置,也沒法推斷相鄰行列的源碼的位置,必須一一添加映射。

 

名稱索引能夠用於快速定位變量和函數壓縮前的名字。

mappings 編碼

爲了節約存儲空間,mappings 會被編碼成一個字符串。

第一步:計算相對值

將映射點中每一個數字替換成當前映射點和上一個映射點相應位置的差,如:

mappings: [
    [
      [1, 0, 2, 5, 1],
      [2, 0, 3, 6, 0]
    ],
    [
      [5, 0, 2, 3, 0]
    ]
]

其中第一個映射點不變,之後每一個映射點上每一個數字都減去上一個映射點(容許跨行)對應位置的數字(若是映射點元素個數不足 5,則省略部分按 0 處理),最後獲得:

mappings: [
    [
      [1, 0, 2, 5, 1],   // 不變
      [1, 0, 1, 1, -1]    // 1 = 2 - 1, 0 = 0 - 0, 1 = 3 - 2, 1 = 6 - 5, 1 = 0 - 1
    ],
    [
      [3, 0, -1, -3, 0]  // 3 = 5 - 2,  0 = 0 - 0, -1 = 2 - 3, -3 = 3 - 6, 0 = 0 - 0
    ]
]

第二步:合併數字

將 mappings 中出現的全部數字寫成一行,不一樣映射點使用,(逗號)隔開,不一樣的行使用;(分號)隔開。

1 0 2 5 1 , 1 0 1 1 1 ; 3, 0 , -1, -3, 0

第三步:編碼數字

對於每一個數字,都使用 VLQ 編碼 將其轉爲字母,具體轉換方式爲:

1. 若是數字是負數,則取其相反數。

2. 將數字轉爲等效的二進制。並在末尾補符號位,若是數字是負數則補 1 不然補 0。

3. 從右往左分割二進制,一次取 5 位,不足的補 0。

4. 將分好的二進制進行倒序。

5. 每段二進制前面補 1,最後一段二進制補 0。這樣每段二進制就是 6 位,其值範圍是 0 到 64(含0,不含64)。

6. 根據 Base64 編碼表將每段二進制轉爲字母:

以 170 爲例,

1)轉爲二進制即:10101010

2)170 是正數,右邊補 0:101010100

3)從右往左分割二進制:10100,  1010。

4)不足 5 位的補 0:01010,  10100

5)倒序:10100, 01010

6)除最後一個前面補 0,其它每段前面補 1:110100, 001010

7)轉爲十進制:52, 10。

8)查表獲得:0K

 

任意一個整數都能經過 VLQ 編碼獲得一串字母和數字表示的文本。

VLQ 編碼最先用於MIDI文件,它能夠很是精簡地表示很大的數值。

第四步:合併結果

將第二步中的每一個數字進行 VLQ 編碼再拼接就是最終的結果。

CAEKC,CACCC;GADHA

5、源映射相關的工具和框架

爲更好理解源映射,可使用 源映射可視化 工具。

爲了處理源映射,可使用官方的 source-map 庫。

同時推薦更好用的庫:source-map-builder,它相比官方的庫性能更高、具備更智能的推導功能。

6、參考連接

Source Map Revision 3 Proposal

http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/

http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html

相關文章
相關標籤/搜索