前端工程打包后代码会跟项目源码不一致,当代码运行出错时控制台上定位出错代码的位置跟项目源码上不对应。这时候我们很难定位错误代码的位置。SourceMap
的用途是可以将转换后的代码映射回源码,如果设置了js
文件对应的map
资源,那么就可以在控制台进行调试时直接定位到源码位置。
前端的构建工具很多,文章只举例两个常用的:vite
和webpack
在vite
在文档介绍中可以看到直接设置build.sourcemap
配置即可。
sourcemap
可配置的值类型为(boolean, ‘inline’, ‘hidden’)几种:
boolean: true | false
默认为false
,不生成map
文件。当设置成true
时,会生成单独的map
文件,并且在对应的bundle
文件中生成相应注释指明map
文件。
将source map
作为一个data url
附加在输出文件中。
hidden
跟true
类似,生成一个map
文件,但是在bundle
问价中并不会生成注释。
在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
对于生成的map
文件,我们需要解析工具将源代码跟sourcemap
进行映射。
在目前的浏览器中大多都默认开启了sourcemap
映射功能。
如果js
文件中有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
)可以实现源码映射,不需要我们手动映射。
使用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
字段为AAAAA,BBBBB;CCCCC
表示转换后的源码分成两行,第一行有两个位置,第二行有一个位置。
1
、4
或5
个可变长度的字段组成(generatedColumn,[sourceIndex, originLine,originColumn, [nameIndex]]
)。sources
属性中的第几个文件。names
属性中的第几个变量(如果该位置没有对应names
属性中的变量,可以省略第五位)。vlq
编码转成字母。SourceMap
的编码流程是将位置从 十进制 -> 二进制 -> vlq
编码 -> base64
编码生成最终的字母。
vlq
编码
vlq
是Variable-length quantity
的缩写,是一种通用、使用任意位数的二进制来表示一个任意大数字的一种编码方式。
规则:
sourcemap
的符号固定为0)-1111
到1111
,也就是-15
到15
(十进制)可以由一个字符表示编码实例 - 将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