> 文章列表 > SourceMap源码映射详细讲解

SourceMap源码映射详细讲解

SourceMap源码映射详细讲解

SourceMap源码映射详细讲解

前端工程打包后代码会跟项目源码不一致,当代码运行出错时控制台上定位出错代码的位置跟项目源码上不对应。这时候我们很难定位错误代码的位置。SourceMap的用途是可以将转换后的代码映射回源码,如果设置了js文件对应的map资源,那么就可以在控制台进行调试时直接定位到源码位置。

SourceMap生成方式

前端的构建工具很多,文章只举例两个常用的:vitewebpack

vite生成SourceMap

vite在文档介绍中可以看到直接设置build.sourcemap配置即可。

sourcemap可配置的值类型为(boolean, ‘inline’, ‘hidden’)几种:

  • boolean: true | false
    默认为false,不生成map文件。当设置成true时,会生成单独的map文件,并且在对应的bundle文件中生成相应注释指明map文件。

    SourceMap源码映射详细讲解

  • inline
    SourceMap源码映射详细讲解

    source map作为一个data url附加在输出文件中。

  • hidden

    true类似,生成一个map文件,但是在bundle问价中并不会生成注释。

webpack生成SourceMap

webpack中也只需要通过设置devtool配置即可。值有以下多种:

  • eval

    会生成被eval函数包裹的模块内容,其中添加了注释用来标识源文件位置(sourceURL用来指定文件名)
    SourceMap源码映射详细讲解

    这种方式因为不需要生成map文件,所以很快,只需要提供对应的源文件地址就可以就进行映射。但是缺少了很多映射信息(行、列等),同时eval方法因为安全问题也不建议使用。

  • source-map

    生成一个map文件,并在bundle文件中添加注释指向map文件。

  • cheap

    source-map类似,不过生成的map文件不会生成源码的列信息(只会映射到源码的行)跟loader中的sourcemap

    通常在定义错误时,只需要关注到行就可以知道错误原因,列信息不是非常必要,这样在打包时也能更快。不过对于需要经过多loader处理的文件,由于不会生成loader相关的sourcemap,可能会导致映射信息不精确。

  • module

    生成的sourcemap包含了loader相关的sourcemap信息。

  • inline

    viteinline配置一样,直接将生成的map文件内容作为data url添加到bundle文件中,不单独生成一个map文件。

  • hidden

    也和vitehidden配置一样。

  • nosources

    生成的map文件中不包含sourceContent字段(sourceContentsources字段都可以映射源码),使得map文件体积可以更小。

除了上述几个外,webpack还支持组合方式,详情可以[文档中的devtool配置]https://www.webpackjs.com/configuration/devtool/#devtool

sourceMap 使用

对于生成的map文件,我们需要解析工具将源代码跟sourcemap进行映射。

浏览器

在目前的浏览器中大多都默认开启了sourcemap映射功能。

在浏览器中按F12进入到开发者工具可以看到:
SourceMap源码映射详细讲解

如果js文件中有sourcemap注释,可以映射到源码中。

没有配置sourcemap时:
SourceMap源码映射详细讲解
SourceMap源码映射详细讲解

配置了sourcemap
SourceMap源码映射详细讲解
SourceMap源码映射详细讲解

手动映射

对于生产环境,为了安全一般都不会在浏览器中进行映射。但是为了能监控定位到错误,我们可以使用手动隐射的方式。

安装source-map

npm i source-map -D

启动一个node服务用来接受错误信息并进行记录:

const { SourceMapConsumer } = require('source-map');
const fs = require('fs');const rawSourceMap = fs.readFileSync(__dirname + '/dist/main.38f7f9c4.js.map', 'utf-8');
console.log(rawSourceMap)
originalPosition('main.38f7f9c4.js:733')
function originalPosition(info) {const [bundleName, line, column] = info.split(':');SourceMapConsumer.with(rawSourceMap, null, (consumer) => {const originalPosition = consumer.originalPositionFor({line: parseInt(line),column: parseInt(column)})console.log(originalPosition);})
}

SourceMap源码映射详细讲解

现在也有许多监控平台(例如sentry)可以实现源码映射,不需要我们手动映射。

SourceMap文件格式

使用webpack打包举例来看一个map文件里都有什么字段。

// index.js
function log() {for(let i = 0; i < 5; i++) {console.log(i)}
}log()

webpack的配置文件中添加devtool: 'source-map'设置。

// 打包后的文件
!function(){for(let o=0;o<5;o++)console.log(o)}();
//# sourceMappingURL=main.js.map

可以看到在打包过程中,代码经过压缩,去空格以及编译转化后,由于代码之间差异性过大,造成无法debug的问题。不过在打包文件的最后有一行//# sourceMappingURL=main.js.map注释指向了对应的map文件。

// main.js.map
{"version":3,"file":"main.js","mappings":"CAAA,WACE,IAAI,IAAIA,EAAI,EAAGA,EAAI,EAAGA,IACpBC,QAAQC,IAAIF,GAIhBE","sources":["webpack:///./index.js"],"sourcesContent":["function log() {\\r\\n  for(let i = 0; i < 5; i++) {\\r\\n    console.log(i)\\r\\n  }\\r\\n}\\r\\n\\r\\nlog()"],"names":["i","console","log"],"sourceRoot":""
}
  • version: 目前source map的标准版本是3。
  • file: 转换后的文件名。
  • mappings: 记录位置信息的字符串。
  • sources: 源文件地址列表,是一个数组,表示可能是多文件进行合并。
  • sourcesContent: 源文件内容(可选的源文件内容列表)。
  • names: 转换前的所有变量名和属性名。
  • sourceRoot: 源文件目录地址,可以用于重新定位服务器上的源文件。

mappings

上述的大部分字段都很好理解,就是mappings很令人疑惑。

为了尽可能减少存储空间且达到记录原始位置和目标位置的映射关系,mappings也是按照了一定的规则生成。

  1. 生成文件中的一行作为一组,用;隔开。

    比如说mappings字段为AAAAA,BBBBB;CCCCC表示转换后的源码分成两行,第一行有两个位置,第二行有一个位置。

  2. 连续的字母共同表示一个位置信息,用逗号分隔每个位置信息。
  3. 一个位置信息由145个可变长度的字段组成(generatedColumn,[sourceIndex, originLine,originColumn, [nameIndex]])。
  • 第一位表示这个位置在转换后的代码第几列,使用的是相较于上一个的相对位置,除非这个字段是第一次出现。
  • 第二位(可选)表示所在文件是属于sources属性中的第几个文件。
  • 第三位(可选)表示转换前代码的第几行。
  • 第四位(可选)表示转换前代码的第几列。
  • 第五位(可选)表示属于names属性中的第几个变量(如果该位置没有对应names属性中的变量,可以省略第五位)。
  1. 字段生成原理是将数值通过vlq编码转成字母。

编码原理

SourceMap的编码流程是将位置从 十进制 -> 二进制 -> vlq编码 -> base64编码生成最终的字母。

vlq编码

vlqVariable-length quantity的缩写,是一种通用、使用任意位数的二进制来表示一个任意大数字的一种编码方式。

规则:

  • 一个数值可能由多个字符组成
  • 每个字符使用6个二进制
    • 如果是表示数值的第一个字符中的最后一个位置,则为符号位,否则用于实际有效值的一位。
    • 0为正,1为负(sourcemap的符号固定为0)
    • 第一个位置是连续位,如果是1,表示下一个字符也属于同一个数值;如果是0,表示这个字符是表示这个数值的最后一个字符。
    • 最后一个位置
  • 至少含有4个有效值,数值范围是-11111111,也就是-1515(十进制)可以由一个字符表示
    • 数值的第一个字符有4个有效值
    • 之后的字符有5个有效值。

最后将6个二进制转成base64编码的字母:
SourceMap源码映射详细讲解

编码实例 - 将29进行 vlq编码

  1. 将29转换成二进制11101
  2. 在最右边补充符号位,29是正数,符号位为0,整个数变成111010
  3. 从右边的最低位开始,将整个数每隔5位,进行分段,变成111010,如果最高位所在的段不足5位,则前面补0,因此两段变成0000111010
  4. 将两段顺序调转变成1101000001
  5. 在每一段的最前面添加一个连续位,除了最后一段为0,其他都变成1,变成111010000001
  6. 将每段都转成base64编码为6B,所以最终29在经过编码后成6B