对应编译文件和源文件的方法
使得编译后的代码可读性高,容易调试

SourceMap配置

webpack的配置文件中配置source maps,需要配置devtool,在webpack4.0中提供了多种配置选项
SourceMap - 图1

下面我们列举四种不同的配置选项进行说明,各具优缺点,描述如下:

devtool选项 配置结果
source-map 在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map 在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map 使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;eval 的作用是使得打包后的 bundle 文件中每个模块代码使用 eval 去执行,保留每个模块的 eval 形式的文件,即便是指定了eval-source-map,仍然会在浏览器开发者工具中生成 webpack-internals:// 文件夹来保存一份里面是eval形式的模块文件;因为 eval 中为字符串形式,所以当源码变动的时候进行字符串处理会提升 rebuild 的速度
cheap-module-eval-source-map 这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

  1. const path = require('path');
  2. module.exports = {
  3. entry: "./app/main.js",
  4. devtool: 'eval-source-map',
  5. output: {
  6. filename: "bundle.js",
  7. path: path.resolve(__dirname, 'public')
  8. }
  9. }

cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

带有source-map关键字的选项会单独打一份.map文件(inline除外),而其他模式一般会将sourcemap文件合并在生成的bundle文件中,这有时会让打包的代码非常肿大。

使用SourceMap进行调试

1. 无SourceMap时

在不使用SourceMap这个选项的时候,我们生成的代码是经过压缩打包后的不适合人阅读调试的代码,在控制台Sources目录下看到:
SourceMap - 图2

2. sourceMap合并在bundle.js中

如图所示都是压缩过的代码,生成Sourcemap后我们就可以在浏览器中直接调试我们的源码,在控制台的sources下,点开可以看到webpack://目录,里面可以直接看到我们开发态的源代码,这样方便我们直接在浏览器中打断点调试。

SourceMap - 图3

3. 生成单独的.map文件

对于生成.map的情况chrome会提示你检测到.map文件
SourceMap - 图4

按command + p会提示你选择要调试的文件,进入文件后打断点调试即可

uglify-js生成SourceMap的整体流程如下
生成AST => 压缩 => 调用addMapping方法,添加对应的映射mapping => 输出压缩后代码时调用toJSON方法生成SourceMap

// source-map/lib/source-map-generator.js

SourceMapGenerator.prototype.toJSON =
  function SourceMapGenerator_toJSON() {
    var map = {
      version: this._version,
      sources: this._sources.toArray(),
      names: this._names.toArray(),
      // 关键是这一句,用了_serializeMappings方法来序列化输出
      mappings: this._serializeMappings()
    };
    // 返回sourceMap
    return map;
  };

// source-map/lib/source-map-generator.js

SourceMapGenerator.prototype._serializeMappings =
  function SourceMapGenerator_serializeMappings() {
    // 省略
    // mappings其实就是外部传进来的压缩前后的代码位置映射集,可以看下方的例子
    for (var i = 0, len = mappings.length; i < len; i++) {
      mapping = mappings[i];
      next = ''

      // 这一段其实就是为了修正如果压缩后文件头部有注释,那么需要在最终生成的映射
      // 前加上N个“;”来表示换行
      if (mapping.generatedLine !== previousGeneratedLine) {
        previousGeneratedColumn = 0;
        while (mapping.generatedLine !== previousGeneratedLine) {
          next += ';';
          previousGeneratedLine++;
        }
      }
      else {
        if (i > 0) {
          // 比较相邻的两个mapping,如果相同就跳过,继续下一个
          if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {
            continue;
          }
          // 同一行的每段映射后需要加上“,”
          next += ',';
        }
      }

      // 输出列 编码
      next += base64VLQ.encode(mapping.generatedColumn
                                 - previousGeneratedColumn);
      previousGeneratedColumn = mapping.generatedColumn;

      // 没有找到源文件的话,就会出现只有1个字段的映射的情况
      if (mapping.source != null) {
        sourceIdx = this._sources.indexOf(mapping.source);
        // 输入文件名索引 编码
        next += base64VLQ.encode(sourceIdx - previousSource);
        previousSource = sourceIdx;

        // 输入列 编码(SourceMap v3规定起始行是0)
        // lines are stored 0-based in SourceMap spec version 3
        next += base64VLQ.encode(mapping.originalLine - 1
                                   - previousOriginalLine);
        previousOriginalLine = mapping.originalLine - 1;

        // 输出行 编码
        next += base64VLQ.encode(mapping.originalColumn
                                   - previousOriginalColumn);
        previousOriginalColumn = mapping.originalColumn;

        // 没有名称的话,会出现4个字段的映射的情况
        // 主要是一些关键字或者符号等,不会被压缩,所以记录名称也就没有意义,但是还是
        // 需要记录它们的位置的
        if (mapping.name != null) {
          nameIdx = this._names.indexOf(mapping.name);
          next += base64VLQ.encode(nameIdx - previousName);
          previousName = nameIdx;
        }
      }

      result += next;
    }
  };
// test.js
// 压缩前                                 // 压缩后
function get (value) {       =>          function get(n){return n}
  return value
}

根据我们上面所盘点的,可以得到如下信息:

{
    "sources:" ["test.js"],
    "names": ["get", "value"] // get不是关键字,后续也有可能被压缩
}

输出位置(行,列)输入索引输入位置(行,列)名称索引代表内容VLQ+Base640,000,0无functionAAAA0,+90+0,+90getSAASA0,+40+1,+51入参valueIAAKC0,+30+1,-12无returnGACZ0,+70+0,+70返回值valueOAAOA
所以最终生成的SourceMap如下:

{
  "version": 3,
  "sources": ["test.js"],
  "names": ["get", "value"],
  "mappings": "AAAA,SAASA,IAAKC,GACZ,OAAOA"
}

https://juejin.im/post/5b9b93705188255c89011d70