SourceMap源码映射详细讲解
前端工程打包后代码会跟项目源码不一致,当代码运行出错时控制台上定位出错代码的位置跟项目源码上不对应。这时候我们很难定位错误代码的位置。SourceMap
的用途是可以将转换后的代码映射回源码,如果设置了js
文件对应的map
资源,那么就可以在控制台进行调试时直接定位到源码位置。
SourceMap生成方式
前端的构建工具很多,文章只举例两个常用的:vite
和webpack
vite生成SourceMap
在vite
在文档介绍中可以看到直接设置build.sourcemap
配置即可。
sourcemap
可配置的值类型为(boolean, ‘inline’, ‘hidden’)几种:
-
boolean: true | false
默认为false
,不生成map
文件。当设置成true
时,会生成单独的map
文件,并且在对应的bundle
文件中生成相应注释指明map
文件。 -
inline
将
source map
作为一个data url
附加在输出文件中。 -
hidden
跟
true
类似,生成一个map
文件,但是在bundle
问价中并不会生成注释。
webpack生成SourceMap
在webpack
中也只需要通过设置devtool
配置即可。值有以下多种:
-
eval
会生成被
eval
函数包裹的模块内容,其中添加了注释用来标识源文件位置(sourceURL
用来指定文件名)
这种方式因为不需要生成
map
文件,所以很快,只需要提供对应的源文件地址就可以就进行映射。但是缺少了很多映射信息(行、列等),同时eval
方法因为安全问题也不建议使用。 -
source-map
生成一个
map
文件,并在bundle
文件中添加注释指向map
文件。 -
cheap
跟
source-map
类似,不过生成的map
文件不会生成源码的列信息(只会映射到源码的行)跟loader
中的sourcemap
。通常在定义错误时,只需要关注到行就可以知道错误原因,列信息不是非常必要,这样在打包时也能更快。不过对于需要经过多
loader
处理的文件,由于不会生成loader
相关的sourcemap
,可能会导致映射信息不精确。 -
module
生成的
sourcemap
包含了loader
相关的sourcemap
信息。 -
inline
和
vite
的inline
配置一样,直接将生成的map
文件内容作为data url
添加到bundle
文件中,不单独生成一个map
文件。 -
hidden
也和
vite
的hidden
配置一样。 -
nosources
生成的
map
文件中不包含sourceContent
字段(sourceContent
和sources
字段都可以映射源码),使得map
文件体积可以更小。
除了上述几个外,webpack
还支持组合方式,详情可以[文档中的devtool
配置]https://www.webpackjs.com/configuration/devtool/#devtool
sourceMap 使用
对于生成的map
文件,我们需要解析工具将源代码跟sourcemap
进行映射。
浏览器
在目前的浏览器中大多都默认开启了sourcemap
映射功能。
在浏览器中按F12
进入到开发者工具可以看到:
如果js
文件中有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);
})
}
现在也有许多监控平台(例如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
也是按照了一定的规则生成。
- 生成文件中的一行作为一组,用
;
隔开。比如说
mappings
字段为AAAAA,BBBBB;CCCCC
表示转换后的源码分成两行,第一行有两个位置,第二行有一个位置。 - 连续的字母共同表示一个位置信息,用逗号分隔每个位置信息。
- 一个位置信息由
1
、4
或5
个可变长度的字段组成(generatedColumn,[sourceIndex, originLine,originColumn, [nameIndex]]
)。
- 第一位表示这个位置在转换后的代码第几列,使用的是相较于上一个的相对位置,除非这个字段是第一次出现。
- 第二位(可选)表示所在文件是属于
sources
属性中的第几个文件。 - 第三位(可选)表示转换前代码的第几行。
- 第四位(可选)表示转换前代码的第几列。
- 第五位(可选)表示属于
names
属性中的第几个变量(如果该位置没有对应names
属性中的变量,可以省略第五位)。
- 字段生成原理是将数值通过
vlq
编码转成字母。
编码原理
SourceMap
的编码流程是将位置从 十进制 -> 二进制 -> vlq
编码 -> base64
编码生成最终的字母。
vlq
编码
vlq
是Variable-length quantity
的缩写,是一种通用、使用任意位数的二进制来表示一个任意大数字的一种编码方式。
规则:
- 一个数值可能由多个字符组成
- 每个字符使用6个二进制
-
- 如果是表示数值的第一个字符中的最后一个位置,则为符号位,否则用于实际有效值的一位。
-
- 0为正,1为负(
sourcemap
的符号固定为0)
- 0为正,1为负(
-
- 第一个位置是连续位,如果是1,表示下一个字符也属于同一个数值;如果是0,表示这个字符是表示这个数值的最后一个字符。
-
- 最后一个位置
- 至少含有4个有效值,数值范围是
-1111
到1111
,也就是-15
到15
(十进制)可以由一个字符表示 -
- 数值的第一个字符有4个有效值
-
- 之后的字符有5个有效值。
最后将6个二进制转成base64
编码的字母:
编码实例 - 将29进行
vlq
编码
- 将29转换成二进制
11101
- 在最右边补充符号位,29是正数,符号位为0,整个数变成
111010
- 从右边的最低位开始,将整个数每隔5位,进行分段,变成
1
和11010
,如果最高位所在的段不足5位,则前面补0,因此两段变成00001
和11010
- 将两段顺序调转变成
11010
和00001
- 在每一段的最前面添加一个连续位,除了最后一段为0,其他都变成1,变成
111010
和000001
- 将每段都转成
base64
编码为6
和B
,所以最终29在经过编码后成6B