sourceMap
于今前端er来讲,已经不再陌生,因为我们随手都能写出比较超前的代码,为了更好的运行这些代码,我们的代码就会经过各种加工处理,这使得实际运行中的代码跟我们手写的代码存在着很大的gap。时光冉冉,我们终会产生根据报错的行和列找到原文件行和列的需求。sourceMap 就是这解决问题的关键,它就记录着已转换代码
和源代码
的映射关系。一旦我们知道报错的行列号,通过 sourceMap 我们就能找出源代码的对应位置。
在浏览器中怎么使用的?
- 生产一个source map
- 加入一个注释在转换后的文件,它指向source map。注释的语法类似
一个 sourceMap 的基本结构如下
{
"version": 3, // sourcemap 版本号
"file": "main.js", // 转换后的文件名
"sources": ["a.js", "b.js"], // 源文件列表,一个文件可能是合并多个小文件而来
"names": [], // 原变量名和属性名,比如使用 uglify 等压缩工具,会修改变量名
"mappings": ";;AAAA,IAAM", // 映射关系
"sourceContent": ["..."] // 源代码(可能没有)
}
深入 sourceMap
怎么才能深入的了解呢,那就是深入灵魂,基于 sourceMap 的作用,我们可以提取出4个关键信息:转换后文件的行(r)、转换后文件的列(c)、原文件的行(r‘)、原文件的列(c’)。因此 soucemap 文件就必然存储着这种映射关系,看看上面的文件结构中的属性名,没错 mappings
就储存着这些关键信息。
我用tsc
编译了一段代码,来做具体分析:
源代码(index.ts):
class Hello {
constructor() {}
}
编译成ES5之后的代码(index.js):
"use strict";
var Hello = /** @class */ (function () {
function Hello() {
}
return Hello;
}());
//# sourceMappingURL=index.js.map
sourceMap文件(index.js.map):
{
"version": 3,
"file": "index.js",
"sources": [
"index.ts"
],
"names": [],
"mappings": ";AAAA;IACE;IAAe,CAAC;IAClB,YAAC;AAAD,CAAC,AAFD,IAEC"
}
mappings
的值就是一个字符串,它包含了一堆字母,还有;
和,
这两个符号,很是有规律。其中的原理呢,就是字符串通过;
和,
的切割,就能拿到行
、列
的对应关系了:通过 ;
切割出每行的信息,再用 ,
切割行信息得到列信息。也就意味着:通过数;
的个数,就能确定编译后文件的行号r
,通过解析 ,
切割出来的数据,就能得到列号 c
、原文件行 r'
、原文件列 c'
。
在这个实际的例子,我们可以看到第一个;
号前是空的,返观上面的代码,我们能发现 "use strict"
,在原文件中的确没有,因此第一行没有任何映射关系。那么后面的 AAAA
,怎么得到其中的信息呢,这段字符其实是被一种压缩算法压缩了,我们只要找到对应的算法解码就行。这个算法就是 VLQ
了,我们直接用现成的库来解码就行 AAAA => 0, 0, 0, 0
,诶?怎么会有四个数字呢,我来解释一下每一个数字的含义:
第一位: 代表在转换后文件的第几列 (同行相对上一个列的偏移量)
第二位: 代表源文件在
sources
里对应的下标第三位: 代表在源文件的第几行(相对于上一个坐标行的偏移量)
第四位: 代表在源文件的第几列(相对于上一个坐标列的偏移量)
我们翻译过来就是 index.js [1, 0] --- index.ts [0, 0]
,下面直接进行超能力翻译了:
第0行
第1行 AAAA => 0, 0, 0, 0 => index.js [1, 0] --- index.ts [ 0, 0];
第2行 IACE => 4, 0, 1, 2 => index.js [2, 4] --- index.ts [(0 + 1) = 1, ( 0 + 2) = 2];
第3行 IAAe => 4, 0, 0, 15 => index.js [3, 4] --- index.ts [(1 + 0) = 1, ( 2 + 15) = 17],
第3行 CAAC => 1, 0, 0, 1 => index.js [3, (4 + 1) = 5] --- index.ts [(1 + 0) = 1, (17 + 1) = 18];
第4行 IAClB => 4, 0, 1, -18 => index.js [4, 4] --- index.ts [(1 + 1) = 2, (18 - 18) = 0],
第4行 YAAC => 12, 0, 0, 1 => index.js [4, (4 + 12) = 16] --- index.ts [(2 + 0) = 2, ( 0 + 1) = 1];
第5行 AAAD => 0, 0, 0, -1 => index.js [5, 0] --- index.ts [(2 + 0) = 2, ( 1 - 1) = 0],
第5行 CAAC => 1, 0, 0, 1 => index.js [5, (0 + 1) = 1] --- index.ts [(2 + 0) = 2, ( 0 + 1) = 1],
第5行 AAFD => 0, 0, -2, -1 => index.js [5, (1 + 0) = 1] --- index.ts [(2 - 2) = 0, ( 1 - 1) = 0],
第5行 IAEC => 4, 0, 2, 1 => index.js [5, (1 + 4) = 5] --- index.ts [(0 + 2) = 2, ( 0 + 1) = 1]
通过上上面的数字含义的,结合我在上面用括号计算过程中加入的括号,不知道大家有没有看明白。编译后文件的行号,我们直接可以获取到,每行的第一个列数据可以直接使用,但是如果这一行里面有多个列数据,那么就需要从左往右依次计算,公式就是前一个列号+偏移量
(每行独立计算),如果是第一个列数据,那前列号就是0
,反复分析第3行和、第4行和第5行,可以看出端倪。源文件的行号
和列号
需要从第一个计算出来的点开始,按顺序一直加上偏移量(偏移量也有可能是负数,比如第4行第一个列数据)。
我们可以发现,解析列信息并不是想象中那么简单,需要通过一定的算法计算才能获得正确的信息,这也是 sourceMap 迭代到第三版的成果,这里面最大的目的就是为了压缩 sourceMap 的体积,但只要我们了解其生成的原理,sourceMap 也就不神秘了,看到这里,我相信你也可以徒手分析 sourceMap 了。
总结
sourceMap 本质上仅仅只是一种位置映射关系,我们可以根据一个坐标,反推出源文件的坐标,但它并不能在运行环境中建立对应的变量映射关系,比如源代码 const name = 1;
, 被转换成了 var a = 1;
,我们在 debug 的时候,如果直接把鼠标放在源代码中的 name
这个变量上,我们并不会得到变量值的提示。由于某些原因,转换后的代码和源代码,并不是每一行,每一列都是有对应关系(最常见的是 tree shaking
它会删除一些代码),当然我们也会遇上在 debug 模式想在某行加断点却加不上的情景😊。