在上一篇文章中,我们深入探讨了Webpack Plugin的工作原理和开发实践。今天,我们将继续Webpack系列,聚焦于一个同样重要的主题------SourceMap。作为现代前端开发中不可或缺的调试工具,SourceMap能够显著提升开发效率和调试体验。让我们一起来揭开SourceMap的神秘面纱。
什么是SourceMap❓
SourceMap是一种映射关系文件,它将编译、压缩的代码映射原代码。在开发过程中,我们经常遇到如下场景:
- 使用TS等预编译语言
- 使用ES6高级语法需通过Babel转译
- 对代码进行压缩、混淆
- 将多个文件打包合并
以上处理后生成的运行代码与原始代码差异巨大,给调试代码来了巨大的困难。SourceMap正是解决这一问题的关键技术。
SourceMap配置
在Webpack里可以通过devtool配置eval、source-map、cheap、module、inline这些关键词相互组合的值,达到不同SourceMap的效果。
            
            
              js
              
              
            
          
          module.exports = {
  devtool: 'eval-source-map'
}每个关键词的作用
| 关键词 | 作用 | 特点 | 使用场景 | 
|---|---|---|---|
| eval | 通过 eval函数执行模块代码 | 构建和重构速度最快 | 开发环境、需要快速的构建速度 | 
| source-map | 生成独立的.map文件 | 映射质量高 | 生产环境、高质量错误跟踪 | 
| cheap | 减少VLQ编码的计算量,减少source-map的体积 | 只映射行号,不映射列号,提升性能 | 开发环境、减少source-map的体积 | 
| module | 包含loader的sourcemap信息 | 对于使用babel、ts的项目方便定位 | 开发环境、以便使用loader的文件定位问题 | 
| inline | 将sourcemap作为DataURL嵌入到bundle中 | 不需要额外的.map文件,但增加了bundle的体积 | 开发环境 | 
SourceMap的推荐配置
开发环境 - eval-cheap-module-source-map
        
            
            
              js
              
              
            
          
          module.exports = {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map'
}生产环境 - source-map
        
            
            
              js
              
              
            
          
          module.exports = {
  mode: 'production',
  devtool: 'source-map'
}如果不想让用户看到.map文件,可以配置hidden-source-map。配置后生成的.map文件不包含引用注释,需要手动关联。
SoureMap的底层原理
生成SourceMap的方法
生成SourceMap的方法很多,我比较喜欢uglify-js的API生成SourceMap文件
安装uglify-js
            
            
              bash
              
              
            
          
          npm i uglify-js源文件内容
            
            
              js
              
              
            
          
          let a = 1;
let b = 2;
let c = 3;生成SourceMap
            
            
              js
              
              
            
          
          const UglifyJS = require("uglify-js");
const fs = require("fs");
const path = require("path");
const result = UglifyJS.minify(
  {
    "index.js": fs.readFileSync(path.join(__dirname, "./src/index.js"), "utf8"), // 读取生成source map的源文件
  },
  {
    compress: false, // 代码不进行压缩
    output: {
      beautify: true,
      indent_level: 2,
    },
    sourceMap: {
      filename: "index.min.js",
      url: "index.min.js.map",
    },
  }
);
fs.writeFileSync("index.min.js", result.code);
fs.writeFileSync("index.min.js.map", result.map);处理后的源代码
            
            
              js
              
              
            
          
          let a = 1;
let b = 2;
let c = 3;
// 此行浏览器会解析此行注释 获取.map文件通过VLQ编码获取源文件精准定位
//# sourceMappingURL=index.min.js.map SourceMap文件格式
            
            
              js
              
              
            
          
          {
  "version": 3,
  "file": "index.min.js",
  "sources": [
    "index.js"
  ],
  "names": [
    "let",
    "a",
    "b",
    "c"
  ],
  "mappings": "AAAAA,IAAIC,IAAI;;AACRD,IAAIE,IAAI;;AACRF,IAAIG,IAAI"
}整个文件其实就是一个JS对象,可以被解释器读取。主要有以下几个属性:
- version Source Map的版本 目前为3
- file 转换后的文件名
- sourceRoot 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
- sources 转换前的文件。值为数组类型,表示可以存在多个文件合并
- names 转换前的所有变量名和属性名
- mappings 记录位置信息和字符串,后续详解
mapping属性
mapping属性的字符串值是SourceMap的灵魂,以最少得字符表示最多的映射信息。编码规则如下:
- 按行分组 :mapping字符串首先以;分隔,每个分号代表转换后代码的一行。如AAAA,IAAIC,IAAI;;AACRD,IAAIE,IAAI;;AACRF,IAAIG,IAAI代码转换后的代码有5行
- 按段分隔 :每一行用,号分隔成多个映射段。每段代表该行的一个位置(通常是某个词法标记的开始)
- 相对位置:每段是VLQ编码的字符串,通常包含1、4或5个字段(不是"变量"),分别表示:
- 生成代码的列位置
- 源文件索引
- 源文件行位置
- 源文件列位置
- (可选)names数组中的变量索引
一个典型的 4 段 VLQ 编码 AAAA 解码后可能代表 [0, 0, 0, 0],它的含义是:
- 生成的列(Generated Column) : 0
- 源文件索引(Source Index) : 0(对应sources数组中的第一个文件)
- 原始行(Original Line) : 0(第 1 行)
- 原始列(Original Column) : 0
💡 解释
VLQ 编码最早用于MIDI文件,后来被多种格式采用。它的特点就是可以非常精简地表示很大的数值。VLQ编码是变长的。如果(整)数值在-15到+15之间(含两个端点),用一个字符表示;超出这个范围,就需要用多个字符表示。它规定,每个字符使用6个两进制位,正好可以借用Base 64编码的字符表。

有可能有第五个数字,但不是必需的,如果有的话,表示属于names中的哪个变量。再看一个例子:
            
            
              js
              
              
            
          
          // 源码
let a = 1;通过uglify-js处理后的mapping值为:
            
            
              js
              
              
            
          
          {
  "version": 3,
  "file": "index.min.js",
  "sources": [
    "index.js"
  ],
  "names": [
    "let",
    "a"
  ],
  "mappings": "AAAAA,IAAIC,EAAI"
}通过VLQ编码转换后得出映射信息
            
            
              js
              
              
            
          
          [0,0,0,0,0], [4,0,0,4,1], [2,0,0,4]- 
0,0,0,0,0\] 对应源文件的标识符`let` 
- 
2,0,0,4\] 对应源文件的标识符`;` 
小结
通过本文的学习,我们深入了解了SourceMap在前端开发中的重要作用:
- 调试利器:SourceMap解决了编译后代码难以调试的问题,让我们能够在浏览器中直接调试原始源代码
- 灵活配置:Webpack提供了多种devtool配置选项,我们可以根据开发和生产环境的不同需求选择合适的SourceMap策略
- 底层原理:SourceMap通过VLQ编码和映射关系,实现了编译后代码与源代码之间的精确定位
掌握SourceMap的工作原理和配置技巧,能够显著提升我们的开发效率和调试体验。希望本文能帮助大家更好地理解和使用这一重要工具!