源映射(Source Map)詳解


一、什么是源映射

為了提高性能,很多站點都會先壓縮 JavaScript 代碼然后上線,

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

這時源映射就登場了。

 

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

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

 

二、如何使用源映射

在 JavaScript 代碼中添加注釋:

//# sourceMappingURL=file.js.map

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

在 Chrome 開發面板(按F12打開)的設置(按F1打開)中,可以通過勾選 "Enable Source Maps" 選項來設置是否需要加載源映射。

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

 

最早瀏覽器是通過 "@ 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,否則可能出現無法添加斷點的問題。

三、如何生成源映射

現在很多生成工具都支持生成源映射,如 Uglify, Grunt, Gulp,可以參考生成工具的文檔。

四、源映射格式詳解

源映射本質是一個 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 個數字:

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

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

這表示映射點的數組長度可能是 1、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

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

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

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

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

六、參考鏈接

Source Map Revision 3 Proposal

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM