源代码在被转换后变得不可读,这给调试带来了非常多的不便。通常我们可以通过 Source Map 来解决这个问题。我们不仅可以给 JavaScript 文件添加 Source Map,还可以给样式文件添加 Source Map。

Webpack 同时支持将 Source Map 内联在输出的 Bundle 中和将 Source Map 单独输出到文件中。在开发过程中,将 Source Map 内联在构建输出中可以加快构建性能。在生产环境中,Source Map 通常会被单独输出到文件中,减少请求文件大小。

使用 Source Map

webpack.parts.js

exports.generateSourceMaps = ({ type }) => ({ devtool: type });
1

Webpack 支持多种 Source Map 选项,每种选项都有着针对构建性能和输出质量的不同考量。假设现在我们在生产环境中使用 source-map 这个选项,开发环境中使用 Webpack 的默认值,则配置如下:

webpack.config.js

const productionConfig = merge([
  ...parts.generateSourceMaps({ type: 'source-map' }),
]);
1
2
3

source-map 是构建速度最慢,但是质量最高的选项,比较适合用在生产环境中。

这时候执行 npm run build, 可以在项目的 dist 目录中看到输出的 .map 文件。

内联 Source Map

所谓内联 Source Map 就是将 Source Map 与构建出来的结果一同输出。Webpack 支持多种内联 Source Map 设置,下面逐一介绍。每一个选项都会有一个样例。

每个样例除了 Source Map 的设置,还额外使用了 optimization.moduleIds = "named" 配置来给每一个模块添加名称,提高可阅读性。同时还将 mode 设置为 false 来避免 Webpack 的默认处理。

devtool: "eval"

eval 选项生成的代码会将每一个模块的源代码包裹在 eval 函数中。

/***/ "./src/index.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _main_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./src/main.css\");\n/* harmony import */ var _main_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_main_css__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _component__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(\"./src/component.js\");\n\n\ndocument.body.appendChild(Object(_component__WEBPACK_IMPORTED_MODULE_1__[\"default\"])());\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ }),
1
2
3
4
5
6
7

devtool: "eval-cheap-source-map"

eval-cheap-source-mapeval 类似,但是会将 Source Map 信息进行 Base64 编码,将就编码结果以 data url 的形式作为 Source Map 的地址保存在构建结果中。eval-cheap-source-map 输出的 Map 信息中只有行数据,没有代码列的 Map 数据。我们将 Source Map 信息解码后可以看到如下信息:

{
  "version": 3,
  "file": "./src/index.js.js",
  "sources": ["webpack:///./src/index.js?3700"],
  "sourcesContent": [
    "import './main.css';\nimport component from \"./component\";\ndocument.body.appendChild(component());"
  ],
  "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA",
  "sourceRoot": ""
}
1
2
3
4
5
6
7
8
9
10

devtool: eval-cheap-module-source-map

eval-cheap-module-source-map 的处理方式相同,只是构建出来的 Source Map 质量更高,但是构建速度更慢。解码信息如下:

{
  "version": 3,
  "file": "./src/index.js.js",
  "sources": ["webpack:///./src/index.js?b635"],
  "sourcesContent": ["import './main.css';\nimport component ..."],
  "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AAEA",
  "sourceRoot": ""
}
1
2
3
4
5
6
7
8

devtool: "eval-source-map"

eval-source-map 得到的 Source Map 质量最高,同时构建速度也最慢。解码后得到的信息也最多。

{
  "version": 3,
  "sources": ["webpack:///./src/index.js?b635"],
  "names": ["document", "body", "appendChild", "component"],
  "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AAEAA,QAAQ,CAACC,IAAT,CAAcC,WAAd,CAA0BC,0DAAS,EAAnC",
  "file": "./src/index.js.js",
  "sourcesContent": ["import './main.css';\nimport component ..."],
  "sourceRoot": ""
}
1
2
3
4
5
6
7
8
9

输出 Source Map 文件

一般来说,在生产环境中,我们会将 Source Map 输出到单独的 .map 文件中。下面几个选项会将 Source Map 信息单独输出到文件中。

devtool: "cheap-source-map"

与上面的内联的 cheap 选项相同,cheap-source-map 生成的 Map 文件中只有代码行的 Map 数据,没有代码列的 Map 数据。同时,来自于 loader 的 Source Map 也不会包含在输出结果中。

检查输出的 .map 文件可以看到如下结果。

{
  "version": 3,
  "file": "main.js",
  "sources": [
    "webpack:///webpack/bootstrap",
    "webpack:///./src/component.js",
    "webpack:///./src/index.js",
    "webpack:///./src/main.css"
  ],
  "sourcesContent": ["...", "// extracted by mini-css-extract-plugin"],
  "mappings": ";AAAA;...;;ACFA;;;;A",
  "sourceRoot": ""
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在构建输出的代码文件中,可以看到类似于 //# sourceMappingURL=main.js.map 这样的注释,来告知浏览器使用的 Source Map 的地址。

devtool: "cheap-module-source-map"

cheap-module-source-map 于上一个选项基本相同,除了会将来自于 loader 的 Source Map 也包含在输出的 Map 文件中。注意该选项只包含代码行的 Map 信息。

查看输出信息如下:

{
  "version": 3,
  "file": "main.js",
  "sources": [
    "webpack:///webpack/bootstrap",
    "webpack:///./src/component.js",
    "webpack:///./src/index.js",
    "webpack:///./src/main.css"
  ],
  "sourcesContent": ["...", "// extracted by mini-css-extract-plugin"],
  "mappings": ";AAAA;...;;ACFA;;;;A",
  "sourceRoot": ""
}
1
2
3
4
5
6
7
8
9
10
11
12
13

devtool: "source-map"

source-map 的输出质量最高,但是性能也最差。输出结果如下:

{
  "version": 3,
  "sources": [
    "webpack:///webpack/bootstrap",
    "webpack:///./src/component.js",
    "webpack:///./src/index.js",
    "webpack:///./src/main.css"
  ],
  "names": [
    "text",
    "element",
    "document",
    "createElement",
    "className",
    "innerHTML",
    "body",
    "appendChild",
    "component"
  ],
  "mappings": ";AAAA;...;;ACFA;;;;A",
  "file": "main.js",
  "sourcesContent": ["...", "// extracted by mini-css-extract-plugin"],
  "sourceRoot": ""
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

devtool: "hidden-source-map"

hidden-source-mapsource-map 相同,但是 hidden-source-map 不会在代码中输出 .map 的引用地址。因此浏览器调试工具无法自动加载 Source Map 文件。

devtool: "nosources-source-map"

nosources-source-map 输出的 Source Map 文件中不包含 sourcesContent 这个字段,也就是说在客户端看不到源代码。但是可以得到报错的基于源代码的堆栈信息。

其他的 Source Map 选项

这里还有一些可以控制 Source Map 输出的选项。

const config = {
  output: {
    // 指定生成的 Source Map 文件名称
    // 可以使用 [file], [id], [fullhash], and [chunkhash] 占位符.
    sourceMapFilename: '[file].map', // 默认配置

    // 默认的 Source Map 引用模板。
    devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]',

    // create-react-app 会使用下面的配置,这样让 Source Map 可以在开发工具中更好的展示
    devtoolModuleFilenameTemplate: (info) =>
      path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

从 Source Map 中生成源代码

我们可以借助于 source-from-sourcemapsopen in new window这个工具,通过 Source Map 和压缩混淆过的代码反向生成源代码。

使用项目依赖包中的 Source Map

source-map-loaderopen in new window 可以帮助我们将项目依赖包中的内联 Source Map 提取出来,从而避免将依赖包的 Source Map 输出到项目代码中。

总结

Source Map 通过建立转换后的代码和源代码的映射关系,给开发调试带来的极大的便利。在开发和生产环境中都可以使用。

Webpack 支持多种 Source Map 的生成方式,通常分为内联 Source Map 和单独输出到文件 Source Map 两种。前者常用在开发环境中,后者常用在生产环境中。

devtool: "source-map" 生成的 Source Map 质量最高,常被用在生产环境中。

通过使用 devtool: "hidden-source-map",我们可以在生产环境中不暴露 Source Map 文件。将错误堆栈上报到其他服务,然后结合 Source Map 查找问题。

在处理第三方依赖包的 Source Map 的时候,我们需要使用 source-map-loader.

在处理样式相关的 Source Map 的时候,我们需要开启对应 loader 的 sourceMap 参数。

关注微信公众号,获取最新推送~