Webpack中Source Map配置深入解析

发布时间:

通常我们运行在浏览器中的代码是经过处理的,处理后的代码可能与开发时代码相差很远,这就导致开发调试和线上排错变得困难。这时Source Map就登场了,有了它浏览器就可以从转换后的代码直接定位到转换前的代码。在webpack中,可以通过devtool选项来配置Source Map。

devtool选项

当mode为development时,devtool默认为‘eval’,当mode为production时,devtool默认为false。

devtool为false和'eval'有啥区别

在回答这个问题之前,我们得做点准备工作。

准备工作

1,创建项目 安装依赖

// 1,创建项目 安装依赖
mkdir sourcemap-demo
cd sourcemap-demo
npm init -y
npm install webpack webpack-cli --save-dev
npm install html-webpack-plugin --save-dev

2,添加文件

// 添加index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>起步</title>
</head>
<body>
</body>
</html>
// 添加src/index.js
import a from './a';
console.log(`hello index`);
a();
// 添加src/a.js
export default function b() {
  console.log('hello a');
}

3,写配置 webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
  entry: './src/index.js',
  mode: 'development', // 注意:这地方要将mode设置为development。因为当mode为production时webpack会开启代码压缩,不利于我们观察现象。
  output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
  },
  devtool: false, // 先配置为false
  plugins: [new HtmlWebpackPlugin()],
};

4,在package.json中添加

"scripts": {
"build": "webpack"
  }

5,执行 npm run build,打包文件生成到了dist文件夹中,至此,准备工作完毕。

观察devtool为false时

我们要观察两个地方,一个是dist/main.js。一个在浏览器中的情况。

1, 在dist/main.js中

var __webpack_modules__ = ({
 "./src/a.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
  "default": () => (b)
});
function b() {
  console.log('hello a');
}
  })
 ...
 var __webpack_exports__ = {};
  (() => {
__webpack_require__.r(__webpack_exports__);
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
console.log(`hello index`);
(0, _a__WEBPACK_IMPORTED_MODULE_0__["default"])();
  })();

内容分析:

  • __webpack_modules__对象对应着我们源码中的js文件, key是路径,value就是经过webpack转换过的模块。
  • 我们写的 console.log(hello index); 这类源码中的内容,基本没什么变化的被打包到dist/main.js中。

2,在浏览器中,观察开发者工具中的Sources。

Webpack中Source Map配置深入解析

内容分析:

  • 只能看到webpack打包后的代码.

小结

devtool为false,只能看到打包后的代码,不利于开发调试和线上排错。

观察devtool为'eval'时

重新编译后。

1, 在dist/main.js中

  var __webpack_modules__ = ({
"./src/a.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (/* binding */ b)\n/* harmony export */ });\nfunction b() {\n  console.log('hello a');\n}\n\n//# sourceURL=webpack://sourcemap/./src/a.js?");
  }),
"./src/index.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./src/a.js\");\n\n\nconsole.log(`hello index`);\n(0,_a__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n\n//# sourceURL=webpack://sourcemap/./src/index.js?");
  })
  });

内容分析:

  • 模块中的内容被eval包裹。内容末尾有// # sourceURL=webpack://sourcemap/./src/index.js。浏览器会对eval后面的sourceURL特殊处理。下面看看怎么特殊处理的

2,在浏览器中

Webpack中Source Map配置深入解析

内容分析:

  • 所有sourceURL的内容会被翻译成上图中左侧下方sourcemap-demo节点的结构,点击某个节点,会看到其eval对应的代码。从中我们看到了console.log(hello index);。

小结

devtool为'eval',可以在浏览器开发者工具中看到模块结构,通常也就是我们源码的结构。也能看到被webpack转换过点代码,在一定程度上能帮助我们调试代码和排查问题了。

devtool为'source-map'和'eval-source-map'有啥区别

观察devtool为'source-map'时

现在将devtool设置成source-map,看看效果。

1, 在dist/main.js中

//# sourceMappingURL=main.js.map

内容分析:

  • 最下面多了一行注释代码sourceMappingURL指向main.js.map。同时dist文件夹下多了个main.js.map。
main.js.map文件

源码和打包后代码的映射文件,有了它,当线上出bug时,浏览器可以在控制台显示报错在源码中的行数和列数。

2,在浏览器中

Webpack中Source Map配置深入解析

内容分析:

  • 在浏览器中可以看到我们的源码了。

小结

  • devtool为'source-map',会生成映射文件,出bug时可以准确定位到问题在源码中的位置,便于开发调试和排查问题。
  • 它同样有缺点,一是source-map的生成比较耗时,二是在开发时,我们会用热更新,没有source-map的话只需替换被更改的模块就行了,但是有source-map时,就要重新生产整个打包文件的map文件,比较耗时。

那么有什么方式可以避免devtool为'source-map'的缺点吗,答案是'eval-source-map'

观察devtool为'eval-source-map'时

1, 在dist/main.js中

var __webpack_modules__ = ({
"./src/a.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (/* binding */ b)\n/* harmony export */ });\nfunction b() {\n  console.log('hello a');\n}//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvYS5qcy5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQWU7QUFDZjtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vc291cmNlbWFwLWRlbW8vLi9zcmMvYS5qcz82ZTFjIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGIoKSB7XG4gIGNvbnNvbGUubG9nKCdoZWxsbyBhJyk7XG59Il0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///./src/a.js\n");
  }),
"./src/index.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./src/a.js\");\n\n\nconsole.log(`hello index`);\n(0,_a__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvaW5kZXguanMuanMiLCJtYXBwaW5ncyI6Ijs7QUFBb0I7O0FBRXBCO0FBQ0EsOENBQUMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9zb3VyY2VtYXAtZGVtby8uL3NyYy9pbmRleC5qcz9iNjM1Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBhIGZyb20gJy4vYSc7XG5cbmNvbnNvbGUubG9nKGBoZWxsbyBpbmRleGApO1xuYSgpOyJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/index.js\n");
  })
  });

内容分析:

  • 与devtool为'source-map'的区别是,不是在文件最底部多了一行//# sourceMappingURL=main.js.map,也没有生成map文件。而是每个模块直接跟随一个sourceMappingURL,其中是源码和打包后代码的映射信息。这样点好处时在webpack因为开发者的更改要进行热更新时,不必重新生成全局map文件,只需重新生成更改模块的map信息就好。

2,在浏览器中

Webpack中Source Map配置深入解析

内容分析:

  • 在浏览器中同样可以看到源码,相比于'source-map',热更新时编译时间更少。

小结

  • 这里总结下'eval'的好处,由于Mapping信息被拆分在了每个模块eval函数后面的sourceMappingURL中,热更新时不必全量生成map数据,从而拥有更快的编译速度。(官网管这叫rebuild速度快)

对于开发调试,eval-source-map模式就完美了吗,如果追求更快的编译速度而不求追求的出现bug时精准的行列信息,只提供bug所在行信息就行,应该用什么?答案是'eval-cheap-source-map'。

devtool为'eval-cheap-source-map'和'eval-cheap-module-source-map'有啥区别

eval-source-map可以在出现bug时准确的定位到出错代码在源码的行和列。但如果你不需要列映射,只需要知道bug所在的行,那可以考虑eval-cheap-source-map,它比eval-source-map开销小,编译的快。

观察devtool为'eval-cheap-source-map'时

1, 在dist/main.js中

这里不贴代码了,相比于eval-source-map,sourceMappingURL后的内容少很多。

2,在浏览器中

与eval-source-map相同,更多细节暂不研究。

3,eval-cheap-source-map的问题

我们增加js代码babal专义的环节,安装 babel

 npm install babel-loader @babel/core @babel/preset-env -D

添加代码

// src/b.js
export default class B {
  constructor() {
this.str = 'hell b'
  }
  sayHello() {
console.log(this.str)
  }
}
// index.js
import a from './a';
+ import B from './b';
console.log(`hello index`);
a();
+ const b = new B();
+ b.sayHello();

查看在浏览器中的表现

Webpack中Source Map配置深入解析

坏了,浏览器中展示点代码是经过loader编译后的代码,不是我们手写的代码了。

经实验,如果设置成eval-source-map,不会有这个问题,浏览器中仍然可以看到源码。

有没有什么设置,既可以没有列映射,又能在浏览器中看到源码的呢?答案是eval-cheap-module-source-map。

观察devtool为'eval-cheap-module-source-map'时

Webpack中Source Map配置深入解析

在这种情况下,源自 loader 的 source map 会得到更好的处理结果。既没有列映射,又能在浏览器中看到源码。

小结

在开发模式下,为方便我们的开发调试。

  • 如果你想要最全的源码,bug精准定位到哪行哪列,使用eval-source-map。
  • 如果只需要将错误定位到哪行,使用eval-cheap-module-source-map。

开发模式下其他配置

inline-source-map,inline-cheap-source-map等inline开头的

就是把map文件内容放打包js代码文件中。看起来没多大用处。

eval-nosources-cheap-source-map 等有nosources关键字的

源码信息不再map文件中,浏览器通常会尝试从 web 服务器或文件系统加载源代码。目前不了解其使用场景。

生产环境的source map配置

(none)

生产环境不配置等同于写了devtool: false。默认不生成 source map,这是个不错的选择。因为别人可以通过 source map看到我们的源码,影响安全。

source-map

在线上环境不必使用eval-*了,没有热更新的情况。也没必要使用cheap-*了,只要发布的时候编译一次,慢点就慢点。

显然使用source-map有个问题,会让用户看到源码。我们可以通过服务器配置,禁止普通用户访问 source map 文件。只让固定IP(开发者的电脑的IP)的用户可以访问到。

hidden-source-map

hidden-source-map - 与 source-map 相同,都会生成.map文件。但不会为 bundle 添加引用注释。

目前,各大监控平台均有针对错误监控的Source Map的解析功能。例如开源监控平台Sentry支持Webpack插件和自身的CLI工具上传map文件后进行解析。

使用hidden-source-map,当线上报错时,将错误堆栈跟踪信息上报给监控平台,监控平台结合map文件就可以告诉我们源码级别的错误信息了。

总结

通过这次配置研究,我们要学习到

  • eval配置的作用和原理。
  • source-map配置的作用和原理。
  • 开发环境和生产环境视情况选择devtool的值。