大家好,我是若川。今天分享一篇我們經常會忽略的定位原始代碼位置原理的文章。文章不長,例子不錯,可以先收藏,有空時動手試試。
學習源碼系列、年度總結、JS基礎系列
前言
我們知道,代碼上線前要經過壓縮,美化,混淆等步驟,真正上線之后的代碼親媽都不認識。這也可以理解,為了防止別人看到你的源碼發現你的漏洞從而去攻擊你的網頁。
但問題是,如果自己的代碼在線上跑出了bug,連自己都看不懂錯在了哪里。這時候就需要代碼還原工具來幫助我們還原一下代碼,從而找到出錯位置。
這個還原神器就是我們今天的主角source map。今天我們來聊聊它是怎么還原我們的代碼的。
source map在哪
通常,我們用webpack的構建去生成代碼的時候,可以去配置devtool 讓它生成source map,這樣在最后生成的dist就會找到.js.map的文件。
以這個list.js為例
const a = 111;console.log(a);
生成的dist文件
有一行代碼去引用了js.map文件,我們在打開這個文件,可以看到,生成的map文件長這樣
{"version":3,"file":"vote/list/list.c1e192cf.js","sources":["webpack:///webpack/bootstrap","webpack:///./src/pages/vote/list/list.js"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/mpres/zh_CN/htmledition/pages/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n","var a = 111;\nconsole.log(a);"],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AClFA;AACA;;;;A","sourceRoot":""}
別看這段那么多,其實也就這幾個字段:
{"version": 3, // source map的版本"file": "", // 轉換后的文件名"source": [], // 來源文件的代碼"names": [], // 轉換前所有的變量和屬性名"mapping": "" // 記錄位置信息的字符串
}
這其中真正用于定位的就是這個mapping字段里的信息。
source map 是如何還原代碼的
由于上面的例子過于復雜,這里我們用個簡單的例子來說明一下。
源代碼
/* 注釋 */
var name = "abc";
壓縮后的代碼
var name="abc";
//# sourceMappingURL=a.js.map
對應的source-map
{"version":3,"sources":["a.js"],"names":["name"],"mappings":";AACA,IAAIA,KAAO","file":"a.js","sourcesContent":["/* 注釋 */\nvar name = \"abc\";"]
}
接下來我們看看這個;AACA,IAAIA,KAAO
在說什么。
其中分號;代表一個空行。逗號,代表一個位置。
AACA標明的是var的位置,它是先經過VLQ編碼,在經過base64編碼而成。VLQ跟base64不懂都沒什么關系,我們這里知道它是一種編碼方式即可。
下面舉個栗子,來看看188經過VLQ 與 base64編碼的過程及結果。
首先188的二進制表示是10111100,不能滿足VLQ 6字節的要求,所以這里將它拆成兩部分,在交換一下。
接著1100前面補1,因為后面還有一塊block。結尾補0,因為188是一個正數。第一段最終轉出來就是111000。
在看后面一段1011,我們只需要在前面補兩個0。其中第一個0表示沒有block在后面了,第二個0是因為不足5位左邊補0。第二段最終轉出來就是001011。
111000對應VLQ就是56,然后在對應base64的4。
而001011對應VLQ是11,在對應base64就是L。
AACA轉回來就是逆方向操作。
有點麻煩,我估計你們也不想算,所以我們先用一個庫vlq來幫忙轉換一下它。
打印結果如下:
可以看到AACA解析出來是0010。其中,
第一位,表示這個位置在(轉換后的代碼的)的第幾列。
第二位,表示這個位置屬于sources屬性中的哪一個文件。
第三位,表示這個位置屬于轉換前代碼的第幾行。
第四位,表示這個位置屬于轉換前代碼的第幾列。
這里有兩點需要注意:
1、位置都是以0為基數算起。
2、計算的是相對與前一個位置的相對位置。
所以這個0010,這里就代表著var在壓縮后代碼的第0列,對應第0個源碼文件的1行0列。
同理,第二個IAAIA轉過來是4004,這個是相對于上一個字符的位置,所以我們需要加起來,也就是4014。說明name在壓縮后代碼的第4列,對應第0個源碼文件的的第1行,第4列。
第三個KAAO轉過來是5007,相加前面的也就是90111,說明abc是轉換后的代碼第9列,對應第0個源碼文件的的第1行,第11列。
再來個栗子
這是轉換前的scipt.js代碼
這是編譯后的代碼scipt-transpiled.js
這個是source map 文件
這個mapping對應回轉換后的代碼就長這樣:
大家可以自行分析一下這個例子 。
以上,就是今天分享的source map 所有內容。
參考文檔:
https://medium.com/@trungutt/yet-another-explanation-on-sourcemap-669797e418ce
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信 ruochuan12 拉你進群。
點擊上方卡片關注我、加個星標
一個愿景是幫助5年內前端人成長的公眾號
可加我個人微信?ruochuan12,長期交流學習
推薦閱讀
我在阿里招前端,該怎么幫你(可進面試群)
2年前端經驗,做的項目沒技術含量,怎么辦?
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。
今日話題
我經常推薦學會使用技術完成開發的同時也要多要研究原理。其實就是不停留在只會使用的層面,重基礎懂原理,知其然知其所以然。歡迎分享、收藏、點贊、在看我的公眾號文章~