一、什么是SourceMap?
首先来看一个常见的错误:
这种报错我们在开发过程中经常会遇到,这其实就是一个错误的堆栈,栈顶是具体报错的信息,此时我们会发现后面的小括号里给我们提示了 (index.js: 28) ,我们点进去看
发现浏览器帮我们定位到报错的具体位置,而这个位置恰好就是我们开发环境下的位置,但是我们知道我们实际运行在浏览器上的代码都是经过打包、压缩后的代码,那浏览器是怎么做到的呢?
假如要我们去实现,最简单的方法就是去创建一个文件保存压缩前的代码和压缩后的代码之间的一个映射关系,其实这也就是sourcemap的原理,只是他在保存这种映射关系上做了很多优化。
至此我们知道了,sourcemap就是对源码的映射,可以讲压缩后的代码再对应回压缩前的代码,使得我们可以快速定位报错的位置。
二、SourceMap的结构
我们先手动去生成一个sourcemap文件,首先创建一个项目 :
mkdir demo
cd demo
npm init
然后全局安装一下 uglify-js 这个包,用来对我们代码进行压缩并生成sourcemap文件的。
npm install uglify-js -g
现在来写代码,没错就这么简单:
//index.js
function add(a, b) {
return a + b;
}
接下来就使用uglify-js来生成对应的sourcemap文件:
uglifyjs index.js -o output.js --source-map "url='/output.js.map'"
最终就会生成两个文件,一个是压缩后的文件,一个是sourcemap文件:
//output.js
function add(a,b){return a+b}
//# sourceMappingURL=/output.js.map
//output.js.map
{
"version":3, //source map的版本
"sources":["index.js"], //转换前的文件,他是一个数组,因为压缩后的文件可能由几个不同的文件组合起来的
"names":["add","a","b"], //转换前所有的变量名和属性名
"mappings":"AAAA,SAASA,IAAIC,EAAGC,GACd,OAAOD,EAAIC" // 用来记录映射信息
}
其实使用
三、VLQ编码
在讲解VLQ编码之前,极力推荐先去看下这篇文章,文章讲述了如何使用VLQ对位置信息进行编码,先面我们将反过来操作,也就是对mappings信息进行反解,看看原始位置信息到底是啥。
首先我们需要下一个反解包 vlq 。
npm install vlq
对我们的 mappings 字符串进行反解。
const vlq = require('vlq')
const mappings = "AAAA,SAASA,IAAIC,EAAGC,GACd,OAAOD,EAAIC";
const arr = mappings.split(",");
const decodeArr = arr.map((str) => vlq.decode(str));
console.log(decodeArr);
//打印结果
/*[
[ 0, 0, 0, 0 ],
[ 9, 0, 0, 9, 0 ],
[ 4, 0, 0, 4, 1 ],
[ 2, 0, 0, 3, 1 ],
[ 3, 0, 1, -14 ],
[ 7, 0, 0, 7, -1 ],
[ 2, 0, 0, 4, 1 ]
]*/
因为现在还是使用的相对位置,我们再转换一下就明了些。
const mappings = "AAAA,SAASA,IAAIC,EAAGC,GACd,OAAOD,EAAIC";
const arr = mappings.split(",");
const decodeArr = arr.map((str) => vlq.decode(str));
const initArr = [0, 0, 0, 0, 0];
const absoluteArr = decodeArr.map((item) => {
const res = [];
for (let i = 0; i < item.length; i++) {
initArr[i] += item[i];
res.push(initArr[i]);
}
return res;
});
console.log(absoluteArr);
/*[
[ 0, 0, 0, 0 ],
[ 9, 0, 0, 9, 0 ],
[ 13, 0, 0, 13, 1 ],
[ 15, 0, 0, 16, 2 ],
[ 18, 0, 1, 2 ],
[ 25, 0, 1, 9, 1 ],
[ 27, 0, 1, 13, 2 ]
]*/
根据下面这张表来解释上面的二维数组。
| 原始代码 | 原始文件(sources) | 压缩后的代码 | 变量(names) |
|---|---|---|---|
| 行 0 ,列 9 | index.js | 行 0 ,列 9 | add |
| 行 0, 列 13 | index.js | 行 0 ,列 13 | a |
| 行 0, 列 16 | index.js | 行 0 ,列 15 | b |
| 行 1 ,列 9 | index.js | 行 0 ,列 25 | a |
| 行 1, 列 13 | index.js | 行 0 ,列 27 | b |
第一行是 [ 0, 0, 0, 0 ] 很明显他代表的就是起点即 function ,第二行[ 9, 0, 0, 9, 0 ] 代表的就是 add ,对应关系如下:
之后以此类推就可以。
四、sourceMap在前端监控中的使用
在前端监控系统中使用sourceMap就是为了将捕获到的错误的具体位置以及产生报错的代码展示出来,那首先就是要去捕获错误,js错误的捕获要分三类:
- 普通js错误
对于普通js错误比如ReferenceError、RangeError、InternalError等,对于这些错误可以全局监听 error 事件。
window.addEventListener('error', handleError)
- promise异常
这种错误会在Promise被reject且没有reject处理器的时候出现,此时我们也可以全局监听 unhandledrejection 事件。
window.addEventListener('unhandledrejection', handleRejection)
- script引入的第三方资源报错
由于浏览器跨域的限制,非同域下的脚本执行抛错,捕获异常的时候,不能拿到详细的异常信息,只能拿到类似 script error 0 这类信息,那如果我们要捕获这类错误就得在script标签上加上 crossorigin = “anonymous”属性。
<script src="http://xxxx" crossorigin="anonymous"></script>
加上这个属性之后就会开启CORS模式, 请求资源时会带上一些信息比如Origin字段去验证请求者的身份,同时服务端也要进行相应的处理比如响应投中增加 Access-Control-Allow-Origin 属性。
现在我们拿到报错信息,比如type、message、stack等, 但是很不幸的是不同的引擎stack格式规范不同,比如我们的V8他就有自己的一套格式规范,具体格式可见 stack-trace-format ,下面列出了三种浏览器的格式:
- Chrome
- Firefox
- Safari
我们发现这三种浏览器的stack格式各不相同,因此对于我们的监控平台就需要将这些格式分别进行格式化处理。