你知道source map如何帮你定位源码么?
The following article is from 前端这件小事 Author daisy
前言
我们知道,代码上线前要经过压缩,美化,混淆等步骤,真正上线之后的代码亲妈都不认识。这也可以理解,为了防止别人看到你的源码发现你的漏洞从而去攻击你的网页。
但问题是,如果自己的代码在线上跑出了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 拉你进群。
推荐阅读
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》多篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,活跃在知乎@若川,掘金@若川。致力于分享前端开发经验,愿景:帮助5年内前端人走向前列。
我经常推荐学会使用技术完成开发的同时也要多要研究原理。其实就是不停留在只会使用的层面,重基础懂原理,知其然知其所以然。欢迎分享、收藏、点赞、在看我的公众号文章~