手把手带你学webpack(6)-- source-map
本篇文章对应源码:github.com/Plasticine-…
在遇到报错的时候,明明运行在浏览器中的代码是经过webpack
打包,被压缩和丑化过的,但是浏览器的开发者工具中却仍然能够找到源码中对应的报错位置,这是如何做到的呢?本篇文章就来探讨一下webpack
中的source-map
1. 初始source-map
我们平时在开发中的代码和最终打包后跑在浏览器中的代码是有区别的,打包出于性能的考虑,会将开发时的源码进行压缩,将所有的空格换行等字符删除,并将变量全部进行丑化,替换成无意义的变量名
这样一来,如果在运行过程中遇到报错也无法知道是哪一行报错了,十分不利于我们调试代码,而source-map
就是用来解决这一问题的
首先要明确,source-map
不是webpack
开发的功能,而是浏览器支持的,它能够建立已转换的代码到源码之间的映射关系,使得浏览器能够根据它还原出源代码
要让source-map
生效,需要在打包后的代码的最后加上一行
//# sourceMappingURL=bundle.js.map
即配置一个sourceMappingURL
属性,其值为source-map
文件所在位置,并且还需要浏览器的开发者工具中开启了Enable JavaScript source maps
后才能生效
2. souce-map的数据结构
在上一篇讲解webpack
模块化原理的文章中,我们在webpack.config.js
中配置了devtools: 'source-map'
,然后打包的结果中就能够看到一个main.js.map
,这个文件就是source-map
文件
source-map
的发展经过了三个版本,由于要和源码之间建立映射关系,那很自然的source-map
文件至少会和源码的大小一样
实际上,第一版的source-map
中,生成的文件大小是源码的十倍!十分离谱,后来第二版做了优化,大小减小了50%
,也就是源码大小的五倍,而目前使用的第三版中又减小了50%
,因此一般来说生成的source-map
文件会是源码的2.5
倍左右
souce-map
文件本质上就是一个json
文件,其数据结构如下
version
:source-map
文件的版本,目前最新是第三版file
:浏览器加载时的文件名mappings
:记录和源码之间位置信息的映射关系,比如哪个函数在源码的第几行第几列,用到了base64 VQL(variable-length-quantity)可变长度值
编码sources
:源码的文件路径以及webpack
运行时环境的一些代码路径sourcesContent
:源码中的代码内容sourceRoot
:sources
中的源码的根目录
3. webpack中使用source-map
3.1 eval
大家都知道webpack
有development
和production
两种模式
在development
模式下,默认使用的devtools
是eval
,也就是把我们的项目源码放到eval
函数中执行,并且在eval
函数的最后会添加上一行注释,用于告知浏览器源码映射信息
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'eval', // 默认就是 eval,因此 development 模式下不写 devtool 配置项也可以
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
};
eval("function errorFn() {\n console.log('hello error');\n\n throw new Error('something wrong...');\n}\n\nmodule.exports = { errorFn };\n\n\n//# sourceURL=webpack://06_webpack_source_map/./src/utils.js?");
上面这行代码就是打包后的一部分结果,浏览器在执行eval
函数的时候解析到最后的注释就知道该去哪里找对应的源码了
eval
这种方式只建议在development
模式下使用,因为它打包速度快,并且又能够提供一定的source-map
的能力,但它不是source-map
,因为并没有生成对应的soruce-map
文件,但是确实能够提供类似srouce-map
的功能,此外,eval
函数存在安全性问题,不建议在生产环境使用
3.2 source-map
devtool
配置为source-map
即可生成source-map
文件,能够更加细致地将打包结果映射到源码中
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'source-map',
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
};
这种方式适用于生产环境下需要在出现错误的时候知道源码位置时使用
3.3 eval-source-map
结合了eval
和source-map
的特点,单独使用eval
的时候,打包的结果中是在eval
函数中写入代码的字符串,并且在最后拼接sourceURL
而如果改成eval-source-map
,则拼接的是sourceMappingURL
,并且值是一串base64
编码,通过dataURL
的形式放在后面,也就是给每个eval
函数都生成了一个直接编码在js
内部的source-map
,而没有生成单独的source-map
文件
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'source-map',
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
};
打包生成的结果中的eval
函数
eval(
"function errorFn() {\n console.log('hello error');\n\n throw new Error('something wrong...');\n}\n\nmodule.exports = { errorFn };\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvdXRpbHMuanMuanMiLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBLG1CQUFtQiIsInNvdXJjZXMiOlsid2VicGFjazovLzA2X3dlYnBhY2tfc291cmNlX21hcC8uL3NyYy91dGlscy5qcz8wMjVlIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIGVycm9yRm4oKSB7XG4gIGNvbnNvbGUubG9nKCdoZWxsbyBlcnJvcicpO1xuXG4gIHRocm93IG5ldyBFcnJvcignc29tZXRoaW5nIHdyb25nLi4uJyk7XG59XG5cbm1vZHVsZS5leHBvcnRzID0geyBlcnJvckZuIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./src/utils.js\n"
);
这种方式适用于在开发模式下需要精确的
source-map
时使用,相比直接的eval
,会更加精确些
3.4 inline-source-map
顾名思义,就是以内联方式存放source-map
文件,它会将source-map
文件的内容编码成base64
后直接放在打包结果的最后
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
};
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7QUFBQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUEsbUJBQW1COzs7Ozs7O1VDTm5CO1VBQ0E7O1VBRUE7VUFDQTtVQUNBO1VBQ0E7VUFDQTtVQUNBO1VBQ0E7VUFDQTtVQUNBO1VBQ0E7VUFDQTtVQUNBO1VBQ0E7O1VBRUE7VUFDQTs7VUFFQTtVQUNBO1VBQ0E7Ozs7Ozs7OztBQ3RCQSxRQUFRLFVBQVUsRUFBRSxtQkFBTyxDQUFDLCtCQUFTOztBQUVyQyIsInNvdXJjZXMiOlsid2VicGFjazovLzA2X3dlYnBhY2tfc291cmNlX21hcC8uL3NyYy91dGlscy5qcyIsIndlYnBhY2s6Ly8wNl93ZWJwYWNrX3NvdXJjZV9tYXAvd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vMDZfd2VicGFja19zb3VyY2VfbWFwLy4vc3JjL2luZGV4LmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIGVycm9yRm4oKSB7XG4gIGNvbnNvbGUubG9nKCdoZWxsbyBlcnJvcicpO1xuXG4gIHRocm93IG5ldyBFcnJvcignc29tZXRoaW5nIHdyb25nLi4uJyk7XG59XG5cbm1vZHVsZS5leHBvcnRzID0geyBlcnJvckZuIH07XG4iLCIvLyBUaGUgbW9kdWxlIGNhY2hlXG52YXIgX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fID0ge307XG5cbi8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG5mdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuXHR2YXIgY2FjaGVkTW9kdWxlID0gX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fW21vZHVsZUlkXTtcblx0aWYgKGNhY2hlZE1vZHVsZSAhPT0gdW5kZWZpbmVkKSB7XG5cdFx0cmV0dXJuIGNhY2hlZE1vZHVsZS5leHBvcnRzO1xuXHR9XG5cdC8vIENyZWF0ZSBhIG5ldyBtb2R1bGUgKGFuZCBwdXQgaXQgaW50byB0aGUgY2FjaGUpXG5cdHZhciBtb2R1bGUgPSBfX3dlYnBhY2tfbW9kdWxlX2NhY2hlX19bbW9kdWxlSWRdID0ge1xuXHRcdC8vIG5vIG1vZHVsZS5pZCBuZWVkZWRcblx0XHQvLyBubyBtb2R1bGUubG9hZGVkIG5lZWRlZFxuXHRcdGV4cG9ydHM6IHt9XG5cdH07XG5cblx0Ly8gRXhlY3V0ZSB0aGUgbW9kdWxlIGZ1bmN0aW9uXG5cdF9fd2VicGFja19tb2R1bGVzX19bbW9kdWxlSWRdKG1vZHVsZSwgbW9kdWxlLmV4cG9ydHMsIF9fd2VicGFja19yZXF1aXJlX18pO1xuXG5cdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG5cdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbn1cblxuIiwiY29uc3QgeyBlcnJvckZuIH0gPSByZXF1aXJlKCcuL3V0aWxzJyk7XG5cbmVycm9yRm4oKTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==
从官方文档可以看到,这种方式的构建速度是最慢的,只适用于构建单个文件的时候使用
3.5 cheap-source-map
这种方式相比source-map
而言,没有建立列映射,也就是说遇到报错的时候,只会告诉你哪一行代码出错了,而不会告诉你哪一列出错了,如果开发时对列映射没有太高要求的话可以使用这种方式,毕竟不用生成列映射,比起source-map
来说会快一些
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'cheap-source-map',
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
};
3.6 cheap-module-source-map
官方文档对这种方式的devtool
并没有进行任何详细介绍,事实上这种方式适用于js
代码被loader
转换过的场景,比如被babel
进行了转换,又比如源码是用typescript
写的,后来经过loader
转成了js
代码,而我们又希望在运行的时候出现报错信息时能够对应回typescript
代码
像这种有loader
对js
进行转换的场景下,想要保证正确的source-map
就需要使用到带有module
的devtool
了,因为除了cheap-module-source-map
,还有很多别的方式也是有module
的,只要是在官方文档中看到带有module
的devtool
都是具有这种特性
下面就以babel
为例,我们通过babel-loader
对js
进行转换,然后看看能否正确对应到转换前的代码
首先安装如下依赖
pnpm i @babel/core @babel-preset-env babel-loader
@babel/core
是babel
的核心,所有功能都要在这个包的基础上运行@babel-preset-env
让我们可以不需要考虑转换成什么版本的js
,它会根据要适配的浏览器自动转换成能兼容相应浏览器的版本,这里我们使用它主要是能够将我们写的es6
代码转成es5
,从而让我们的源码和打包后的结果有差异,方便观察source-map
是否生效
babel-loader
,用于和webpack
搭配使用,转换js
文件
接下来配置loader
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map',
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
module: {
rules: [
{
test: /\.js/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
],
},
],
},
};
然后我们写一个具有es6
特性的语法的函数
function errorFn1() {
const foo = 'foo';
const bar = 'bar';
const say = () => {
console.log('hello');
};
say();
console.log(1 + 1, 2 + 2, aaa);
}
使用到了const
、箭头函数
,经过babel
转换成es5
后,代码的位置会和源码中不一样,那么在浏览器中如果仍然能够找到转换前的源码,则说明cheap-module-source-map
生效了
可以看到,在浏览器中确实能够看到转换前的源码,这就是
cheap-module-source-map
中module
的作用,事实上官方文档中这么多的配置项我们不需要害怕,只需要知道每个关键字是什么意思,那么它们组合起来无非就是各种特性的叠加而已
3.7 hidden-source-map
也是一个见名知意的配置项,相比于source-map
,就是将最后的//# sourceMappingURL=main.js.map
这句注释删除了,这也就意味着source-map
不会生效了,但是仍然会生成source-map
文件的
官方文档中给我们的建议是在只需要知道有错误出现时给我们在控制台输出出来的话就可以使用这种方式
3.8 nosources-source-map
这种方式能够在出现错误的时候告诉我们是源码中哪个文件第几行出错了,但是不会在浏览器中给我们生成源码
总结
了解完以上这几个devtool
配置项,就足够了,官网的26
个配置项就是根据eval
、hidden
、inline
、cheap
、module
、nosources
这几个关键字组合出来的
但是组合也是有规则的,官方文档中给出的规则如下:
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
转载自:https://juejin.cn/post/7098526261747646477