sourcemap知多少

想必你們都對這一段截圖耳熟能詳: html

image

由於檢測並加載了sourcemap,因此能夠直接定位到編譯前代碼的特定位置。因此用一句話解釋sourcemap很簡單,就是一段維護了先後代碼映射關係的json描述文件。但若是細究其內容,能夠泡杯茶說一段故事了~html5

歷史淵源

在2009年google的一篇 文章 中,在介紹Cloure Compiler(一款js壓縮優化工具,可類比於uglify-js)時,google也順便推出了一款調試工具:firefox插件Closure Inspector,以方便調試編譯後代碼。這就是sourcemap的最初代啦!webpack

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.git

2010年,在第二代即 Closure Compiler Source Map 2.0 中,sourcemap肯定了統一的json格式及其他規範,已幾乎具備如今的雛形。最大的差別在於mapping算法,也是sourcemap的關鍵所在。第二代中的mapping已決定使用base 64編碼,可是算法同如今有出入,因此生成的.map相比如今要大不少。es6

2011年,第三代即 Source Map Revision 3.0 出爐了,這也是咱們如今使用的sourcemap版本。從文檔的命名看來,此時的sourcemap已脫離Clousre Compiler,演變成了一款獨立工具,也獲得了瀏覽器的支持。這一版相較於二代最大的改變是mapping算法的壓縮換代,使用VLQ編碼生成base64前的mapping,大大縮小了.map文件的體積。github

Sourcemap發展史的有趣之處在於,它做爲一款輔助工具被開發出來。最後它輔助的對象日漸式微,而它卻成爲了技術主體,被寫進了瀏覽器中。web

.map文件

{
    "version": 3,
    "sources": ["test.es6.js"],
    "names": [],
    "mappings": ";;AAAA,IAAM,MAAM,GAAG,SAAT,MAAM,CAAI,CAAC;SAAK,CAAC,GAAG,CAAC;CAAA,CAAC",
    "file": "test.js",
    "sourcesContent": ["const square = (x) => x * x;"]
}
複製代碼

這就是一段簡單的map文件,其中算法

  • version:sourcemap版本(如今都是v3)
  • sources:源文件列表(若是是打包成bundle.js的,那源文件就有很長一堆了)
  • sourcesContent: 原文件內容(若是是webpack壓縮的,能夠看到這裏顯示的代碼是經webpack鏈接後的__webpack_require__那一層)
  • names: 原變量名與屬性名(壓縮時可能會改變變量名稱)
  • mapping:映射json
  • file:編譯後文件

接下來,就來詳細瞅瞅主要元素mapping把!json

mapping策略全攻略

html5rocks的sourcemap教程 (這也是最經典的一篇sourcemap博文了)所說,數組

sourcemap v1最開始生成的sourcemap文件大概有轉換後文件的10倍大。sourcemap v2將之減小了50%,v3又在v2的基礎上減小了50%。因此目前133k的文件對應的sourcemap文件大小大概在300k左右。

那麼它是如何用最精簡的方式維護現文件到源文件的映射的呢?對於整個映射而言,重要的幾要素無非是「原文件、原行數、原列數、原變量名」,因此對映射算法的優化,實質也就是對這一段數據的存儲優化。

我發現了一篇很是好的文章How do source maps work,做者經過簡單的示例解釋了map的大體工做原理。接下來我會介(梗)紹(概)下做者的幾個要點。

sourcemap示例分析

原代碼:

const square = (x) => x * x;
複製代碼

Babel編譯後代碼:

"use strict";

var square = function square(x) {
  return x * x;
};

//# sourceMappingURL=test.js.map
複製代碼

Mappings(接下來的分析都會以這份mapping爲準喔):

";;AAAA,IAAM,MAAM,GAAG,SAAT,MAAM,CAAI,CAAC;SAAK,CAAC,GAAG,CAAC;CAAA,CAAC"
複製代碼

如上,能夠看到這段mappings有幾個關鍵元素:分號; 逗號, 及分隔出的一串一眼看不懂的字母。其中:

  • 分號:表明分隔行
  • 逗號:表明分隔列
  • 上文四字字母:通過base64轉換事後的VLQ編碼,表明了每一個segment的原位置定位,好比var, square, =…

附:【base64 VLQ編碼 - 十進制轉換表 】(先僅供參考,具體轉換規則見下文VLQ編碼部分)

image

一段segment中的VLQ編碼,其實既能夠是4位,也能夠是5位。每一位都有特定含義:

  • 第一位[0]:表明在轉換後文件的第幾列(相對於前一個position的相對位置)
  • 第二位[1]:表明源文件在sources裏對應的文件序列// 上述mapping的第二位都是A,由於原文件都是第0位
  • 第三位[2]: 表明在源文件的第幾行 // 上述mapping的第三位都是A,由於源文件都在也只有第1行
  • 第四位[3]:表明在轉換後文件的第幾列(相對於前一個position的相對位置)
  • 第五位[4]:表明屬於names裏的哪個變量

帶着解釋,再看如下兩個問題

上述mapping爲何以;;開頭呢?

上述mapping以;;開頭,是由於babel編譯後新增的use strict及下面空行沒有隊對應的原文~

上述mapping的1、二個segement [AAAA, IAAM] 是怎麼map到原文件位置的?

[AAAA,IAAM] 轉換爲VLQ編碼是[0000, 4006]。第二個segment裏的4表明square在轉換後文件的第4列,而6表明square在現文件的第6列。

0|1|2|3|4|  
v|a|r| |s|q|u|a|r|e|  
0|1|2|3|4|5|6|  
c|o|n|s|t| |s|q|u|a|r|e|
複製代碼

以上,相信再看見一段mappings時,不說能立馬知道它表明了哪一段源文件,但至少不會兩眼一抹黑了~

其實從以上過程讀來,在知道了規則的狀況下,反編譯一段編碼老是容易的。難的實際上是創造和優化規則的過程。好比將源文件名放在一個數組中,經過存儲原文件在數組中的index,而不是源文件name自己;好比存儲的column index是相對位置而不是絕對位置;好比經過;分隔行經過,分隔列,而不是記錄行號和列號;好比爲何選擇了經base64轉換的VLQ編碼…正是這些微小的一點點比對出來的細節,才能積少成多,將mapping文件最大化的壓縮。

VLQ(Variable Length Quantities)編碼

要了解VLQ編碼,原本我是拒絕的…我也許永遠也不會用到…可是一想來都來了,不如也順便了解一下叭!就當作陶冶情操呢~

So..瞭解完成後,感受文本編碼和網絡技術一般會講到傳遞xx包要怎麼封裝一串數據同樣,重點都是差很少的。一是如何分割數字(即表示連續),好比1和17在一塊兒組成117的時候,怎麼來表示這是1和17而不是一百一十七。二是如何表示正負。

一個VLQ小節有6位(因此恰好適合經過base64轉換成字符呀),第一位表明連續,最後一位通常表明正負,因此中間至少有4位能夠表示value。

image

如上,能夠看到-15~15均可以用一個VLQ小節來表示。當|Number|>15時,則須要用到多個VLQ小節。另外,正負只需用第一小節的最後一位表示。因此多個小節時,第>1個小節的第6位能夠不表明正負,也表明內容。

若是本身有須要使用VLQ編碼的話,則能夠參考市面上現成的庫~好比GitHub - Rich-Harris/vlq

番外:webpack的幾種sourcemap選項

用webpack的人都知道,它有好多種sourcemap選項Devtool | webpack。在沒了解sourcemap的深層本質以前,我對這幾個選項都只知其一;不知其二,就死記硬揹着cheap-module-eval-source-map是最優解╮( ̄▽ ̄")╭

其實這幾個選項,都是關鍵字cheap, module, eval, inline, hidden + sourcemap的排列組合,只要清楚各自的做用及意義就行了。

  • cheap:一看就是簡略版的source-map,cheap不包含兩樣東西。①不映射列名②不映射loader編譯前的代碼。若是存在loader,好比babel或者jsx,那麼就存在雙重sourcemap了。生成後代碼 => loader處理後代碼 => 源代碼。
  • module:映射到loader處理前代碼。通常和cheap搭配使用,這樣cheap-module屬性就僅僅是關閉列映射了。
  • eval:eval自己指經過每個模塊主體都被eval()包裹並執行。而eval-source-map則意味着在每一個模塊下,sourceMappingUrl都經過DataURI的方式加載。
    image
  • inline:直接放在編譯後js底部的sourceMappingUrl也是DataURI形式的。
    image
  • hidden:隱藏屬性。編譯後js底部不會標明sourceMappingUrl,須要手動加載。可用於錯誤監控服務器上報錯誤日誌時肯定位置etc。

對我而言,行數必需列數沒必要需,映射loader編譯前的代碼也必需,再加上eval在從新構建時比較快,因此cheap-module-eval-source-map確實是一個不錯的選擇。

另外,可別把source-map帶到線上去了呀,那會給人人帶來視奸你代碼的機會。

最後,當在開發模式下debug代碼時,要記得感謝sourcemap和瀏覽器的支持。每個小小的細節和好用之處,都離不開前人長久的努力呀。

Refs & Recommend Readings:

相關文章
相關標籤/搜索