source map如何帮我们定位源码

1,071 阅读6分钟

前言

我们知道,代码上线前要经过压缩,美化,混淆等步骤,真正上线之后的代码亲妈都不认识。这也可以理解,为了防止别人看到你的源码发现你的漏洞从而去攻击你的网页。

但问题是,如果自己的代码在线上跑出了bug,连自己都看不懂错在了哪里。这时候就需要代码还原工具来帮助我们还原一下代码,从而找到出错位置。

这个还原神器就是我们今天的主角source map。今天我们来聊聊它是怎么还原我们的代码的。

source map在哪

通常,我们用webpack的构建去生成代码的时候,可以去配置devtool 让它生成source map,这样在最后生成的dist就会找到.js.map的文件。

以这个list.js为例


const a = 111;

console.log(a);

生成的dist文件

/***/1:
/***/(function(module,exports,__webpack_require__)){
    module.exports=__webpack_require__("779063cf0aeaac0d5b8");

/****/}),
/***/"779063cf0aeaac0d5b8":
/****/(function(module,exports){
    var a=111;
    console.log(a);
/****/})
/****/}),
//#
 sourceMappingURL=list.cle192cf.js.map

有一行代码去引用了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编码的过程及结果。

微信图片_20210624225058.png 首先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。

微信图片_20210624225523 (1).png AACA转回来就是逆方向操作。

有点麻烦,我估计你们也不想算,所以我们先用一个库vlq来帮忙转换一下它。

微信图片_20210624225906.png 打印结果如下:

微信图片_20210624225936.png

可以看到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代码

微信图片_20210624230001.png 这是编译后的代码scipt-transpiled.js

微信图片_20210624230023.png 这个是source map 文件

微信图片_20210624230058.png 这个mapping对应回转换后的代码就长这样:

微信图片_20210624230120.png 大家可以自行分析一下这个例子 。

以上,就是今天分享的source map 所有内容。

参考文档:

medium.com/@trungutt/y…