定位|万字长文(关于sourcemap,这篇文章就够了)

王志远,微医前端技术部医疗支撑组
前言
而今,只要是工程化的项目,大多离不开 sourcemap 的身影,一言蔽之:构建处理前的代码和处理后的代码之间的桥梁。但却很少有同学真的去深入了解它的运作原理,真问起来也就停留在“啊,有个.map 文件,可以通过它定位到源码信息”,来,我们去瞅瞅,源码是一句简单的`console.log('好好学习,天天向上'`)的`.map`文件

如果我告诉你,位置信息就在mapping对应的这堆字母里
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
sourcemap成为了房间里的大象,一旦出现诸如“无法映射到源文件”“只能映射到 loader 处理后的文件”等问题,多数人是毫无头绪的;而就像 TJ 大神说的: "不要直接 copy 解决方案,要理解后自己去实现";
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
闲言少叙,书归正传(不好意思,最近爱看评书),通过本文你将收获什么呢?
本文目标
  • sourcemap配置项给你安排的明明白白,顺带送上生产环境、开发环境最佳实践
  • sourcemap定位原理给你安排的明明白白,Base64-VQL是怎么做到生成mapping记录源码和处理后代码间的映射
  • 感恩大奉送,编码给你安排的明明白白,base64 编码、VLQ 编码、base64-vlq 编码的三世孽缘
读至此处,您还要跑?
冰冰动图大合集云盘,en,是不可能给的,就给一张你们瞅瞅好了
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
sourcemap:devTools 配置项二三事 对于sourcemap而言,我们最常见的,莫过于在 webpack 的配置项devTools中进行使用,而有多少种供我们选择的配置呢?
也不多,二十种的样子,好了,官网链接在此,大家去背吧,背完记得喊一声,本文完。
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
抱歉我皮了,所谓变中取定,这么多种配置项其实只是五个关键字 eval、source-map、cheap、module 和 inline 的组合罢了,请牢记这张表,破阵心法,忽悠时方可娓娓道来。

关键字 含义
source-map 产生.map 文件
eval 使用 eval 包裹模块代码
cheap 不包含列信息(关于列信息的解释下面会有详细介绍)也不包含 loader 的 sourcemap
module 包含 loader 的 sourcemap(比如 jsx to js ,babel 的 sourcemap),否则无法定义源文件
inline 将.map 作为 DataURI 嵌入,不单独生成.map 文件

怎么理解呢?实战见真知。
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
举例详解 文件源码如下
let a = 1,b; b = a;

source-map 处理后输出结果
//# sourceMappingURL=bundle.js.map


关键字 特点
source-map 定位信息最全,但也.map 文件最大,效率最低

eval 处理后输出结果
eval("var a = 1,\nb; \nb = a; \n\n//\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0\n\n//# sourceURL=webpack:///./src/index.js?");


关键字 特点 解决问题
eval 用``eval 包裹源代码进行执行 利用字符串可缓存从而提效

解决问题:原作者解释whyeval,关键在于下面两句话
devtool: "source-map" cannot cache SourceMaps for modules and need to regenerate complete SourceMap for the chunk. It's something for production.
devtool: "eval-source-map" is really as good as devtool: "source-map", but can cache SourceMaps for modules. It's much faster for rebuilds.
翻译来说划重点:加 eval 和不加是一样的????,但加了eval后可以缓存,于是更????。
Inline-source-map处理后输出结果
//# sourceMappingURL=data: ...(base64 字符串)


关键字 特点 解决问题
inline 将 map 作为 DataURI 嵌入,不单独生成.map 文件 减少文件数

cheap-source-map处理后输出结果
//# sourceMappingURL=bundle.js.map


关键字 特点 解决问题 存在的问题
cheap 错误信息只会定义到行,而不会定义到列 精准度降低换取文件内容的缩小

对于cheap-source-map而言,只会定义到出错的这一行
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
而对于source-map而言,则会精准到列
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
存在的问题
  1. 错误信息只会定义到行,而不会定义到列
  2. 对于经由 babel 之类工具转义的代码,只能定位到转换后的代码
这就引出了我们最后的一个关键字
cheap-module-source-map处理后输出结果
//# sourceMappingURL=bundle.js.map


关键字 特点 解决问题
module 会保留 loader 处理前后的文件信息映射 解决对于使用``cheap 配置项导致无法定位到 loader 处理前的源代码问题

测试代码
# sum let sum =(a, b) => { return a + b } debugger export default sum; # index.js import sum from './sum'; console.log(sum);

对于cheap-source-map而言,此时页面 debugger 展示源码是 es5 的代码,因为已经被 babal 转义了
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
而对于source-map而言,则会精准到原始代码
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
配置项关键字小结
至此,我们`source-map`的五个关键词的学习也就告一段落了,而最开始提到官网给出的二十几种配置无非是选词组合而已,再附送下一些常见配置项的关键参数对比吧。

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
配置项最佳实践 开发环境
  • 我们在开发环境对 sourceMap 的要求是:快(eval),信息全(module),
  • 且由于此时代码未压缩,我们并不那么在意代码列信息(cheap),
所以开发环境比较推荐配置:devtool: cheap-module-eval-source-map
生产环境
  • 一般情况下,我们并不希望任何人都可以在浏览器直接看到我们未编译的源码,
  • 所以我们不应该直接提供 sourceMap 给浏览器。但我们又需要 sourceMap 来定位我们的错误信息,
  • 一方面 webpack 会生成 sourcemap 文件以提供给错误收集工具比如 sentry,另一方面又不会为 bundle 添加引用注释,以避免浏览器使用。
这时我们可以设置devtool: hidden-source-map
至此,关于sourcemap在 webpack 中的应用层面我们就算是了解个七七八八了。但其实,这只是一个开头小菜
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
本文最大目标来啦:sourcemap 到底怎么做到源文件和处理后文件映射的?
输出内容分析:map 文件详解 要分析实现,还是得先从现象下手,假定源文件script.js内容为
let a=1; let b=2; let c=3;

其输出内容为 script-min.js
var a=1,b=2,c=3;

script-min.js.map
{"version":3,"file":"script-min.js","lineCount":1,"mappings":"AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE; ","sources":["script.js"],"names":["a","b","c"]}

文件字段具体含义分析
字段 含义
version Source map 的版本,目前为 3
file 转换后的文件名
sourceRoot 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
sources 转换前的文件,该项是一个数组,表示可能存在多个文件合并
names 转换前的所有变量名和属性名
mappings 记录位置信息的字符串

可以看到,既然我们要定位,自然最关心的是具有【记录位置信息】功能的 mapping 属性,接下来详细讲解如何分析mapping
mapping 属性值含义
分析角度 含义
行对应 以分号(; )表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
位置对应 以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
分词信息 以 VLQ 编码表示,代表记录该位置对应的转换前的源码位置、原来属于那个文件等信息。

  • 【行对应】很好理解,即一个分号为一行,因为压缩后基本上都是一行了,所以这个没啥有用信息;
  • 【位置对应】可以理解为分词,每个逗号对应转换后源码的一个位置;
  • 【分词信息】是关键,如AAAA代表该位置转换前的源码位置,以VLQ编码表示;
其中【分词信息】每组最多五位(如果不是变量,只会有四位),分别是:
  • 第一位,表示这个位置在【转换后代码】的第几列。
  • 第二位,表示这个位置属于【sources 属性】中的哪一个文件。
  • 第三位,表示这个位置属于【转换前代码】的第几行。
  • 第四位,表示这个位置属于【转换前代码】的第几列。
  • 第五位,表示这个位置属于【names 属性】的哪一个变量。
到此,我们也算是知道 map 文件到底是怎么组成的了。
小思考
Q:但为什么这么设定呢?理解绝对比死记更有效,其他都好理解,但谈到分词信息中的位置对应,我们下意识应该都会想到坐标,记录组成元素在编译后文件和源文件的坐标,就形成了映射;但我们看到的`mapping`却是字符串,为什么?A:因为体积,如果直接坐标记录信息,至少存在两点空间损耗:编译后文件的纵坐标大的惊人;因为坐标信息是数字,如果采用数组存储,将有大量存储空间浪费。注意上面的`version`字段,象征着版本,而对于现在的默认也是最新版`Source Map Revision 3.0`(V3)而言,通过使用 Base64 VLQ 编码,大大缩小了.map 文件的体积,而这也是本文最有价值的思考点:`Base64 VLQ`是啥?为什么能做到缩减体积。

此处附送base64vlq 在线转换地址,将上面的mappings对应的字符串输入,将会得到对应的数字信息,如AAAA对应的是0000,这两者之间的映射规则就是base64vlq编码。
整理目标 到此,我们整理下接下来要做的事情,抬头看天,低头走路。
我们希望解决坐标信息占用空间过大的问题,主要在于两点
  • 编译后文件列号过大问题:因为会编译成一行,可以想象靠后的元素纵坐标是很大的
  • 数据结构占据空间问题:数组自然比字符串更耗费空间
随着对这两个问题的思考,我们将会彻底理解,为啥我们用于记录位置信息的mapping会是这个鬼样子
AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE

打起精神,继续学!冰冰续命
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
相对位置解决列号过大问题 对于第一点输出后的位置元素的列号特别大的问题,可以采用相对位置的方案进行解决,具体规则如下
  • 第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少
举例而言,假设 a.js 内容为feel the force,处理后输出the force feel ,其 names 为:['feel','the','force'], source: ['a.js']
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
则其按照相对位置输出的关系如下
| 字符组合 | 位置类别 | 输出位置 | 输入位置 | 映射(输出 x |属于文件在 source 的索引 | 输入 x | 输入 y | 变量在 names 的索引) | | --- | --- | --- | --- | --- | | feel | 绝对 | 10, 0 | 0, 0 | 10 | 0 | 0 | 0 | 0 | | the | 相对 | -10, 0 | 5, 0 | -10 | 0 | 5 | 0 | 1 | | force | 相对 | 4, 0 | 4, 0 | 4 | 0 | 4 | 0 | 2 |
其中【位置类别】的相对代表相对上一个元素坐标的偏移,比如`the`的输出位置相对于`feel`就是 x 轴左移 10,y 轴不变,所以其输出位置为(-10, 0);其输入位置是 x 轴右移 5,y 轴不变,所以其输入位置为(5 ,0),`force`对比`the`类推即可。有心的小伙伴会发现【输出 y 坐标】在映射中没有记录,其原因也很简单,因为处理后的输出代码都是一行的,固定为 0,所以也就没必要记录了现在,我们就来到了第二点了,如何压缩`mapping`?涉及到压缩体积,便逃不掉编码

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
就像上面说的,sourcemap 通过`Base64 VLQ`编码进行了缩小.map 文件的体积的处理。那就开始琢磨下关键而神奇的`Base64 VLQ`吧

base64VLQ 解决数据结构占据空间问题
以坐标信息`[0,0,0,0], [4,0,0,4,0]`为例首先,我们得明确,对于源、目标文件的元素坐标映射关系,数组是不可能用数组的,这辈子是不可能用数组的,用字符串不香吗?(原因就不解释了)

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
既然是字符串,原例就变成了`0,0,0,0 | 4,0,0,4,0` ,有没有感觉这个`,`有点不顺眼?本来它们就都是描述同一个映射关系,干嘛还浪费这空间,想想我们编译后的那一大串,如果保留这个分隔符,别扭的很,那如何去掉呢?主角`Base64-VLQ`登场。Base64-VLQ 编码见名知意,其实就是 VLQ 编码方式和 base64 编码的“一套组合拳”,它能去除分隔符主要在于 VLQ 编码方式【变长】的特性,关键点就一句:用二进制表示,进行分组后每组最高位表示连续性,如果是 1,代表这组字节后面的一组字节也属于同一个数;如果是 0,表示该数值到这就结束了。不明白?十脸懵逼?没关系,咱一步步来。柿子挑软的捏,要了解`Base64 VLQ`,咱就先查漏补缺下最熟悉的陌生人:`base64`编码。

ezgif.com-optimize.gif BASE64 编码 我们开发同学最初了解到base64大概是在小icon图标的处理上,当时了解到的是可以将图片的二进制转为文本,从而减少 http 请求,但只适用于小图标等体积小的内容,因为使用base64编码处理过会导致被处理对象体积增加 33%;那么base64到底是什么?它出现就是为了处理小图标吗?了解事物的经典三问
是什么? 在 MDN 中的定义:
是一组相似的二进制到文本(binary-to-text)的编码规则,使得二进制数据在解释成 radix-64 的表现形式后能够用 ASCII 字符串的格式表示出来。
为什么出现? 回想一下,有没有遭遇过用记事本打开exejpgpdf这些文件时,看到一大堆乱码?很简单,在 ASCII 码中规定,031、127 这 33 个字符属于控制字符,32126 这 95 个字符属于可打印字符(来源于Unicode 官网),也就是说网络传输、文本处理只能使用这 95 个字符,不在这个范围内的字符无法使用。那么该怎么才能传输其他字符呢?这就就需要一个二进制到字符串的转换方法。
怎么做到的? 编码本身并不复杂,对使用者而言按图索骥而已,关键是它规则为什么这么设定,以及修补规则出现的原因。
既然 ASCII 码表中存在不可打印字符,那我们就定义一个新码表,其范围固定在可打印字符内。(这就意味着要多个新码表字符表示一个 ASCII 码表字符,原因很简单,你要用苹果、梨表示所有水果,那只好定义两个苹果是西瓜、两个梨是番茄、一个苹果一个梨是。。。排列组合)
新码表字符的组成单元占几个字节? 我们知道基础 ASCII 码,使用 7 位二进制数表示组成单元,新码表的表示范围小于 ASCII 码表(因为要确保新码表中都是可打印字符),这也就意味着,新码表使用的二进制数必须少于七位,而二进制数越少代表其能表示的字符越少,那就需要更多个二进制数来表示一个字符,而这个位数应该是越多越好的,因为这样我们所有的组成元素(新码表)就多了,于是定 6 位吧。2^6 是 64,于是新码表叫做 base64(这块纯属于个人理解,仅供推理记忆,如有错误请不吝赐教)
如何 ASCII 码进行 Base64 编解码 ASCII 码字符占 8 位二进制,而 Base64 占 6 位,取最小公倍数即为 24,即可以用 4 个 base64 字符去表示 3 个 ASCII 码字符。遂有如下转换规定
  1. ASCII 码字符串根据 ASCII 码对照表转换为二进制数值;
  2. 把二进制数值按每 6 位进行划分;
    1. 假设字符数是 3 的倍数,比如三个 ASCII 码字符,就可以三个为一组,用四个 base64 字符来表示(3 * 8 == 4 * 6,enen,应该好理解的)
    2. 如果待编码字符串的长度不是 3 的倍数,则用 0 补位. 如果有连续 6 位都是 0 的话, 就用=来表示。
  3. 然后 6 位二进制转化为十进制根据 Base64 对照表找到编码字符.
对照表 定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
扩展 在JavaScript中,原生提供了 base64 和 ASCII 码之间的转换 API

函数名 功能 参数
atob base64 字符串转 ASCII 码字符串 base64 字符串
btoa ASCII 码字符串转 base64 字符串 ASCII 码字符串

举例而言 以 ASCII 的 A 字符为例,A 转为二进制如上010000001,不足三位,所以补 0,从而补齐 24 位
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
再 6 位为一组,对照Base64 编码表,全为 0 的话用=号代替
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
所以,得出结果,A 字符对应的 Base64 编码是QQ==
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
至此,我们就算是对 base64 这位“最熟悉的陌生人”至少能答出个来龙去脉了
扩展-修补规则:URL 安全的 Base64 编码 修补规则出现背景
Base64 编码可用于在[HTTP](http://zh.wikipedia.org/wiki/HTTP)环境下传递较长的标识信息,然而,标准的 Base64 并不适合直接放在 URL 里传输,因为 URL 编码器会把标准 Base64 中的「/」和「+」字符变为形如「%XX」的形式,而这些「%」号在存入数据库时还需要再进行转换,因为[ANSI](http://zh.wikipedia.org/wiki/ANSI) [SQL](http://zh.wikipedia.org/wiki/SQL)中已将「%」号用作通配符。咱证明下权威性,来看看`rfc`中的定义

For base 64, the non-alphanumeric characters (inparticular, "/") may be problematic in file names and URLs.
翻译过来即:

对于 base 64 编码, 非字母表中的字符 (特别是 "/") 可能会在文件名和 URL 中出现问题

修补规则
为解决此问题,可采用一种**用于 URL 的改进 Base64**编码,具体也很简单,加密时先执行三条规则再转`base64`即可(解密反之)

  • 不在末尾填充=
  • +*替换
  • /-替换
扩展-实现:在JavaScript中如何实现base64url的编码解码? 编码
function urlsafe_b64encode(str) { base64Str = base64_encode(str); base64UrlStr = base64Str.replaceaLL('+','*').replaceaLL('/','-').replaceaLL('=',''); return base64UrlStr; }

解码
function urlsafe_b64decode(base64UrlStr) { data = https://www.it610.com/article/base64Str.replaceaLL('*','+').replaceaLL('-','/'); let mod4 = strlen(data) % 4; if (mod4) { data = https://www.it610.com/article/data.substr('====', mod4); } return base64_decode(data); }

有兴趣的小伙伴可以使用处理 base64url 的 npm 包地址自行去验证下哦
# npm i base64url const base64url = require('base64url'); console.log('字符串base64base64URL'); console.log(' A', encode('A'),'',base64url("A")); //AQQ==QQ

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
VLQ 编码 这是一个很陌生的词汇,但却是sourcemap实现的核心工具,还是老样子,了解事物的经典三问
是什么?
VLQ 是 Variable-length quantity 的缩写,变长编码,用以通过任意位二进制精简地表示很大的数值。
为什么出现? 缩减多数字组成的元素所占据的空间(比如按上文所生成的mapping,就是通过|进行区分不同数字的,这个|其实最好可以省掉,因为只是为了阅读者好理解而已)
怎么做到的? 将数字转化为二进制,然后规定通过二进制始末位具有标识数字的起始的特殊含义,从而节省分隔符所占的空间,设定如下:
  • 一个二进制字节有 8 个位,在 VLQ 编码中设定最高位为是否连续的标识,除了最高位,如果不足 7 个位的倍数则高位补 0;
  • 对于大数字而言,需要多个单元进行表示,那么如果得知这几个单元属于同一个数字,编码规则设定最高位表示连续性,如果是 1,代表这组字节后面的一组字节也属于同一个数;如果是 0,表示该数值到这就结束了;这就避免了分隔符,如|,的出现定位|万字长文(关于sourcemap,这篇文章就够了)
    文章图片

举例而言 对于数字 7 和 1200 以及-7,如果用 VLQ 分别该怎么进行表示呢?

数字 二进制
7 111
1200 10010110000

对 7 而言
  • 先判断是不是七的倍数:不是,只有三位,所以前置位补 4 个 0;
  • 只有一个单元,自然最高位是 0(标识该数的结束);
得出的 VLQ 编码就是0000111
对于 1200 而言
  • 先判断是不是七的倍数:不是,有 11 位,所以前置位补 3 个 0,处理对象变为00010010110000
  • 只有两个单元,所以第一个单元最高位是 1(标识下一个单元还是表示该数),第二个单元是 0;
最后得到的结果即1000100100110000
到此,VLQ的编码也算是告一段落了,其实就是一个规定,规定二进制某些位具有特定含义,从而节省空间,反正处理时按规定解码就好了,也不需要人去看,再怎么难理解也是计算机的事儿,懒人改变世界嘛。
扩展一:VLQ 偏移自然数:Git 底层格式 在了解 VLQ 编码的过程中,意外的了解到了很多冰山之下的知识,比如【VLQ 与自然数的相互转换】在 Git 中居然有专门的实现的一个算法:双射计数法(bijective numeration),主要是如何做到一一映射从而避免多字节 VLQ 的冗余现象,这个与本文主题相差有点大,不做过分拓展,推荐一篇文章《深扒 Git 底层格式:VLQ 偏移自然数》
扩展二:在JavaScript中如何实现 VLQ 的编码?
/**如何对数值进行 VLQ 编码 * 1. 将数值改写成二进制形式 10001001 * 2. 七位一组做分组,不足的补 0 * 3. 最后一组开头补 0,其余补 1 * 4. 拼接 得出编码 * @param {*} num*/function encodeForVLQ(num) {let binary = num.toString(2); let padded = binary.padStart(Math.ceil(binary.length / 7) * 7, '0'); let groups = padded.match(/\d{7}/g); groups = groups.map((group,index)=>{let pre = (index==0 && groups.length > 1?'1':'0')return pre + group; }); let vlqCode = groups.join(''); return vlqCode}console.log(encodeForVLQ2(7)); console.log(encodeForVLQ2(1200));

至此,我们就完成了对 VLQ 编码处理的了解,但我们可以发现输出的是二进制数,而如果我们希望获得的是字符串,就需要使用到BASE64 vlq编码处理了
Base-VLQ 编码 见名知意,其实就是 VLQ 编码方式和 base64 编码的结合。不过有几点与 VLQ 的区别也需要注意一下
  • Base64 VLQ 需要能够表示负数,于是用第一个单元的最后一位来作为符号标志位
  • 在 Base64 VLQ 中,因为要和 base64 相对应,所以修改vlq7 位一组的设定,改为 5 位一组,加上设定为最高位的连续位正好六位。
是什么?
对数字进行 VLQ 编码处理后,使用 base64 字符表示 VLQ 编码的结果。
为什么出现? 考虑到 mapping 文件的可阅读和文本软件处理的问题,VLQ 转化后的二进制应该通过可打印字符去表示。
怎么做到的? 大致与 VLQ 编码的处理逻辑相似,都是分组,然后用最高位表示连续,最关键的不同在于base64-vlq需要表示负数,于是用第一个单元的最后一位来作为符号标志位。
  • 在 base64 中,将 VLQ 存储单元是 8 个 btye 的设定改为 6 个,这样就对应上了 base64 的存储格式。
  • 对于正负而言,编码规则设定第一个单元的最后一位用于表示正负数,零正一负;
    • 这里需要注意,为什么说是【第一个单元】,因为一共六个位,去掉一个表示连续,一个表示正负,那能表示的范围是[-15,15],如果数字过大,就会需要多个单元去描述(这也是说其变长的原因)非第一个单元是不需要表示正负的,所以只需要最高位表示是否终止即可。
  • 将 VLQ 每个单元对应的 base64 字符存储下来即可
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
举例而言 对于数字 7 和 1200 以及-7,如果用base64-VLQ分别该怎么进行表示呢?

数字 二进制
7 111
1200 10010110000

首先,对 7 而言
  • 只有一个单元,自然最高位是 0;
  • 正数,所以最后一位是 0;
  • 最后只有三位,所以前置位补 0;
得出的 VLQ 编码就是001110
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
其次,对于 1200 而言 先完成第一个单元
  • 多个单元,所以连续,最高位是 1;
  • 正数,所以最后一位是 0;
  • 超过一个单元,所以对于第一个单元在二进制中从后取出四个数出来填充四位即可
那得到的第一个单元的组成就是100000
再完成第二个单元
  • 还是填不满,所以联系,最高位是 1
  • 非第一个单元,所以不管正负了,取出五个数填充五位即可
得到的第二个单元的组成就是101011
例推下去,最后得到的结果即100000101011000010
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
根据mapping获取信息 知道了 mapping 文件时如何来的,关键在于我们常见的场景是有类似AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE的 mapping 文件,那我们怎么从中获取信息呢?很简单,按照编码方式反着来就好了,具体可见【扩展-解码】的实现。
扩展:在JavaScript中如何实现base64-VLQ的编码解码? 在此实现一个简易版本,其实目前有base64-vql的处理包,处理 vql 的 npm 包地址,其内部实现大多用到的是诸如>>> |之类的位运算,在此就不过分展开了,有机会单独出一篇文章,思路是相似的。
编码
let base64 = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P','Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v','w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']; /**如何对数值进行 bae64-VLQ 编码 * 1. 将数值改写成二进制形式* 2. 五位一组做分组,不足的补 0,并将组倒序排序 * 3. 最后一组开头补 0,其余补 1 * 4. 转 bae64 进制,即通过对应索引在 base64 码表中取值* @param {*} num*/function encode(num) {//1. 改写成二进制形式,如果是负数的话是绝对值转二进制let binary = (Math.abs(num)).toString(2); //2.正数最后边补 0,负数最右边补 1,127 是正数,末位补 0binary = num >= 0 ? binary + '0' : binary + '1'; //3.五位一组做分组,不足的补 0let zero = 5 - (binary.length % 5); if (zero > 0) {binary = binary.padStart(Math.ceil(binary.length / 5) * 5, '0'); }let parts = []; for (let i = 0; i < binary.length; i += 5) {parts.push(binary.slice(i, i + 5)); }//4. 将组倒序排序parts.reverse(); //5. 最后一组开头补 0,其余补 1for (let i = 0; i < parts.length; i++) {if (i === parts.length - 1) {parts[i] = '0' + parts[i]; } else {parts[i] = '1' + parts[i]; }}//6. 转 bae64 进制,即通过对应索引在 base64 码表中取值let chars = []; for (let i = 0; i < parts.length; i++) {chars.push(base64[parseInt(parts[i], 2)]); }return chars.join('')}let result = encode(137); console.log(result);

解码
let base64 = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P','Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v','w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']; function getValue(char) {let index = base64.findIndex(item => item == char); //先找这个字符的索引let str = (index).toString(2); //索引转成 2 进制str = str.padStart(6, '0'); //在前面补 0 补到 6 位//最后一位是符号位,正数最后一位是 0,负数最后一位为 1let sign = str.slice(-1)=='0'?1:-1; //最后一组第一位为 0,其它的第一位为 1str = str.slice(1, -1); return parseInt(str, 2)*sign; }function decode(values) {let parts = values.split(','); //分开每一个位置let positions = []; for(let i=0; i[item[2],item[3],0,item[0],]); console.log('offsets',offsets); let origin = {x:0,y:0}; let target = {x:0,y:0}; let mapping=[]; for(let i=0; i[${target.x},${target.y}]`); }console.log('mapping',mapping);

有兴趣的小伙伴可以使用处理 vql 的 npm 包地址自行去验证下哦
# npm i vlq const vlq = require('vlq'); console.log(vlq.encode( 137 )); // yIconsole.log(vlq.decode( 'yI' )); // 137

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
关于base64-vlq的尾声 到此,base64-VLQ的编码也算是告一段落了,其实就是一个规定,规定二进制某些位具有特定含义,从而节省空间,反正处理时按规定解码就好了,也不需要人去看,再怎么难理解也是计算机的事儿,懒人改变世界嘛。
尾声
很喜欢《看见》,看来很多遍,其中一篇是《真实自有万钧之力》,大意讲的是拍摄时要“去雕饰,去匠气”,真正有价值的东西会自然而然的流露出来;研究 sourceMap 原理的过程中很“突兀”的想到了这句话,很多时候,我们对高大上的名词趋之若鹜,却很少思考一些像`sourcemap`这类实际解决痛点的“硬核”知识点置若罔闻,看到编码就“累觉不爱”,老实讲,要不是因为设定了交稿日期,我起码还得拖个一两周,因为看编码实在是太太太烦了,又不常用,但坚持下来后才发现很多诸如`base64`、`ASCII`之类的知识点串联成线,“通了”,大概很多小伙伴都或多或少有过这种类似顿悟的感觉,其实就算前端出了名的新知识点层出不穷,我们还是可以看到很多通用的、沉在冰山之下的“道”,而这,则需要静下来去思考。

尾声之外
是一个干到凌晨三点的“卷”日子,答应了交稿就必须做到魄力,微医家的小伙伴就是这么言出必行(害,虽然是自己拖拖拉拉到最后一天才发现排版有问题),所以,还不快到碗里来? 微医招聘,地标杭州,团队大、大佬多、好看小姐姐多,速来(大佬们留言,我去捞人)

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片
Refs & Recommend Readings: An Introduction to Source Maps JavaScript 标准参考教程(alpha) 处理 vql 的 npm 包地址 ASCII 字符集详解 MDN:Base64 的编码与解码 阮一峰: Base64 笔记 阮一峰:JavaScript Source Map 详解 unicode 官网 rfc4648.txt 涉及到的工具 处理 vql 的 npm 包地址
base64vlq 在线转换
往期推荐
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片

Vite 太快了,烦死了,是时候该小睡一会了。

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片

如何实现比 setTimeout 快 80 倍的定时器?

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片

万字长文!总结Vue 性能优化方式及原理

定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片

90 行代码的 webpack,你确定不学吗?

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
  2. 欢迎加我微信「huab119」拉你进技术群,长期交流学习...
  3. 关注公众号「前端劝退师」,持续为你推送精选好文,也可以加我为好友,随时聊骚。
定位|万字长文(关于sourcemap,这篇文章就够了)
文章图片

【定位|万字长文(关于sourcemap,这篇文章就够了)】点个在看支持我吧,转发就更好了

    推荐阅读