sourcemap
参考资料:
文件结构
{
"version": 3,
"file": 'react.development.js',
"sources": [
"/Users/wyl/github/debug-react/react/packages/shared/ReactVersion.js",
"/Users/wyl/github/debug-react/react/packages/shared/ReactSymbols.js",
"/Users/wyl/github/debug-react/react/packages/react/src/ReactCurrentDispatcher.js",
...
],
"sourcesContent": [
"/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// TODO: this is special because it gets imported during build.\n//\n// TODO: 18.0.0 has not been released to NPM;\n// It exists as a placeholder so that DevTools can support work tag changes between releases.\n// When we next publish a release, update the matching TODO in backend/renderer.js\n// TODO: This module is used both by the release scripts and to expose a version\n// at runtime. We should instead inject the version number as part of the build\n// process, and use the ReactVersions.js module as the single source of truth.\nexport default '18.2.0';\n",
...
],
"names": [
"REACT_ELEMENT_TYPE",
"Symbol",
"for",
"REACT_PORTAL_TYPE",
"REACT_FRAGMENT_TYPE",
"REACT_STRICT_MODE_TYPE",
...
],
"mappings": ";;;;;;EAOA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACA,qBAAe,QAAf;;ECNA;..."
}
version: 版本file: 转换后的文件名sourceRoot: 源文件目录前缀sources: 转换前的文件列表sourcesContent: ''names: 转换前的所有变量名和属性名mappings: 记录位置信息的字符串
有趣的mappings属性
最初看到这一串字符串是一脸懵逼,甚至觉得连续的分号:";;;;;;EAOA..." 什么鬼,非常不正常。
接下来不妨看看官方的描述 👈,解释了为什么这么用。(可以在这篇翻译的文章后面查看原文的思路)
Base64 VLQ 维持了 sourcemap 的轻量化【意译】
最初,
source map对所有映射关系有了非常详细的输出,最终的结果就是它生成的代码大小达到10倍之多。在版本2中缩减了50%, 然后版本3再次缩减了50%,对于一个133kb的文件最终会得到一个大约300kb的source map。那它们是如何缩减了大小还能维持复杂的映射关系呢?
VLQ(可变长度数量)会和Base64编码一起使用。mappings属性是一个超级大的字符串。在这个字符串中,分号;代表生成文件的行号。每一行都有逗号,表示那一行中的每个segment。每个segment都有1,4,5不等的长度。有一些看起来会更长一点,这些包含了连续的比特位。每个segment都建立在前一个segment的基础上,这有助于减小文件大小。
【注】Base编码对照表 👈
像我上面提到的每个片段可能会有
1,4,5不等的长度。这个图表考虑用一个4位的可变长度加上一个连续比特位(g)。 我们把这个segment拆解开,看来下source map是如何解决原始位置的问题。字符上面的值(A => 0,A => 0,g => 32,B => 1,C => 2)纯粹是用Base64解码的值,需要有一些步骤去得到他们真正的值。每个segment通常解决了下面5件事儿:
- 生成的列
- 出现的原始文件
- 原始行号
- 原始列
- 和 如果存在原始名称
并不是每个
segment都有名字,方法名或者参数,所以segment通常在4到5个长度之间切换。 上面图表那个segment中的g是所谓的连续比特位,这允许在Base64 VLQ解码阶段进一步优化。连续比特位允许你构建一个segment的值去存储一个很大的数字而不用特意有一个空间,这是一种非常聪明节省空间的技术,可以追溯到midi格式。上图的
AAgBC进一步处理之后将返回0,0,32,16,1——32作为连续比特位可以帮助构建下一个16那个值。B用Base64解码之后是1。所以能用到重要的值是0,0,16,1。这就让我们知道在生成的map文件中第1行(行是通过分号计数的)第0列对应的是第0个文件(数组中的第0个文件是foo.js),第16行第1列。为了展示下
segment是如何解码的,我会引用下Mozilla的Source Map JavaScript library👈。你也可以看下WebKit的开发者工具source mapping code👈,它也是用JavaScript写的。为了正确理解我们是如何从
B得到的16,我们需要对位运算符有一个基础的理解,然后看下是如何作用于源码的映射。通过按位与(&)比较数字(32)和VLQ_CONTINUATION_BIT(二进制100000或者32), 前面那个g被标记作为连续比特位。32 & 32 = 32 // or 100000 | | V 100000每一位都出现
1才会返回1。所以如上图所示,33 & 32解码的值也是32,因为它们仅共享32位位置。然后对于每个前面的连续位,都会左移增加5位。在上述情况下,它只移动5次,因此将1(B)左移5。1 << 5 // 32 // Shift the bit by 5 spots ______ | | V V 100001 = 100000 = 32然后将数字(
32)右移一位,这个值就从一个VLQ变过来了。32 >> 1 // 16 //or 100000 | | V 010000 = 16所以我们得出:是如何从
1变成16的。看起来是个复杂的过程,不过一旦这个数字开始变大,就会变得很有意义。
上文大体思路 👆:
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA" ⬅️ 因为规定按分号(;)切分,所以表示打包文件 out.js 的第 1 行
}
Base64 + VLQ是有编码和解码两个过程,文章描述的是解码过程- 从上面的
map文件中举例说明了AAgBC,如果用纯粹的Base64解码得到的值是0 0 32 1 2 - 但这并不是真正的值,需要一些步骤才能得到
- 真正的值是
0 0 16 1。(表示当前行第0列对应第0个原文件的第16行 第1列)
实现
在 JavaScript 中,有两个函数被用来处理解码和编码 base64 字符串:
- atob() 解码
- btoa() 编码
btoa('a') // YQ==
atob('YQ==') // a
