Chrome 开发工具 - Source Maps
当您的源代码经历了转换时,调试就成了问题。在浏览器中调试时,如何判断原始代码在哪里?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 文件,其中存放了开发代码和生产代码之间的映射关系。在开发期间,映射信息会直接内联到包中。
在浏览器中启用源映射
要在浏览器中使用源映射,必须根据特定浏览器的说明显式启用源映射:
- Chrome。有时,Chrome 检查器中不会更新源映射。目前,我们可以通过 alt-r 来重新加载源映射。
- 火狐
- IE Edge
- 苹果浏览器
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-map
与 source-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
如果您想要更多地控制源映射的生成,可以使用 SourceMapDevToolPlugin 或EvalSourceMapDevToolPlugin
代替 Webpack 默认方案。后者是一种更有限的替代方案,正如其名称所述,它可以方便地生成基于 eval
的源映射。
这两个插件都可以更精细地控制您要为特定部分的代码生成源映射,SourceMapDevToolPlugin
还可以严格控制源映射结果。使用任一插件都可以完全跳过 devtool
选项。
鉴于 WebPack 默认情况下只为.js
和.css
文件生成源映射,你可以使用SourceMapDevToolPlugin
来解决这个问题。它可以通过传递 test: /\.(js|jsx|css)($|\?)/i
来匹配更多格式的文件。
EvalSourceMapDevToolPlugin
仅接受 module
和 lineToLine
选项。因此,它可以被认为是一个 devtool: "eval"
的别名,只是它拥有更多的灵活性。
更改源映射前缀
您可以使用 pragma 字符为源映射选项添加前缀,该 pragma 字符将注入到源映射引用中。Webpack 默认使用 #
,它被现代浏览器支持,因此您无需进行设置。
要覆盖它,您必须为源地图选项添加前缀(例如,@source-map
)。在更改之后,您会在 JavaScript 文件中看到通过 //@
而不是 //#
对源映射文件进行引用。(假设使用了单独的源映射类型)
使用依赖源映射
假设您正在使用内联源映射的分发包,您可以使用 source-map-loader 使 Webpack 发现这些源映射。如果不对包进行设置,您将获得最小化的调试输出。通常,您可以跳过此步骤,因为这是一个特殊情况。
样式的源映射
如果要为样式文件启用源映射,可以通过启用 sourceMap
选项来实现此目的。同样的想法适用于样式 loader,如 css-loader,sass-loader 和 less-loader。
当您在导入时使用相对地址,css-loader 有一个已经发现的问题,要解决此问题,您需要将 output.publicPath
设置为服务器的地址。
总结
源映射在开发过程中很方便。它们提供了更好的调试应用程序的方法,因为您仍然可以检查生成的原始代码。即使是生产用途,它们也很有价值,并允许您在提供客户友好版本的应用程序时调试问题。
回顾一下:
- 源映射在开发和生产过程中都很有用。它们提供有关正在发生的事情的更准确信息,并使调试可能出现的问题变得更快。
- Webpack 支持各种源映射变体。它们可以根据生成位置拆分为内联和单独的源映射。由于速度的原因,内联源映射在开发过程中很方便。单独的源映射适用于生产,然后加载它们变为可选。
devtool: "source-map"
生成的源映射质量最高,在生产环境下很有价值。cheap-module-eval-source-map
在开发环境下是一个不错的初选方案。- 如果您想在生产过程中只获得堆栈跟踪信息,请使用
devtool: "hidden-source-map"
。您可以捕获输出并将其发送到第三方服务供您检查。这样您就可以捕获错误并修复它们。 - 相较于
devtool
,SourceMapDevToolPlugin
和EvalSourceMapDevToolPlugin
对结果拥有更多的控制。 - 如果依赖项提供源映射,source-map-loader 可以派上用场。
- 为样式设置源映射需要额外的工作。您必须为正在使用的每个样式相关的 loader 启用
sourceMap
选项。
在下一章中,您将学习拆分打包,并将当前的包分为应用程序包和外部依赖包两种。