source-map解析原理

source-map的作用在于关联编译后的代码和源码, 可以让我们实现打断点调试和线上错误代码定位, 基本原理就是记录源码和编译后的目标代码的位置, 来实现定位.

source-map的基本格式

  1. 所在位置

当我们执行webpack打包的时候, 会生成很多.map结尾的文件, 这些就是source-map文件

而每个source-map文件都有对应的js文件, 在这些生成js文件的底部, 又有//# sourceMappingURL=xxxxx.map这种注释, 浏览器会根据这个信息, 找到对应的source-map文件

  1. 基本格式

我们点开一个source-map文件, 格式化后, 可以看到其基本格式类似于

arduino 复制代码
{
  version : 3,
  file: "target-code.js", // 目标代码的文件名, 也就是编译后的文件名
  sourceRoot : "", // 源码根目录
  sources: ["foo.js", "bar.js"], // 源代码文件
  names: ["a", "b"], // 源码中的变量名
  mappings: "AAgBC,SAAQ,CAAEA;AAAEA", // 位置映射(重要)
  // 源代码文件的内容, 和sources属性相对应
  sourcesContent: ['const a = 1; console.log(a)', 'const b = 2; console.log(b)']
}

注意, 这里的mappings是核心, 它记录了位置映射, 先来了解下其值的含义:

  • 分号代表行分隔, 例如, 上面代码中的mappings值包含一个分号, 说明这里映射的源码有2行
  • 逗号代表映射的位置, 例如, 示例中映射了源码第1行的3个位置和第2行的1个位置
  • 每个位置有五位, 分别代表
    • 源码列数(行数已经通过分号确定了)
    • 源文件名, 这个本质上一个索引, 通过这个索引可以找到sources中对应的元素,从而确定源文件名
    • 对应源文件行数
    • 对应源文件列数
    • 源码中的变量名, 这里本质上也是一个索引, 通过这个索引可以找到names中对应的属性, 从而确定这个位置对应的源码中的变量

看到这里, 其他的都好理解, 关键是mappings中的值到底是个什么鬼? 其实这里用了VLQ编码, 好了, 再说这个之前, 我们先来思考下,如果要我们自己实现source-map, 该怎么做?

实现source-map思路

初步构思

其实source-map本质就是将源代码和目标代码关联起来, 假设我们有这么一段代码转换

arduino 复制代码
// 这里其实就是简单的位置转换
"feel the force" ⇒ "the force feel"

那如果要设计一个source-map, 我们该如何做, 首先能想到的就是, 位置映射,我把你俩位置全部一一关联起来!

目标代码中的位置 源文件名 源码中的位置 对应字符
行 1, 列 0 source.txt 行 1, 列 5 t
行 1, 列 1 source.txt 行 1, 列 6 h
行 1, 列 2 source.txt 行 1, 列 7 e
行 1, 列 4 source.txt 行 1, 列 9 f
行 1, 列 5 source.txt 行 1, 列 10 o
行 1, 列 6 source.txt 行 1, 列 11 r
行 1, 列 7 source.txt 行 1, 列 12 c
行 1, 列 8 source.txt 行 1, 列 13 e
行 1, 列 10 source.txt 行 1, 列 0 f
行 1, 列 11 source.txt 行 1, 列 1 e
行 1, 列 12 source.txt 行 1, 列 2 e
行 1, 列 13 source.txt 行 1, 列 3 l

打个比方, t这个字符, 在源码中的第1行第5列, 在目标代码的第1行第0列, 所以为了表示t, 我们可以写出如下代码

arduino 复制代码
// 目标行数|目标列数|源文件名|源代码行数|源代码列数
1|0|source.txt|1|5

所以最终, 我们的mappings应该是

makefile 复制代码
mappings:1|0|source.txt|1|5, 1|1|source.txt|1|6, 1|2|source.txt|1|7, 1|4|source.txt|1|9, 1|5|source.txt|1|10, 1|6|source.txt|1|11, 1|7|source.txt|1|12, 1|8|source.txt|1|13, 1|10|source.txt|1|0, 1|11|source.txt|1|1, 1|12|source.txt|1|2, 1|13|source.txt|1|3

如果代码多了, 那source-map体积将增大很多

去掉行号

根据前面的介绍, 我们知道, 行号可以通过分号来表示, 所以我们的第一位, 可以去掉了

makefile 复制代码
mappings:0|source.txt|1|5, 1|source.txt|1|6, 2|source.txt|1|7, 4|source.txt|1|9, 5|source.txt|1|10, 6|source.txt|1|11, 7|source.txt|1|12, 8|source.txt|1|13, 10|source.txt|1|0, 11|source.txt|1|1, 12|source.txt|1|2, 13|source.txt|1|3

可视化字符提取

我们可以发现, 为了表达'the'这个单词, 我门用了三组数据来表达

0|source.txt|1|5, 1|source.txt|1|6, 2|source.txt|1|7

但是现实中, 一个标识符(包括变量名/函数名等等), 其实里面的字符都是连续的, 所以我们完全可以只记录这个'the'开始的位置, 然后, 将这个单词完整地记录在另一个数组中, 而这个数组, 就是我们之前介绍的names属性中的内容

arduino 复制代码
names: ['the', 'force', 'feel']

而此时的'the'就可以表示为

0|source.txt|1|5|0

所以此时完整的表示就是

0|source.txt|1|5|0, 4|source.txt|1|9|1, 10|source.txt|1|0|2

整合源文件名

我们把变量名统一另外存在了一个属性中, 同样的道理, 一直重复的文件名, 也应如此, 而原本写文件名的位置只留下一个索引即可, 现在我们的映射有

vbnet 复制代码
file: ['source.txt']
names: ['the', 'force', 'feel']
mappings: 0|0|1|5|0, 4|0|1|9|1, 10|0|1|0|2

相对位置转换

我们的源码列数, 都是使用绝对位数, 也就是第一个单词定位到源码第5列之后, 后一个单词还是从头开始, 算到第9列, 如果代码量少还好, 如果很多的话, 就会导致列数非常之大, 所以, 我们可以采用相对位置来对源码列数进行定位:

例如,

  • 源码列中:

第一个单词列数为5, 那就是5

第二个为9 ,那么可以理解为第二个相对于第一个+4, 所以可以记为4

第三个为0, 可以定义为第二个(绝对位置为9)的位置-9, 所以可以记为-9

  • 目标代码列中:

第一个单词列数为0, 那就是0, 保持不变

第二个为4 ,那么可以理解为第二个相对于第一个+4, 所以可以记为4, 保持不变

第三个为10, 可以定义为第二个(绝对位置为4)的位置+6, 所以可以记为6

  • 变量数组索引

第一个单词变量索引为0, 保持不变

第二个单词变量索引为1, 相对第一个来说+1, 所以仍记为1

第三个单词变量索引为2, 相对第二个来说+1, 所以记为1

现在我们的映射有

vbnet 复制代码
file: ['source.txt']
names: ['the', 'force', 'feel']
mappings: 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1

VLQ编码

我们的source-map看上去已经很简洁了, 但是, 如果代码多了, 其中的数字仍有可能很大, 一旦数字大了, 就有可能依然占用很大的空间

在介绍VLQ之前, 我们来看下一个简单的例子

  1. 如果我们要记录连续几个数字
arduino 复制代码
// 我们可以用|来作为数字之间的分隔
1|2|3|4
// 如果说这些数字都是个位数, 则可以去掉|, 这样可以节省空间
1234
  1. 但是我们要记录的不可能永远都是个位数, 例如:
arduino 复制代码
// 如果中间有超过2位数的数字, 我们还是要|来分隔,否则完全分不清到底有几组数字
1|23|456|7

而使用|, 毕竟是数字符号之外的符号, 不利于体积的减小, 我们要有一种新的记录 方式来记录这段数字.

VLQ以二进制方式呈现

先看这段数字, 和上面是同一组数据, 都是分了4组

1234567

我们来捋一捋规则

  • 1底下没有横线, 说明1不连续, 到这里, 第一组就终止了.
  • 2底下有横线, 说明这是个连续的数字, 继续往后看, 3底下没有了, 那么23就是一组
  • 45底下都有横线, 说明还在连续, 6底下没有了, 说名连续中止, 456就是一组
  • 7底下没有横线, 说明最后一组只有7.

在此, 以6bit为一组, 我们姑且称之为: '比特组', 表示一组数字, 如果一个比特组不够, 则再加一个比特组(也是6bit)

B5 B4 B3 B2 B1 B0
是否连续 数字的值 正负

可以看出, 这6位中:

  • 第一位的含义是本数字是否连续, 也就是前面说的, 底下是否有横线(1-连续, 0-不连续)
  • 中间四位表示数字的值的二进制表达形式
  • 最后一位表示这个数字的正负(1-负数, 0-正数), 注意, 只有第一个比特组需要记录正负
Binary group Meaning
000000 0
000001 -0
000010 1
000011 -1
000100 2
000101 -2
... ...
011110 15
011111 -15
100000 未结束的0
100001 未结束的-0
100010 未结束的1
100011 未结束的-1
... ...
111110 未结束的15
111111 未结束的-15

只有第一个比特组才需要记录正负, 后续的数字, 最后一位参与数值表达:

B5 B4 B3 B2 B1 B0
是否连续 数值(二进制形式, 包括最后一位B0)

用VLQ二进制方式编码

我们现在可以按照这套规则, 对上面案例中的1234567, 进行二进制编码

  1. 首先是1, 它是正数, 二进制形式用4位足以表达(0001), 所以一个比特组就够了, 这个比特组肯定就是不连续的了(后面没有新的比特组了):
B5(C) B4 B3 B2 B1 B0(S)
0(不连续) 0 0 0 1 0(正数)
  1. 再是23, 它是正数, 二进制形式10111, 第一个比特组只有4位来表示数值, 不够, 所以这里需要2个比特组:
B5(C) B4 B3 B2 B1 B0(S) B5(C) B4 B3 B2 B1 B0
1 0 1 1 1 0 0 0 0 0 0 1

第一个比特组中, 首位和末位分别表示'连续'和'正数'的含义, 中间四位是23的二进制形式 (10111)的后四位, 即0111;

第二个比特组中, 首位表示'不连续', 后续五位全部都可用来表示数值: 而我们只剩下一个1需要表示, 所以就是00001;

  1. 然后是456, 正数, 二进制形式111001000, 高达9位, 一个比特组也放不下:
B5(C) B4 B3 B2 B1 B0(S) B5(C) B4 B3 B2 B1 B0
1 1 0 0 0 0 0 1 1 1 0 0
  1. 最后是7, 正数, 二进制形式111, 一个bit组就够了
B5(C) B4 B3 B2 B1 B0(S)
0 0 1 1 1 0

所以, 综合以上所有的二进制数据, 我们可以得到:

000010 101110 000001 110000 011100 001110

这样, 就是按照我们所述的规则, 表示出了: 1234567. 然后我们再根据__base64 编码表__的出最终的结果

  1. 先将我们得到的二进制数据转为十进制
arduino 复制代码
000010 101110 000001 110000 011100 001110
// 转换
2 46 1 48 28 14
// 对照base64表得到
CuBwco

现在回到我们之前的source-map中的mappings

makefile 复制代码
mappings: 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1

按照之前介绍的方法, 我们可以计算得出

arduino 复制代码
// 第一组
0|0|1|5|0
// 转为VLQ二进制形式
000000 000000 000010 001010 000000
// 转十进制
0 0 2 10 0
// 对照base64表
AACKA

// 第二组
4|0|1|4|1
// 转为VLQ二进制形式
001000 000000 000010 001000 000010
// 转十进制
8 0 2 8 2
// 对照base64表
IACIC

// 第二组
6|0|1|-9|1
// 转为VLQ二进制形式
001100 000000 000010 010011 000010
// 转十进制
12 0 2 19 2
// 对照base64表
MACTC

综合:

我们的source-map的最终代码就是

arduino 复制代码
{
  "version": 3,
	"file": ['source.txt'],
  "sources": ["source-code.txt"],
  "names": ['the', 'force', 'feel'],
  "mappings": "AACKA, IACIC, MACTC"
}
相关推荐
吃杠碰小鸡34 分钟前
lodash常用函数
前端·javascript
emoji11111144 分钟前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
麦兜*1 小时前
轮播图带详情插件、uniApp插件
前端·javascript·uni-app·vue
陈大爷(有低保)1 小时前
uniapp小案例---趣味打字坤
前端·javascript·vue.js
m0_748236581 小时前
《Web 应用项目开发:从构思到上线的全过程》
服务器·前端·数据库