Source Maps

Chrome 开发工具 - Source Maps

source-maps,Chrome-dev-tool

当您的源代码经历了转换时,调试就成了问题。在浏览器中调试时,如何判断原始代码在哪里?Source Maps(源映射) 通过提供原始代码和转换后代码之间的映射 来解决这个问题。除了编译过后的 JavaScript 之外,这也适用于样式。

一种方法是在开发期间跳过源映射,但要受到浏览器兼容性的限制。如果您使用没有任何扩展的 ES2015 并使用现代浏览器进行开发,这样做没有什么问题。并且可以避免与源映射相关的所有问题,同时获得更好的性能。

如果您使用 Webpack 4 和 mode 选项,该工具将在 development 模式下自动为您生成源地图。但是,生产环境使用时需要注意。

如果您想更详细地了解 Source Maps 背后的想法,请阅读 Ryan Seddon 对该主题的介绍

要查看 Webpack 如何处理源映射,请参阅该工具的作者的 source-map-visualization

内联源映射和单独的源映射

Webpack 可以生成内联或单独的源映射文件。由于更好的性能,内联源映射在开发过程中很有价值,而单独的源映射在生产环境中使用非常方便,因为它可以保持小的 bundle 尺寸。在这种情况下,加载源映射是可选的。

您可能希望为生产环境生成源映射,因为这样可以轻松地检查您的应用程序。通过禁用源映射,代码实际上进行了某种混淆。无论您是否要为生产启用源映射,它们都可以方便地进行分段。跳过源映射会加快构建速度,因为生成最佳质量的源映射可能是一项复杂的操作。

隐藏的源映射仅提供堆栈跟踪信息。您可以将它们与监视服务连接,以便在应用程序崩溃时获取堆栈信息,从而方便您修复相关问题。虽然这并不理想,但最好还是了解可能出现的问题。

研究您正在使用的 loader 的文档以查看 loader 特定提示是个好主意。例如,使用 TypeScript,您必须设置一个特定的标志,以使其按预期工作。

启用源映射

Webpack 提供了两种启用源映射的方法。有一个 devtool 快捷方式字段。您还可以找到两个插件,提供更多调整选项。插件将在本章末尾简要讨论。除了 Webpack 之外,您还必须在用于开发的浏览器上启用对源映射的支持。

在 Webpack 中启用源映射

首先,我们可以直接在 Webpack 中配置。如果需要,您可以将其转换为使用插件的形式:

webpack.parts.js

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

Webpack 支持各种源映射类型。这些因质量和构建速度而异。目前,您将在生产环境中启用了 source-map,并在开发环境中默认使用。我们将部分配置合并到主配置上:

webpack.config.js

const productionConfig = merge([

  parts.generateSourceMaps({ type: "source-map" }),

  ...
]);

source-map 在构建上时最慢的,但质量也是最好的;对于生产环境来说,这是比较合适的。

如果你现在构建项目(npm run build),你应该在输出中看到源映射:

Hash: b59445cb2b9ae4cea11b
Version: webpack 4.1.1
Time: 1347ms
Built at: 3/16/2018 4:58:14 PM
       Asset       Size  Chunks             Chunk Names
     main.js  838 bytes       0  [emitted]  main
    main.css   3.49 KiB       0  [emitted]  main
 main.js.map   3.75 KiB       0  [emitted]  main
main.css.map   85 bytes       0  [emitted]  main
  index.html  220 bytes          [emitted]
Entrypoint main = main.js main.css main.js.map main.css.map
...

仔细看看那些 .map 文件,其中存放了开发代码和生产代码之间的映射关系。在开发期间,映射信息会直接内联到包中。

在浏览器中启用源映射

要在浏览器中使用源映射,必须根据特定浏览器的说明显式启用源映射:

Webpack 支持的源映射类型

Webpack 支持的源映射类型可以分为两类:

  • 内联源映射将映射数据直接添加到生成的文件中。
  • 单独的源映射将映射数据发送到单独的源映射文件,并使用注释将源链接到它们。隐藏的源映射会故意省略注释。

由于速度快,内联源映射是开发时的理想选择。鉴于它们使 bundle 变得很大,单独的源映射是生产环境的首选解决方案。如果性能开销可以接受,开发期间也可以使用单独的源映射。

内联源映射类型

Webpack 提供了多个内联源映射变体。通常都是基于 eval来做的,WebPack issue#2145 中建议使用 cheap-module-eval-source-map,因为它在速度和质量之间取得了比较好的平衡,在 Chrome 和 Firefox 浏览器中工作的也比较稳定。

为了更好地了解可用选项,下面对其一一介绍,同时为每个选项提供了一个小例子。源代码只包含一个 console.log('Hello world'),我们还使用了 webpack.NamedModulesPlugin,以使输出更容易理解。实际上,您会看到许多用于处理映射的代码。

devtool: “eval”

使用 eval 生成代码,其中每个模块都包含在一个 eval 函数中:

webpackJsonp([1, 2], {
  "./src/index.js": function(module, exports) {
    eval("console.log('Hello world');\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = ./src/index.js\n// module chunks = 1\n\n//# sourceURL=webpack:///./src/index.js?")
  }
}, ["./src/index.js"]);

devtool: “cheap-eval-source-map”

cheap-eval-source-map 在之前的基础上更进一步,它将源映射相关的内容编码为 base64 字符串。其结果中仅包含行数据,同时丢失列信息。

webpackJsonp([1, 2], {
  "./src/index.js": function(module, exports) {
    eval("console.log('Hello world');//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9hcHAvaW5kZXguanMuanMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9hcHAvaW5kZXguanM/MGUwNCJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zb2xlLmxvZygnSGVsbG8gd29ybGQnKTtcblxuXG4vLy8vLy8vLy8vLy8vLy8vLy9cbi8vIFdFQlBBQ0sgRk9PVEVSXG4vLyAuL2FwcC9pbmRleC5qc1xuLy8gbW9kdWxlIGlkID0gLi9hcHAvaW5kZXguanNcbi8vIG1vZHVsZSBjaHVua3MgPSAxIl0sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZVJvb3QiOiIifQ==")
  }
}, ["./src/index.js"]);

如果解码该 base64 字符串,则会获得包含源映射的信息:

{
  "file": "./src/index.js",
  "mappings": "AAAA",
  "sourceRoot": "",
  "sources": [
    "webpack:///./src/index.js?0e04"
  ],
  "sourcesContent": [
    "console.log('Hello world');\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = ./src/index.js\n// module chunks = 1"
  ],
  "version": 3
}

devtool: “cheap-module-eval-source-map”

cheap-module-eval-source-map 和上面的选项一样,只是有更高的质量和更低的性能:

webpackJsonp([1, 2], {
  "./src/index.js": function(module, exports) {
    eval("console.log('Hello world');//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9hcHAvaW5kZXguanMuanMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vYXBwL2luZGV4LmpzPzIwMTgiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc29sZS5sb2coJ0hlbGxvIHdvcmxkJyk7XG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIGFwcC9pbmRleC5qcyJdLCJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VSb290IjoiIn0=")
  }
}, ["./src/index.js"]);

再次,解码数据会得到以下信息:

{
  "file": "./src/index.js",
  "mappings": "AAAA",
  "sourceRoot": "",
  "sources": [
    "webpack:///src/index.js?2018"
  ],
  "sourcesContent": [
    "console.log('Hello world');\n\n\n// WEBPACK FOOTER //\n// src/index.js"
  ],
  "version": 3
}

在这种特殊情况下,选项之间的差异很小。

devtool: “eval-source-map”

eval-source-map 选项生成最高质量的内联源映射。它也是最慢的,因为它生成的数据最多:

webpackJsonp([1, 2], {
  "./src/index.js": function(module, exports) {
    eval("console.log('Hello world');//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9hcHAvaW5kZXguanM/ZGFkYyJdLCJuYW1lcyI6WyJjb25zb2xlIiwibG9nIl0sIm1hcHBpbmdzIjoiQUFBQUEsUUFBUUMsR0FBUixDQUFZLGFBQVoiLCJmaWxlIjoiLi9hcHAvaW5kZXguanMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJjb25zb2xlLmxvZygnSGVsbG8gd29ybGQnKTtcblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gLi9hcHAvaW5kZXguanMiXSwic291cmNlUm9vdCI6IiJ9")
  }
}, ["./src/index.js"]);

这次可以为浏览器提供更多的映射数据:

{
  "file": "./src/index.js",
  "mappings": "AAAAA,QAAQC,GAAR,CAAY,aAAZ",
  "names": [
    "console",
    "log"
  ],
  "sourceRoot": "",
  "sources": [
    "webpack:///./src/index.js?dadc"
  ],
  "sourcesContent": [
    "console.log('Hello world');\n\n\n// WEBPACK FOOTER //\n// ./src/index.js"
  ],
  "version": 3
}

单独的源映射

Webpack 还可以生成适用于生产环境的源映射。这些文件都是以 .map 扩展名结尾的单独文件,仅在需要时由浏览器加载。这样您的用户就可以获得良好的性能,同时您可以更轻松地调试应用程序。

source-map 是一个合理的默认值。尽管以这种方式生成源映射需要更长的时间,但您也可以获得最佳质量。如果您不需要在生产环境获得源映射,则可以跳过该设置并获得更好的性能。

devtool: “cheap-source-map”

cheap-source-map 类似于上面的简化选项。结果中不包含列映射信息。此外,不会使用来自 loader 的源映射,例如 css-loader

检查 .map 文件会显示以下输出:

{
  "file": "main.9aff3b1eced1f089ef18.js",
  "mappings": "AAAA",
  "sourceRoot": "",
  "sources": [
    "webpack:///main.9aff3b1eced1f089ef18.js"
  ],
  "sourcesContent": [
    "webpackJsonp([1,2],{\"./src/index.js\":function(o,n){console.log(\"Hello world\")}},[\"./src/index.js\"]);\n\n\n// WEBPACK FOOTER //\n// main.9aff3b1eced1f089ef18.js"
  ],
  "version": 3
}

源代码的末尾会包含 //# sourceMappingURL=main.9a...18.js.map 这样的注释来映射 .map 文件。

devtool: “cheap-module-source-map”

cheap-module-source-map 与前面的相同,只是来自 loader 的源映射被简化为一个行映射。在这种情况下,它会产生以下输出:

{
  "file": "main.9aff3b1eced1f089ef18.js",
  "mappings": "AAAA",
  "sourceRoot": "",
  "sources": [
    "webpack:///main.9aff3b1eced1f089ef18.js"
  ],
  "version": 3
}

cheap-module-source-map 生成的源映射在压缩后会被损坏,尽量避免使用这一选项。

devtool: “hidden-source-map”

hidden-source-mapsource-map 是相同的,只是它不会将源映射的引用写入源文件。如果您只是希望只是使用堆栈跟踪信息,而不愿意直接将源映射公开给开发工具,这很方便。

devtool: “nosources-source-map”

nosources-source-map 创建一个没有 sourcesContent 的源映射。但是,您仍然可以获得堆栈跟踪信息。如果您不希望将源代码公开给客户端,则该选项很有用。

官方文档包含有关 devtool 选项的更多信息。

devtool: “source-map”

source-map 提供最好的质量和完整的结果,但它也是最慢的选择。输出结果反映了这一点:

{
  "file": "main.9aff3b1eced1f089ef18.js",
  "mappings": "AAAAA,cAAc,EAAE,IAEVC,iBACA,SAAUC,EAAQC,GCHxBC,QAAQC,IAAI,kBDST",
  "names": [
    "webpackJsonp",
    "./src/index.js",
    "module",
    "exports",
    "console",
    "log"
  ],
  "sourceRoot": "",
  "sources": [
    "webpack:///main.9aff3b1eced1f089ef18.js",
    "webpack:///./src/index.js"
  ],
  "sourcesContent": [
    "webpackJsonp([1,2],{\n\n/***/ \"./src/index.js\":\n/***/ (function(module, exports) {\n\nconsole.log('Hello world');\n\n/***/ })\n\n},[\"./src/index.js\"]);\n\n\n// WEBPACK FOOTER //\n// main.9aff3b1eced1f089ef18.js",
    "console.log('Hello world');\n\n\n// WEBPACK FOOTER //\n// ./src/index.js"
  ],
  "version": 3
}

其他源映射选项

还有一些其他的选项会影响源映射的生成:

{
  output: {
    // 修改源映射的名字
    // 你可以使用 [file], [id], and [hash] 作为替代
    // 默认的选项已经足够胜任大多数情况了
    sourceMapFilename: '[file].map', // 默认

    // 这是一个源映射文件的默认模板
    // 其格式依赖于开发工具的设置,不需要频繁改动
    devtoolModuleFilenameTemplate:
      'webpack:///[resource-path]?[loaders]'
  },
}

官方文档有更多关于 output 的细节。

如果您使用的是 terser-webpack-plugin 并且仍然需要源映射,则需要为该插件设置 sourceMap: true。否则,结果不是您所期望的,因为 terser 将代码的进一步转换,从而破坏映射。对代码有更改的其他插件和 loader 也必须这样做。css-loader 和相关 loader 就是一个很好的例子。

SourceMapDevToolPlugin 和 EvalSourceMapDevToolPlugin

如果您想要更多地控制源映射的生成,可以使用 SourceMapDevToolPluginEvalSourceMapDevToolPlugin 代替 Webpack 默认方案。后者是一种更有限的替代方案,正如其名称所述,它可以方便地生成基于 eval 的源映射。

这两个插件都可以更精细地控制您要为特定部分的代码生成源映射,SourceMapDevToolPlugin 还可以严格控制源映射结果。使用任一插件都可以完全跳过 devtool 选项。

鉴于 WebPack 默认情况下只为.js.css 文件生成源映射,你可以使用SourceMapDevToolPlugin 来解决这个问题。它可以通过传递 test: /\.(js|jsx|css)($|\?)/i 来匹配更多格式的文件。

EvalSourceMapDevToolPlugin 仅接受 modulelineToLine 选项。因此,它可以被认为是一个 devtool: "eval" 的别名,只是它拥有更多的灵活性。

更改源映射前缀

您可以使用 pragma 字符为源映射选项添加前缀,该 pragma 字符将注入到源映射引用中。Webpack 默认使用 #,它被现代浏览器支持,因此您无需进行设置。

要覆盖它,您必须为源地图选项添加前缀(例如,@source-map)。在更改之后,您会在 JavaScript 文件中看到通过 //@ 而不是 //# 对源映射文件进行引用。(假设使用了单独的源映射类型)

使用依赖源映射

假设您正在使用内联源映射的分发包,您可以使用 source-map-loader 使 Webpack 发现这些源映射。如果不对包进行设置,您将获得最小化的调试输出。通常,您可以跳过此步骤,因为这是一个特殊情况。

样式的源映射

如果要为样式文件启用源映射,可以通过启用 sourceMap 选项来实现此目的。同样的想法适用于样式 loader,如 css-loadersass-loaderless-loader

当您在导入时使用相对地址,css-loader 有一个已经发现的问题,要解决此问题,您需要将 output.publicPath 设置为服务器的地址。

总结

源映射在开发过程中很方便。它们提供了更好的调试应用程序的方法,因为您仍然可以检查生成的原始代码。即使是生产用途,它们也很有价值,并允许您在提供客户友好版本的应用程序时调试问题。

回顾一下:

  • 源映射在开发和生产过程中都很有用。它们提供有关正在发生的事情的更准确信息,并使调试可能出现的问题变得更快。
  • Webpack 支持各种源映射变体。它们可以根据生成位置拆分为内联和单独的源映射。由于速度的原因,内联源映射在开发过程中很方便。单独的源映射适用于生产,然后加载它们变为可选。
  • devtool: "source-map" 生成的源映射质量最高,在生产环境下很有价值。
  • cheap-module-eval-source-map 在开发环境下是一个不错的初选方案。
  • 如果您想在生产过程中只获得堆栈跟踪信息,请使用 devtool: "hidden-source-map"。您可以捕获输出并将其发送到第三方服务供您检查。这样您就可以捕获错误并修复它们。
  • 相较于 devtoolSourceMapDevToolPluginEvalSourceMapDevToolPlugin 对结果拥有更多的控制。
  • 如果依赖项提供源映射,source-map-loader 可以派上用场。
  • 为样式设置源映射需要额外的工作。您必须为正在使用的每个样式相关的 loader 启用 sourceMap 选项。

在下一章中,您将学习拆分打包,并将当前的包分为应用程序包和外部依赖包两种。

用户头像
登录后发表评论