1

背景

Source Map 的概念在前端工具链中已存在多年,随着前端工程化的兴起,Source Map 的重要性日益增加,在越来越多的前端项目里被使用。

什么是 Source Map?

Source Map 本身是一种文件,它提供了原始文件与编译后的文件之间的映射规则,使得开发者能够调试原始代码,帮助开发人员进行调试和排查。

为什么需要 Source Map?

在如今的前端开发中,代码经过打包工具进行打包和压缩后,已经不适合开发人员去阅读,当开发人员需要调试或者排查问题时,调试和报错信息都指向处理过后的代码,难以进行理解和调试。Source Map 根据映射规则能够提供原始代码,节省开发人员的时间。

如何使用 Source Map?

在 webpack 官方文档里面有着多种 Source Map 的配置,如何在不同的环境下选择合适的 Source Map 这个问题困扰了许多刚刚接触 Webpack Source Map 配置的开发人员。
这里我列出官方文档链接: https://webpack.docschina.org/configuration/devtool#root
查看过后发现非常多的配置项,但是仔细观察的话可以发现所有配置项汇总来看都离不开固定的这几个词汇:

  • eval
  • source-map
  • cheap
  • inline
  • nosources
  • hidden

实际上每个单词都有自己的含义,他们之间能相互组合来形成新的配置项,只要理解了上面的这几个词汇所代表的含义,那么现有的所有 Source Map 配置项的含义都不言而喻了。

eval

首先需要了解下 JavaScript 里面的 eval 函数,其作用就是将传入的参数当做 JavaScript 代码去执行。
eval 模式下 Source Map 文件是如何被引入的,可以看一个例子。
在浏览器控制台输入一个简单的打印语句
image.png
发现最右边有VM507:1这样的字眼,这个其实是为运行打印语句而临时创立的虚拟机环境,点击VM507:1,跳到了一个新生成的 tab,里面的内容就是打印语句。
image.png
我们通过sourceURL来声明代码指向的所属的文件路径,具体的格式为//# sourceURL=xxx.js
image.png
上面图片右侧变成了 foo.js,说明 sourceURL 的指定生效了,尝试点击一下。
image.png
发下也新开了一个 tab,里面的内容就是传递给 eval 函数的字符串内容。
可能有人就会问,上面的这部分内容和 eval 模式有什么关系,我们接着往下看。
首先把 webpack 的配置文件里面 Source Map 的模式设置为 eval

module.exports = {
  ...
  devtool: "eval"
}

测试代码如下

// index.js
import Image from "./assets/redux的副本.png";

import { add } from "./module-1";
import { multiple } from "./module-2";

console.log("===", Image);

console.log(add(1, 2));

console.log(multiple(1, 2));

// module-1.js
export const add = (a, b) => {
  return a + b;
};

// module-2.js
export const multiple = (a, b) => {
  return a * b;
};

查看打包结果

/* 2 */
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  eval(
    "__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   add: () => (/* binding */ add)\n/* harmony export */ });\nvar add = function add(a, b) {\n  console.log(c);\n  return a + b;\n};\n\n//# sourceURL=webpack://webpack-test/./src/module-1.js?"
  );
},
  /* 3 */
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    eval(
      "__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   multiple: () => (/* binding */ multiple)\n/* harmony export */ });\nvar multiple = function multiple(a, b) {\n  return a * b;\n};\n\n//# sourceURL=webpack://webpack-test/./src/module-2.js?"
    );
  };

发现每个模块都是用了 eval 函数,里面的参数最末尾都是使用 sourceURL 来指向所属文件。
image.png

source-map

我们把devtool的值改成eval-source-map,相比之前eval模式追加了source-map,来看下效果。

/* 2 */
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  eval(
    "__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   add: () => (/* binding */ add)\n/* harmony export */ });\nvar add = function add(a, b) {\n  console.log(c);\n  return a + b;\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMi5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQU8sSUFBTUEsR0FBRyxHQUFHLFNBQU5BLEdBQUdBLENBQUlDLENBQUMsRUFBRUMsQ0FBQyxFQUFLO0VBQzNCQyxPQUFPLENBQUNDLEdBQUcsQ0FBQ0MsQ0FBQyxDQUFDO0VBQ2QsT0FBT0osQ0FBQyxHQUFHQyxDQUFDO0FBQ2QsQ0FBQyIsInNvdXJjZXMiOlsid2VicGFjazovL3dlYnBhY2stdGVzdC8uL3NyYy9tb2R1bGUtMS5qcz82ZmMyIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjb25zdCBhZGQgPSAoYSwgYikgPT4ge1xuICBjb25zb2xlLmxvZyhjKTtcbiAgcmV0dXJuIGEgKyBiO1xufTtcbiJdLCJuYW1lcyI6WyJhZGQiLCJhIiwiYiIsImNvbnNvbGUiLCJsb2ciLCJjIl0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2\n"
  );
},
  /* 3 */
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    eval(
      "__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   multiple: () => (/* binding */ multiple)\n/* harmony export */ });\nvar multiple = function multiple(a, b) {\n  return a * b;\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMy5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQU8sSUFBTUEsUUFBUSxHQUFHLFNBQVhBLFFBQVFBLENBQUlDLENBQUMsRUFBRUMsQ0FBQyxFQUFLO0VBQ2hDLE9BQU9ELENBQUMsR0FBR0MsQ0FBQztBQUNkLENBQUMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly93ZWJwYWNrLXRlc3QvLi9zcmMvbW9kdWxlLTIuanM/YWRjYiJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgY29uc3QgbXVsdGlwbGUgPSAoYSwgYikgPT4ge1xuICByZXR1cm4gYSAqIGI7XG59O1xuIl0sIm5hbWVzIjpbIm11bHRpcGxlIiwiYSIsImIiXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///3\n"
    );
  };

因为 eval-source-map 里面包含了 eval,所以整个模块的代码仍旧被 eval 函数所包裹,因为多出了 source-map,所以代码指定所属文件的方式发生了变化,通过 sourceMappingURL 指定了原文件,但是原代码文件内容以 dataURL 的方式内嵌进了打包文件里。
image.png
这里我们对代码进行一点调整,故意使用一个未声明的变量。

// module-1.js
export const add = (a, b) => {
  console.log(c);
  return a + b;
};

去浏览器查看控制台信息
image.png
发现报错不仅指明了文件名称,还请求的标明了错误所在行列。

cheap

我们在上一步的配置基础上增加 cheap,devtool 设置为 eval-cheap-source-map。
继续观察打包文件,基本看不出差异

/* 2 */
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  eval(
    "__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   add: () => (/* binding */ add)\n/* harmony export */ });\nvar add = function add(a, b) {\n  console.log(c);\n  return a + b;\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMi5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQUE7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly93ZWJwYWNrLXRlc3QvLi9zcmMvbW9kdWxlLTEuanM/Njg2OCJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgdmFyIGFkZCA9IGZ1bmN0aW9uIGFkZChhLCBiKSB7XG4gIGNvbnNvbGUubG9nKGMpO1xuICByZXR1cm4gYSArIGI7XG59OyJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2\n"
  );
},
  /* 3 */
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    eval(
      "__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   multiple: () => (/* binding */ multiple)\n/* harmony export */ });\nvar multiple = function multiple(a, b) {\n  return a * b;\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMy5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQUE7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vd2VicGFjay10ZXN0Ly4vc3JjL21vZHVsZS0yLmpzPzdjZjIiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IHZhciBtdWx0aXBsZSA9IGZ1bmN0aW9uIG11bHRpcGxlKGEsIGIpIHtcbiAgcmV0dXJuIGEgKiBiO1xufTsiXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3\n"
    );
  };

在浏览器查看打印结果
image.png
对比没有添加 cheap 的时候,发现少了报错代码所在的列信息。cheap 翻译过来就是廉价的,在这里可以理解为阉割版的意思。

module

webpack 会对模块进行多次处理,每次处理都会生成 sourcemap,最终会存在多个 sourcemap。
sourcemap.png
默认情况下 sourcemap 只能从打包文件关联到模块的代码。
sourcemap.png
上面 eval-cheap-source-map 调试时发现是 babel-loader 转译过后的代码,如果需要调试最初的源码就需要加上 module,它会将每次的 sourcemap 关联起来,最终能够映射回最初的源码。
我们现在把 devtool 更改为 eval-cheap-module-source-map 并对比上面配置为 eval-cheap-source-map 来看看效果。
image.png
如预料一般,展示了最初的源码。

inline

在之前配置项含有 eval 字符串时,每个模块内都会有 eval 函数包裹模块内容,sourcemap 的映射是从模块开始映射,当更换成 inline 后,sourcemap 的映射是从整个 bundle 映射。
我们将 devtool 设置为 inline-cheap-source-map
查看打包文件image.png
整个文件只在文件末尾有指向源文件的 sourceMappingURL,这就是和 eval 的不同之处。

nosources

当设置为非 nosources 时,生成的 sourcemap 文件里面会包含 sourceContent,会把源码直接以字符串的形式保存在 sourceContent 里面,在调试时能够使用源码来调试。
当设置了 nosources 时,生成的 sourcemap 文件里面不再包含 sourceContent,在浏览器上调试时会提示找不到源文件,但是却能定位到具体的文件。
image.png

hidden

当设置 hidden 时,生成的打包文件不会关联 sourcemap 文件,也就是不会在文件末尾通过 sourceMappingURL 的方式来指明源文件,在打包过程还是会产生 sourcemap 文件。
我们把 devtool 设置为 hidden-source-map,看下效果。
image.png
看到确实产生了 sourcemap 文件
image.png
生成的打包文件没有关联 sourceMappingURL
image.png
打断点时也不会跳转到源文件。

如何选择

开发环境建议使用eval-cheap-module-source-map,我这里讲下原因

  • 前端项目都引入了模块化机制,在 loader 转换过后差别较大,需要调试 loader 转换前的代码
  • 前端项目基本引入了格式化的工具,例如 Prettier,一行的长度基本在 80-120 个字符宽度,定位行数已经足够排查。
  • 更改文件后,重新编译速度快。

生产环境建议使用 none,也就是不生成 sourcemap 文件,防止有人通过 sourcemap 复原源代码。

建议

前面提到的几个关键词汇目前都已经提及并进行解释,面对官方提供的繁杂的 sourcemap 配置项,可以拆解后再做分析,如果对上面的内容有所理解,是完全能够知道最终结果的。
因为 webpack 配置文件是支持数组形式的,可以自行充分验证每一项是否符合上面所述。

// 可以自行补充
const sourceMapList = [
  "eval",
  "eval-source-map",
  "inline-cheap-source-map",
  "eval-nosources-cheap-source-map",
];

module.exports = sourceMapList.map((item) => {
  return {
    mode: "none",
    devtool: item,
    entry: path.resolve(__dirname, "src/index.js"),
    output: {
      filename: `js/${item}.js`,
      path: path.resolve(__dirname, "dist"),
      clean: true,
    },
    module: {
      rules: [
        {
          test: /\.(j|t)sx?/,
          use: [
            {
              loader: "babel-loader",
              options: {
                presets: ["@babel/preset-env"],
              },
            },
          ],
        },
      ],
    },
    devServer: {
      static: path.resolve(__dirname, "dist"),
      hot: true,
      port: 3000,
      open: true,
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, "public", "index.html"),
        filename: `${item}.html`,
      }),
    ],
  };
});

截图如下
image.png


Tqing
112 声望16 粉丝