手把手带你学webpack(6)-- source-map

643 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

本篇文章对应源码: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文件,其数据结构如下

  • versionsource-map文件的版本,目前最新是第三版
  • file:浏览器加载时的文件名
  • mappings:记录和源码之间位置信息的映射关系,比如哪个函数在源码的第几行第几列,用到了base64 VQL(variable-length-quantity)可变长度值编码
  • sources:源码的文件路径以及webpack运行时环境的一些代码路径
  • sourcesContent:源码中的代码内容
  • sourceRootsources中的源码的根目录

3. webpack中使用source-map

3.1 eval

大家都知道webpackdevelopmentproduction两种模式

development模式下,默认使用的devtoolseval,也就是把我们的项目源码放到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函数的时候解析到最后的注释就知道该去哪里找对应的源码了 image.png 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()],
};

image.png 这种方式适用于生产环境下需要在出现错误的时候知道源码位置时使用


3.3 eval-source-map

结合了evalsource-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"
);

image.png 这种方式适用于在开发模式下需要精确的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==

从官方文档可以看到,这种方式的构建速度是最慢的,只适用于构建单个文件的时候使用 image.png


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代码 像这种有loaderjs进行转换的场景下,想要保证正确的source-map就需要使用到带有moduledevtool了,因为除了cheap-module-source-map,还有很多别的方式也是有module的,只要是在官方文档中看到带有moduledevtool都是具有这种特性

下面就以babel为例,我们通过babel-loaderjs进行转换,然后看看能否正确对应到转换前的代码 首先安装如下依赖

pnpm i @babel/core @babel-preset-env babel-loader
  • @babel/corebabel的核心,所有功能都要在这个包的基础上运行
  • @babel-preset-env让我们可以不需要考虑转换成什么版本的js,它会根据要适配的浏览器自动转换成能兼容相应浏览器的版本,这里我们使用它主要是能够将我们写的es6代码转成es5,从而让我们的源码和打包后的结果有差异,方便观察source-map是否生效

image.png

  • 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生效了 image.png 可以看到,在浏览器中确实能够看到转换前的源码,这就是cheap-module-source-mapmodule的作用,事实上官方文档中这么多的配置项我们不需要害怕,只需要知道每个关键字是什么意思,那么它们组合起来无非就是各种特性的叠加而已


3.7 hidden-source-map

也是一个见名知意的配置项,相比于source-map,就是将最后的//# sourceMappingURL=main.js.map这句注释删除了,这也就意味着source-map不会生效了,但是仍然会生成source-map文件的 image.png 官方文档中给我们的建议是在只需要知道有错误出现时给我们在控制台输出出来的话就可以使用这种方式


3.8 nosources-source-map

这种方式能够在出现错误的时候告诉我们是源码中哪个文件第几行出错了,但是不会在浏览器中给我们生成源码 image.png image.png


总结

了解完以上这几个devtool配置项,就足够了,官网的26个配置项就是根据evalhiddeninlinecheapmodulenosources这几个关键字组合出来的

但是组合也是有规则的,官方文档中给出的规则如下:

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map