sourceMap到底是个啥玩意?
一、前言
sourceMap是一个由来已久的名词,自从2013年jQuery开始支持以来,开始逐渐广泛的被应用于各种打包工具上,最具标志性的便是前端er必须具备的webpack。
webpack是一个模块打包工具,在使用的过程中有许多配置项可以选择,例如:source-map、cheap-module-source-map、cheap-source-map、eval-source-map等等(webpack的sourceMap是什么?),所以我觉得有必要了解一下sourceMap到底是做什么的,到底是怎么来的。
二、为什么选择sourceMap?
这里抛出三个问题,大家自己带着问题去阅读本文,寻找答案。
- sourceMap为什么会出现?能做什么?
- sourceMap为开发者带来了什么便利?
- sourceMap较其他同类工具有什么区别?优点?
三、什么是sourceMap?
简单来说,sourceMap是一个.map文件,里面储存着位置信息,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码,为开发者带来巨大的便利。
现如今各大框架横行,JavaScript脚本正变得越来越复杂。大部分源码(尤其是各种函数库和框架)都要经过转换,才能投入生产环境。
然而我们熟知的转换过程,也就是发布成线上代码,是需要经过几个步骤的:
- 压缩,减小体积
- 多个文件合并,减少HTTP请求数
- 通过编译或者转译,将其他语言编译成JavaScript
通常,JavaScript的解释器会告诉你,第几行第几列代码出错。但是,这对于转换后的代码毫无用处。举例来说,原本一个代码量及其庞大的库或者是框架,就好比说jQuery 1.9,压缩后只有3行,每行3万个字符,所有内部变量都改了名字。你看着报错信息,感到毫无头绪,根本不知道它所对应的原始位置。
这就是sourceMap想要解决的问题。
sourceMap的具体内容
拿vue举个例子,npm run build 之后的dist文件夹里面,存在着这么一些文件:
让我们依次打开文件看看内容:
我们发现编译之后的代码文件里面,有这么一行代码,其实它指向的就是我们的map文件:
//# sourceMappingURL=about.747bb252.js.map
我们打开对应的.map文件看看内容:
这么看起来太费劲了,让我们稍微整理一下:
{
//SourceMap的版本,目前为3
"version":3,
//转换前的文件,该项是一个数组,表示可能存在多个文件合并
"sources":["webpack:///./src/views/About.vue?da29","webpack:///./src/views/About.vue"],
//转换前的所有变量名和属性名
"names":["render","_vm","this","_h","$createElement","_self","_c","_m","staticRenderFns","staticClass","_v","script","component"],
//记录位置信息的字符串
"mappings":"8GAAA,IAAIA,EAAS,WAAa,IAAIC,EAAIC,KAASC,EAAGF,EAAIG,eAAsBH,EAAII,MAAMC,GAAO,OAAOL,EAAIM,GAAG,IACnGC,EAAkB,CAAC,WAAa,IAAIP,EAAIC,KAASC,EAAGF,EAAIG,eAAmBE,EAAGL,EAAII,MAAMC,IAAIH,EAAG,OAAOG,EAAG,MAAM,CAACG,YAAY,SAAS,CAACH,EAAG,KAAK,CAACL,EAAIS,GAAG,+B,YCAtJC,EAAS,GAKTC,EAAY,eACdD,EACAX,EACAQ,GACA,EACA,KACA,KACA,MAIa,aAAAI,E","file":"js/about.747bb252.js",
//转换前的文件内容列表,与sources列表依次对应
"sourcesContent":["var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _vm._m(0)}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"about\"},[_c('h1',[_vm._v(\"This is an about page\")])])}]\n\nexport { render, staticRenderFns }","import { render, staticRenderFns } from \"./About.vue?vue&type=template&id=1ae8a7be&\"\nvar script = {}\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports"],
//转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
"sourceRoot":""
}
没有什么难的对吧?都很好理解,不过让我们来看看有趣的地方。
四、mappings的解析
map文件的mappings属性。这是一个很长的字符串,它分成三层。
第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
第三层是位置转换,以VLQ编码表示,代表该位置对应的转换前的源码位置。
以"AAAAA,BBBBB;CCCCC"为例,就表示,转换后的源码分成两行,第一行有两个位置,第二行有一个位置。
五、mappings位置对应的原理
其实每个位置使用五位,表示五个字段。从左边算起的话,大概是这样:
第一位,表示这个位置在(转换后的代码的)的第几列。
第二位,表示这个位置属于【sources属性】中的哪一个文件。
第三位,表示这个位置属于转换前代码的第几行。
第四位,表示这个位置属于转换前代码的第几列。
第五位,表示这个位置属于【names属性】的哪一个变量
举个例子 :假设现在有a.js,内容为feel the force,处理后为b.js,内容为the force feel
以feel为例,它在输出中的位置是(0,9),a.js是sources的第1个(这里只是举例),输入中的位置是(0,0),feel是names的第1个(这里只是举例)。
那么映射关系为:
0 9 1 0 1
最后将 09101 表示为 Base64 VLQ 即可。
值得说明的几点是:
- 所有的值都是以0作为基数
- 五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位
- 每一位都采用VLQ编码表示,由于VLQ编码是可变长的,所以每一位可以由多个字符构成
- 为什么不保存转换后代码的行号,因为我们输出的文件总是一行,这样输出的行号就可以省略,因为都是0,没必要写出来
- 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述
到这里又有小伙伴要问了,那啥是相对位置啊?让我们看看示意图:
第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the的输出位置为(0,-10),因为the在feel的左边数10下才能到这个位置。
六、VLQ,Base64 VLQ
最后,谈谈如何用VLQ编码和Base64 VLQ表示数值。VLQ是Variable-length quantity
的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式。这种编码最早用于MIDI文件,后来被多种格式采用。它的特点就是可以非常精简地表示很大的数值,用来节省空间。这种编码需要用最高位表示连续性,如果是1,代表这组字节后面的一组字节也属于同一个数;如果是0,表示该数值到这就结束了。
让我们举个例子,方便理解: 如何对数值137进行VLQ编码?
这个规则是怎么来的,俺也不太清楚,具体的大家可以浏览VLQ的相关网站进行查阅,我们这里只说结果:
如图所示,137的VLQ编码形式为10000001 00001001
Base64 VLQ与一般的VLQ不一样,他在转换步骤上又增添了一些比较复杂的做法:
- 一个Base64字符只能表示 6bit(2^6)的数据
- Base64 VLQ需要能够表示负数,于是用最后一位来作为符号标志位
- 由于只能用6位进行存储,而第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了
- 如果这组数是某个数值的VLQ编码的第一组字节,那它的最后一位代表"符号",0为正,1为负;
- 如果不是,这个位没有特殊含义,被算作数值的一部分
我们举个例子看看与VLQ的转换区别: 如何对数值137进行Base64 VLQ编码?
如图所示,137 通过Base64 VLQ表示为yl
这里我们可以看出来,在VLQ中,编码顺序是从高位到低位,而在Base64 VLQ中,编码顺序是从低位到高位,因为它比VLQ多了一个倒序排序的操作。
七、总结
现在我们可以回答文章开头的几个问题了:
- sourceMap为什么会出现?能做什么?
答:sourceMap的出现就是因为现如今的框架和库很多,打包之后才能被浏览器识别渲染,这无疑对我们开发者来讲是不友好的。sourceMap能做的就是快速定位问题存在的位置。 - sourceMap为开发者带来了什么便利?
答:快速定位bug,使项目投入生产环境之后,如果出现bug的时候,可以快速定位到bug的位置,从而快速解决问题,提高开发效率。 - sourceMap较其他同类工具有什么区别?优点?
答:暂时没有其他工具可以进行相互对比。sourceMap的优点就是不会很难理解,上手门槛不高,且能为开发者们带来便利,提高开发效率。