为什么需要 source map?
在各种前端框架盛行的时代,用户浏览器的运行代码和前端开发写的原始代码已经很不一样了,因为原始代码经过loaders 的预编译(TypeScript / Babel 等)以及压缩工具的混淆、合并、压缩之后已经变得面目全非。
通常 JS 引擎会告诉你,第几行第几列代码出错,但这对于转换后的代码毫无用处。举例来说,jquery-1.11.2.js
压缩后只有 3 行,每行 3 万多字符,所有内部变量都改了名字,你看着报错信息,根本不知道它所对应的原始位置。source map 就是为了解决这个问题而生的。
什么是 source map?
source map 是一个信息文件,里面储存着位置信息。也就是说,转换后代码的每一个位置所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。
source map 格式
{
"version": 3, // source map 的版本。
"sources": ["script.js"], // 转换前的文件,该项是一个数组,表示可能存在多个文件合并。
"names": [], // 转换前的所有变量名和属性名。
"mappings": ";;AAAA,IAAM,QAAQ,GAAG,SAAX,QAAW,CAAA,IAAI;AAAA,yBAAa,IAAb;AAAA,CAArB;;AACA,QAAQ,CAAC,WAAD,CAAR", // 记录位置信息的字符串。
"file": "script-transpiled.js", // 转换后的文件名。
"sourcesContent": ["const sayHello = name => `Hello ${name}`;\nsayHello('sourcemap');"] // 转换前文件的内容,当没有配置 sources 的时候会使用该项。
}
mappings
-
以分号(;)相隔,每一段表示一行位置信息。比如上面的 mappings,第 0 & 1 行没有位置信息,
"AAAA,IAAM,QAAQ,GAAG,SAAX,QAAW,CAAA,IAAI"
表示第 2 行(转换后代码的第几行)位置信息,依此类推。 -
以逗号(,)相隔,每一段表示一个位置信息。比如第 2 行的
AAAA
、IAAM
等。 -
通过 SOURCEMAP V3 MAPPINGS PARSER 转换 mappings 会得到类似下面的数组。 以
"mappings": ";;AAAA,IAAM,WAAW;AAAA,oBAAiB"
为例。// 相对位置:每一行的后一个位置相对前一个位置 2) [0,0,0,0], [4,0,0,6], [11,0,0,11] // AAAA,IAAM,WAAW 3) [0,0,0,0], [20,0,0,17] // AAAA,oBAAiB // 绝对位置:相对位置的基础上每一行加上前一个位置。 2) [0,0,0,0], [4,0,0,6], [15,0,0,17] // [15,0,0,17] = [4,0,0,6](前一个位置)+[11,0,0,11](相对位置) 3) [0,0,0,0], [20,0,0,17]
- 每一行开头的 2) 3) 表示转换后代码第几行。
- 每个数组的位数表示如下:
- 第一位,表示转换后代码的第几列。
- 第二位,表示 sources 属性中的哪一个文件,sources 数组的下标。
- 第三位,表示转换前代码的第几行。
- 第四位,表示转换前代码的第几列。
- 第五位,表示 names 属性中的哪一个变量,names 数组的下标,不是必需的。
- 举例:2) [15,0,0,17],它表示转换后代码的第 2 行第 15 列,对应转换前文件 sources[0] 中的 names[0] 变量,该位置是第 0 行第 17 列。
VLQ[Variable-length quantity]编码
VLQ 编码是变长的。如果整数值在-15 到 +15 之间(含两个端点),用一个字符表示;超出这个范围,就需要用多个字符表示。它规定,每个字符使用6个二进制位,正好可以借用 Base 64 编码的字符表。
最后一位(最低位)的含义,取决于这 6 个位是否是某个数值的 VLQ 编码的第一个字符。如果是的,这个位代表"符号"(sign),0为正,1为负(Source map的符号固定为0);如果不是,这个位没有特殊含义,被算作数值的一部分。
对数值16进行VLQ编码
- 第一步,将16改写成二进制形式10000。
- 第二步,在最右边补充符号位。因为16大于0,所以符号位为0,整个数变成100000。
- 第三步,从右边的最低位开始,将整个数每隔5位,进行分段,即变成1和00000两段。如果最高位所在的段不足5位,则前面补0,因此两段变成00001和00000。不够两段的话就只有一段。
- 第四步,将两段的顺序倒过来,即00000和00001。
- 第五步,在每一段的最前面添加一个"连续位",除了最后一段为0,其他都为1,即变成100000和000001。
- 第六步,将每一段转成Base 64编码。
使用方式
假设转换后文件为 sample.min.js
。
-
在请求
sample.min.js
的 Response Headers 中设置 X-SourceMap,来指定 Source Map 的路径。X-SourceMap: /path/to/sample.min.map
-
在
sample.min.js
的末尾添加如下注释-
map 文件
//# sourceMappingURL=sample.min.js.map
-
Data URLs
//# sourceMappingURL=data:application/json;base64,Asdi...
-
具体事例
有如下文件
- 源文件(script.js)
- 转换后文件(script-transpiled.js)
- source map 文件(script-transpiled.js.map)
// script.js
const sayHello = name => `Hello ${name}`;
sayHello('sourcemap');
通过如下命令将 script.js es6 转化成 script-transpiled.js es5,并且生成 source map。
npx babel script.js --out-file script-transpiled.js --source-maps --presets=@babel/preset-env
// script-transpiled.js
"use strict";
var sayHello = function sayHello(name) {
return "Hello ".concat(name);
};
sayHello('sourcemap');
//# sourceMappingURL=script-transpiled.js.map
// script-transpiled.js.map
{
"version": 3,
"sources": ["script.js"],
"names": [],
"mappings": ";;AAAA,IAAM,QAAQ,GAAG,SAAX,QAAW,CAAA,IAAI;AAAA,yBAAa,IAAb;AAAA,CAArB;;AACA,QAAQ,CAAC,WAAD,CAAR",
"file": "script-transpiled.js",
"sourcesContent": ["const sayHello = name => `Hello ${name}`;\nsayHello('sourcemap');"]
}
mappings 行分隔
// 相对位置信息
2) [0,0,0,0], [4,0,0,6], [8,0,0,8], [3,0,0,3], [9,0,0,-11], [8,0,0,11], [1,0,0,0], [4,0,0,4] // AAAA,IAAM,QAAQ,GAAG,SAAX,QAAW,CAAA,IAAI
3) [0,0,0,0], [25,0,0,13], [4,0,0,-13] // AAAA,yBAAa,IAAb
4) [0,0,0,0], [1,0,0,-21] // AAAA,CAArB
6) [0,0,1,0], [8,0,0,8], [1,0,0,1], [11,0,0,-1], [1,0,0,-8] // AACA,QAAQ,CAAC,WAAD,CAAR
// 通过相对位置计算出绝对位置信息
2) [0,0,0,0], [4,0,0,6], [12,0,0,14], [15,0,0,17], [24,0,0,6], [32,0,0,17], [33,0,0,17], [37,0,0,21]
3) [0,0,0,0], [25,0,0,13], [29,0,0,0]
4) [0,0,0,0], [1,0,0,-21]
6) [0,0,1,0], [8,0,1,8], [9,0,1,9], [20,0,1,8], [21,0,1,0]
script.js 和 script-transpiled.js 的位置对应如图所示: Snipaste_2020-06-18_17-32-07.png
什么是 Data URLs?
前缀为
data:
协议的URL,其允许内容创建者向文档中嵌入小文件。
Data URLs 由四个部分组成:前缀(data:
)、指示数据类型的MIME类型、如果非文本则为可选的 base64
标记、数据本身:
data:[<mediatype>][;base64],<data>
// 简单的 text/plain 类型数据
data:,Hello%2C%20World!
// 上一条示例的 base64 编码版本
data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D
// 一个会执行 JavaScript alert 的 HTML 文档。注意 script 标签必须封闭。
data:text/html,<script>alert('hi');</script>
在浏览器地址栏中输入传统的 URL 是通过 URL 指定的路径去服务器找到资源然后返回到浏览器;而 Data URLs 本身就携带着数据。
webpack 中的 source map
webpack 通过 devtool 控制是否生成,以及如何生成 source map。
devtool 关键词
devtool 就是以下各种关键词的组合。
The pattern is: [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map.
- eval:使用 eval 包裹模块代码。
- source-map: 生成一个单独的 source map 文件,即 .map 文件。(注意与 source map 这个统称概念区分)
- cheap:source map 没有列映射(column mapping),忽略 loader source map。
- module:将 loader source map 简化为每行一个映射(mapping),比如 jsx to js ,babel 的 source map。
- inline:source map 通过 Data URLs 的方式添加到 bundle 中。
- hidden:不会为 bundle 添加引用注释。
- nosources:source map 不包含
sourcesContent(源代码内容)
。
下面我们看看几种组合的 source map。
source-map
source map 以 .map 文件的方式生成,通过 //# sourceMappingURL
在 bundle 末尾添加。
// source-map-bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["bundle"],{
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils.js */ "./utils.js");
const sum = Object(_utils_js__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2)
/***/ }),
/***/ "./utils.js":
/*!******************!*\
!*** ./utils.js ***!
\******************/
/*! exports provided: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
function add (x, y) {
return x + y
}
/***/ })
},[["./index.js","runtime~bundle"]]]);
//# sourceMappingURL=source-map-bundle.js.map
// source-map-bundle.js.map
{"version":3,"sources":["webpack:///./index.js","webpack:///./utils.js"],"names":[],"mappings":";;;;;;;;;;AAAA;AAAA;AAAgC;;AAEhC,YAAY,qDAAG,M;;;;;;;;;;;;ACFf;AAAA;AAAO;AACP;AACA,C","file":"source-map-bundle.js","sourcesContent":["import { add } from './utils.js'\n\nconst sum = add(1, 2)","export function add (x, y) {\n return x + y\n}"],"sourceRoot":""}
inline-source-map
source map 以 Data URLs 的方式生成并通过 //# sourceMappingURL
在 bundle 末尾添加,不单独生成 .map 文件。
// inline-source-map-bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["bundle"],{
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils.js */ "./utils.js");
const sum = Object(_utils_js__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2)
/***/ }),
/***/ "./utils.js":
/*!******************!*\
!*** ./utils.js ***!
\******************/
/*! exports provided: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
function add (x, y) {
return x + y
}
/***/ })
},[["./index.js","runtime~bundle"]]]);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9pbmRleC5qcyIsIndlYnBhY2s6Ly8vLi91dGlscy5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7O0FBQUE7QUFBQTtBQUFnQzs7QUFFaEMsWUFBWSxxREFBRyxNOzs7Ozs7Ozs7Ozs7QUNGZjtBQUFBO0FBQU87QUFDUDtBQUNBLEMiLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAtYnVuZGxlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgYWRkIH0gZnJvbSAnLi91dGlscy5qcydcblxuY29uc3Qgc3VtID0gYWRkKDEsIDIpIiwiZXhwb3J0IGZ1bmN0aW9uIGFkZCAoeCwgeSkge1xuICByZXR1cm4geCArIHlcbn0iXSwic291cmNlUm9vdCI6IiJ9
cheap-source-map
source map 以 .map 文件的方式生成,通过 //# sourceMappingURL
在 bundle 末尾添加。
-
没有列映射
我们可以看看 .map 文件的 mappings 字段:只有分号,没有逗号,说明只有行信息,没有列信息。
"mappings":";;;;;;;;;;AAAA;AAAA;AAAA;AACA;AACA;;;;;;;;;;;;ACFA;AAAA;AAAA;AACA;AACA;;;;A"
通过 SOURCEMAP V3 MAPPINGS PARSER 解码 mappings 得到位置信息,可以看到只有源文件和转换后文件的行信息,列全是 0 。
// 转换前 => 转换后,#0 #1 分别是 sources[0]、sources[1] ([0,0](#0)=>[10,0]) // 源文件,即 sources[0] 的第 0 行 => 转换后文件的第 10 行 ([0,0](#0)=>[11,0]) ([0,0](#0)=>[12,0]) ([1,0](#0)=>[13,0]) ([2,0](#0)=>[14,0]) ([0,0](#1)=>[26,0]) // 源文件,即 sources[1] 的第 0 行 => 转换后文件的第 26 行 ([0,0](#1)=>[27,0]) ([0,0](#1)=>[28,0]) ([1,0](#1)=>[29,0]) ([2,0](#1)=>[30,0])
-
忽略 loader source map:如果你使用了诸如 babel 等代码编译工具时, 定位到的原始代码将是经过 babel 编译后的代码位置,而非原始代码。
// cheap-source-map.js.map
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["bundle"],{
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils.js */ "./utils.js");
const sum = Object(_utils_js__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2)
/***/ }),
/***/ "./utils.js":
/*!******************!*\
!*** ./utils.js ***!
\******************/
/*! exports provided: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
function add (x, y) {
return x + y
}
/***/ })
},[["./index.js","runtime~bundle"]]]);
//# sourceMappingURL=cheap-source-map-bundle.js.map
// cheap-source-map-bundle.js.map
{"version":3,"file":"cheap-source-map-bundle.js","sources":["webpack:///./index.js","webpack:///./utils.js"],"sourcesContent":["import { add } from './utils.js'\n\nconst sum = add(1, 2)","export function add (x, y) {\n return x + y\n}"],"mappings":";;;;;;;;;;AAAA;AAAA;AAAA;AACA;AACA;;;;;;;;;;;;ACFA;AAAA;AAAA;AACA;AACA;;;;A","sourceRoot":""}
eval
每个模块使用 eval()
包裹,通过 //# sourceURL
在模块末尾添加模块来源,它不单独产生 .map 文件。它和其他模式不一样的地方是它依靠 sourceURL 来定位原始代码, 而其他所有选项都使用 source map 方式来定位。
// eval-bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["bundle"],{
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils.js */ \"./utils.js\");\n\n\nconst sum = Object(_utils_js__WEBPACK_IMPORTED_MODULE_0__[\"add\"])(1, 2)\n\n//# sourceURL=webpack:///./index.js?");
/***/ }),
/***/ "./utils.js":
/*!******************!*\
!*** ./utils.js ***!
\******************/
/*! exports provided: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"add\", function() { return add; });\nfunction add (x, y) {\n return x + y\n}\n\n//# sourceURL=webpack:///./utils.js?");
/***/ })
},[["./index.js","runtime~bundle"]]]);
eval-source-map
每个模块使用 eval()
包裹,source map 以 Data URLs 的方式生成并通过 //# sourceMappingURL
在模块末尾添加(注意是模块末尾,不是 bundle 末尾),不单独生成 .map 文件。
// eval-source-map-bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["bundle"],{
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils.js */ \"./utils.js\");\n\n\nconst sum = Object(_utils_js__WEBPACK_IMPORTED_MODULE_0__[\"add\"])(1, 2)//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9pbmRleC5qcz80MWY1Il0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBZ0M7O0FBRWhDLFlBQVkscURBQUciLCJmaWxlIjoiLi9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGFkZCB9IGZyb20gJy4vdXRpbHMuanMnXG5cbmNvbnN0IHN1bSA9IGFkZCgxLCAyKSJdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./index.js\n");
/***/ }),
/***/ "./utils.js":
/*!******************!*\
!*** ./utils.js ***!
\******************/
/*! exports provided: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"add\", function() { return add; });\nfunction add (x, y) {\n return x + y\n}//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi91dGlscy5qcz8xYjIzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBTztBQUNQO0FBQ0EiLCJmaWxlIjoiLi91dGlscy5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBhZGQgKHgsIHkpIHtcbiAgcmV0dXJuIHggKyB5XG59Il0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./utils.js\n");
/***/ })
},[["./index.js","runtime~bundle"]]]);