SourceMap解析
前端发展至今已不再是刀耕火种的年代了,出现了typescript、babel、uglify.js等功能强大的工具。我们手动撰写的代码一般具有可读性,并且可以享受高级语法、类型检查带来的便利,但经过工具链处理并上线的代码一般不具有可读性,且为了兼容低版本浏览器往往降级到低级语法,这些代码在转换过程中发生了变化,使我们并不能马上识别原始代码的组合方式,这提供了一定的源码安全性。虽然带来了这些好处,但最终代码的排错是一个难点,SourceMap作为一种代码索引的工具,已经被广泛应用于这类场景了,它通过保存转换前和转换后代码在行、列上的对应关系,形成类似“映射”的结构,一旦转换的代码出了问题,可以查找到对应原始代码的位置。本文针对webpack SourceMap的生成方法进行了探讨,涉及Base64 VLQ编码的基本知识,配合案例进行讨论,希望能对想了解它的开发者有所帮助。

生成SourceMap
我们先创建一个文件index.js,书写一些ES6的语法,然后配置webpack利用babel转换到低级语法。
// index.js
const foo = 'hello';
const bar = (a, b) => a+b;
然后配置webpack生成SourceMap文件
const path = require('path')
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
exclude: /node_modules/
}
}
]
}
]
},
devtool: 'source-map',
optimization: {
runtimeChunk: {
name: 'manifest'
}
}
}
devtool指定了生成的sourceMap类型,这里我们选择最原始的source-map即可。注意使用optimization.runtimeChunk选项抽离webpack注入的骨架代码,这些代码会干扰我们分析。
运行打包后,在dist目录得到四个文件,分别是main.js, main.js.map, manifest.js, manifest.js.map, 其中main.js是输出的代码文件,而main.js.map是SourceMap文件。
先看main.js, 文件的10-14行就是转化后的代码,可见原始代码中的const和箭头函数语法均被低级语法代替。
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["main"],{
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports) {
var foo = 'hello';
var bar = function bar(a, b) {
return a + b;
};
/***/ })
},[["./index.js","manifest"]]]);
//# sourceMappingURL=main.js.map
再看main.js.map文件, 这是一个JSON格式的文件,其中names字段包含了所有原始代码里的形参和实参,sourcesContent字段是原始代码,mappings字段则是生成的sourceMap。
{
"version": 3,
"sources": ["webpack:///./index.js"],
"names": ["foo", "bar", "a", "b"],
"mappings": ";;;;;;;;;AAAA,IAAMA,GAAG,GAAG,OAAZ;;AACA,IAAMC,GAAG,GAAG,SAANA,GAAM,CAACC,CAAD,EAAIC,CAAJ;AAAA,SAAUD,CAAC,GAACC,CAAZ;AAAA,CAAZ,C",
"file": "main.js",
"sourcesContent": ["const foo = 'hello';\r\nconst bar = (a, b) => a+b;\r\n"],
"sourceRoot": ""
}
SourceMap的格式
SourceMap的格式以;作为行分隔符,以,作为行中条目的分隔符,每个条目包含4-5个编码字符,这5个字符分别表示:
- 该位置在转化后的代码中的列数(相对于上一个条目)
- 文件序号
- 该位置在原始代码中的行数(相对于上一个条目)
- 该位置在原始代码中的列数(相对于上一个条目)
- 可能没有,该位置包含names属性中的哪个变量的声明,对应该属性的index (相对于上一个变量出现的index)
编码分析
这些信息都是数字格式,使用Base64 VLQ进行编码,在二进制位运算基础上操作,具体步骤为:
- 如果数字大于或等于0,左移一位;如果数字小于0,先取绝对值,然后左移一位,接着将末位置为1;
- 取数字最低的5位,并将数字右移5位;
- 如果此时数字为0,使用Base64编码字符序列输出第2步中取到的5位;如果数字不为0,则将第2步中取到的5位前面补1,使用Base64编码字符序列输出字符并循环第2步;
Base64编码序列表:

下面举两个例子具体来看下。
首先看16这个数:
- 16(10000)大于0,按照第1步,左移一位,变成100000;
- 按照第2步,取最低的5位,得到00000,数字剩余1,按照第3步,在00000前方补1得到100000,转化为十进制是32,对应的字符是
g,此时有数字剩余,继续第2步; - 按照第2步,取最低的5位,得到1,数字剩余0,按照第3步,直接输出1对应的字符'B';
经过转化,16对应的Base64 VLQ编码是gB。
再看-2333这个数:
- -2333小于0,按照第1步,先取绝对值得到2333(100100011101),左移一位,然后末位置为1,变成1001000111011;
- 按照第2步,取最低的5位,得到11011,数字剩余10010001,按照第3步,在11011前方补1得到111011,转化为十进制是59,对应的字符是
7,此时有数字剩余,继续第2步; - 按照第2步,取最低的5位,得到10001,数字剩余100,按照第3步,在10001前方补1得到110001,转化为十进制是49,对应的字符是
x,此时有数字剩余,继续第2步; - 按照第2步,取最低的5位,得到100,数字剩余0,按照第3步,100转化为十进制是4,直接输出对应的字符
E;
经过转化,-2333对应的Base64 VLQ编码是7xE。
解码分析
经过上面的格式分析,mappings开头的每个分号都对应着转换后代码中的一行,通过观察转换后的文件我们发现mappings开头有9个分号,代表这9行内容都是webpack自己加进去的,跟我们的源代码没有关系,所以这里就直接忽略他们。
了解了编码方式,其实解码就是编码的反操作,就不赘述具体步骤了。为了帮助解析mappings这堆字符的含义,我们直接引入vlq这个库。
先分析第10行,利用下面的代码解析vlq字符串:
const vlq = require('vlq')
const source = 'AAAA,IAAMA,GAAG,GAAG,OAAZ'
function extract (sourceString) {
const lines = sourceString.split(';')
return lines.map(line => line.split(',').map(vlq.decode))
}
console.log(extract(source))
得到一个数组:
[
[
[ 0, 0, 0, 0 ], // 第10行第0列对应原始代码第1行第0列
[ 4, 0, 0, 6, 0 ], // 第0-3列对应原始代码第0-5列 (var -> const),同时包含names[0], 即foo变量的声明
[ 3, 0, 0, 3 ], // 第4-6列对应原始代码第6-8列 (foo -> foo)
[ 3, 0, 0, 3 ], // 第7-9列对应原始代码第9-11列 ( = -> = )
[ 7, 0, 0, -12 ] // 第10-16列对应源代码从第12列直到下一行开头('hello' -> 'hello';)
]
]
接下来的第11行是一个空行,直接用一个;结束。
再接下去是箭头函数的转换,我们接着看第12行,把AACA,IAAMC,GAAG,GAAG,SAANA,GAAM,CAACC,CAAD,EAAIC,CAAJ进行转化,得到:
[
[
[ 0, 0, 1, 0 ], // 第12行第0列对应原始代码第2行第0列
[ 4, 0, 0, 6, 1 ], // 第0-3列对应原始代码0-5列 (var -> const ),同时包含names[0+1], 即bar变量的声明
[ 3, 0, 0, 3 ], // 第4-6列对应原始代码第6-8列 (bar -> bar)
[ 3, 0, 0, 3 ], // 第7-9列对应原始代码第9-11列 ( = -> = )
[ 9, 0, 0, -6, 0 ], // 第10-18列没对应到内容,原始代码回到第5列 (function -> )
[ 3, 0, 0, 6 ], // 第19-21列对应原始代码第6-11列 (bar -> bar = )
[ 1, 0, 0, 1, 1 ], // 第22列对应原始代码第12列 ( ( -> ( ),同时包含names[1+1], 即形参a的声明
[ 1, 0, 0, -1 ], // 第23列没对应到内容,原始代码回到第11列
[ 2, 0, 0, 4, 1 ], // 第24-25列对应原始代码第1行第12-15列(, -> (a, ),同时包含names[2+1], 即形参b的声明
[ 1, 0, 0, -4 ] // 第26列没对应到内容,原始代码回到第11列
]
]
后面的都是以此类推,就不一一分析了。
以上就是SourceMap编解码的大体流程,github地址在这里,感兴趣的可以自己尝试一下。
cheap-source-map 和 eval-source-map
最后来看看在开发中用得较多的这两种SourceMap,分别以cheap和eval作为前缀。我们先分析cheap,顾名思义,这种SourceMap比较“便宜”一些,由于大多数情况下我们只需要映射源码的行号,而列号和变量信息其实不是必需的,因为一行代码也就那么些字符,出错后找到对应的行进行检查即可。这种方式节省了大量的存储和计算开销,我们把上面的devtool设置成cheap-source-map再编译,看下main.js.map文件:
{
"version": 3,
"file": "main.js",
"sources": ["webpack:///./index.js"],
"sourcesContent": ["var foo = 'hello';\n\nvar bar = function bar(a, b) {\n return a + b;\n};"],
"mappings": ";;;;;;;;;AAAA;AACA;AACA;AACA;AACA;;;;A",
"sourceRoot": ""
}
与source-map不同,这里的sourcesContent字段保存的是经过babel转换后的代码,这意味着它是webpack生成的代码与经babel转化后的代码的映射,而非与原始代码的映射。再看mappings信息,前9行仍然是没法对应,都以一个分号表示,第10行是AAAA,解码后是[0, 0, 0, 0]代表sourcesContent的第1行; 第11-14行都是AACA,解码后是[0, 0, 1, 0]分别代表sourcesContent的第2-5行,这几行都是由原始代码中的箭头函数解析得到的。
再来看看eval-source-map,使用它SourceMap信息始终内联在代码文件中,比如这样:
eval("var foo = 'hello';\n\nvar bar = function bar(a, b) {\n return a + b;\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9pbmRleC5qcz80MWY1Il0sIm5hbWVzIjpbImZvbyIsImJhciIsImEiLCJiIl0sIm1hcHBpbmdzIjoiQUFBQSxJQUFNQSxHQUFHLEdBQUcsT0FBWjs7QUFDQSxJQUFNQyxHQUFHLEdBQUcsU0FBTkEsR0FBTSxDQUFDQyxDQUFELEVBQUlDLENBQUo7QUFBQSxTQUFVRCxDQUFDLEdBQUNDLENBQVo7QUFBQSxDQUFaIiwiZmlsZSI6Ii4vaW5kZXguanMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBmb28gPSAnaGVsbG8nO1xyXG5jb25zdCBiYXIgPSAoYSwgYikgPT4gYStiO1xyXG4iXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///./index.js\n");
看来看去,我们的代码就是前面一小段,后面带着一个很长的尾巴sourceMappingURL,这个是什么东西呢?不妨用base64解码一下:
JSON.parse(atob('eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9pbmRleC5qcz80MWY1Il0sIm5hbWVzIjpbImZvbyIsImJhciIsImEiLCJiIl0sIm1hcHBpbmdzIjoiQUFBQSxJQUFNQSxHQUFHLEdBQUcsT0FBWjs7QUFDQSxJQUFNQyxHQUFHLEdBQUcsU0FBTkEsR0FBTSxDQUFDQyxDQUFELEVBQUlDLENBQUo7QUFBQSxTQUFVRCxDQUFDLEdBQUNDLENBQVo7QUFBQSxDQUFaIiwiZmlsZSI6Ii4vaW5kZXguanMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBmb28gPSAnaGVsbG8nO1xyXG5jb25zdCBiYXIgPSAoYSwgYikgPT4gYStiO1xyXG4iXSwic291cmNlUm9vdCI6IiJ9'))
{
"version": 3,
"sources": ["webpack:///./index.js?41f5"],
"names": ["foo", "bar", "a", "b"],
"mappings": "AAAA,IAAMA,GAAG,GAAG,OAAZ;;AACA,IAAMC,GAAG,GAAG,SAANA,GAAM,CAACC,CAAD,EAAIC,CAAJ;AAAA,SAAUD,CAAC,GAACC,CAAZ;AAAA,CAAZ",
"file": "./index.js.js",
"sourcesContent": ["const foo = 'hello';\r\nconst bar = (a, b) => a+b;\r\n"],
"sourceRoot": ""
}
可见其只不过是把map信息以base64格式存储在代码中,换汤不换药,其实还是那些东西,穿了个马甲而已。
总结
- 本文探索了SourceMap的编解码原理,这种常用的源码映射工具使用了Base64 VLQ编码,引入vlq库可以轻松地进行编解码;
- 对webpack中常用的cheap-source-map和eval-source-map进行了分析,其实跟上者大同小异;
References
[1]. JavaScript Source Map 详解
[2]. Decoding and Encoding Base64 Vlqs in Source Maps
[3]. wiki
SourceMap解析的更多相关文章
- Webfunny知识分享:webpack sourceMap解析源码
前端的业务越来越庞大,导致我们需要引入的js等静态资源文件的体积也越来越大,不得不使用压缩js文件的方式来提高加载的效率. 编译工具的诞生,极大地方便了我们处理js文件的这一过程,但压缩后的js文件极 ...
- 从无到有<前端异常监控系统>落地
导火索 有一天一个测试同事的一个移动端页面白屏了,看样子是页面哪里报错了. 我自己打开页面并没有报错,最后发现报错只存在于他的手机,移动端项目又是在微信环境下,调试起来会比较麻烦,最后用他手机调试才 ...
- jQuery2.x源码解析(构建篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...
- happypack 原理解析
说起 happypack 可能很多同学还比较陌生,其实 happypack 是 webpack 的一个插件,目的是通过多进程模型,来加速代码构建,目前我们的线上服务器已经上线这个插件功能,并做了一定适 ...
- 利用sourcemap来调试sass
最近项目用上了sass,作为css的预处理器,它可以让我们用程序化的思维书写样式,极大的简化了css的开发,实在是前端居家旅行必备的利器. 我们都知道,在项目中,样式的频繁调试是不可避免的,用上sas ...
- Spring初始化 Map 和 解析Json value
单独定义Map数据结构的bean: <bean id= "expToLevelMap" class="org.springframework.beans.facto ...
- gulp源码解析(二)—— vinyl-fs
在上一篇文章我们对 Stream 的特性及其接口进行了介绍,gulp 之所以在性能上好于 grunt,主要是因为有了 Stream 助力来做数据的传输和处理. 那么我们不难猜想出,在 gulp 的任务 ...
- vue-cli的webpack模版项目配置解析
上一篇文章已经分析了build/dev-server.js,里面使用到了其他config文件. 那么我们这篇文章,按着dev-server.js的使用顺序,来分析下其他文件. 首选,调用check-v ...
- vue-cli的webpack模版,相关配置文件dev-server.js与webpack.config.js配置解析
1.下载vue-cli npm install vue-cli -g vue-cli的使用与详细介绍,可以到github上获取https://github.com/vuejs/vue-cli 2.安装 ...
- 简单解析nestJS目录
使用Nest CLI设置新项目非常简单 .只需确保 安装了npm,然后在OS终端中使用以下命令: $ npm i -g @nestjs/cli $ nest new project-name $ cd ...
随机推荐
- vue3文档学习
1.vue的核心功能 1.声明式渲染2.响应性: vue会自动跟踪JavaScript状态并在其发生变化时响应式的更新DOM. 2.渐进式框架 根据不同的需求场景,可以用不同的方式使用vue: 1.无 ...
- php对接钉钉机器人报警接口
<?php function request_by_curl($remote_server, $post_string) { $ch = curl_init(); curl_setopt($ch ...
- 我与CSP的一点小事
今天是20220311 见了YG老师,感觉被打了鸡血.然后想当初研一的时候,有了这篇LeetCode习题集 现在突然有了一点刺激之后,决定记录下这次的CSP经历,说无论怎么样是the shit,这次就 ...
- JS中函数的length以及arguments的length如何得到?
function a(x,y){} a.length // 2 function b(x,y=2,z){} b.length // 1 function c(x,...args){} c.length ...
- clickhouse杂记
1,clickhouse show tables SHOW [TEMPORARY] TABLES [FROM ] [LIKE ''] [LIMIT ] [INTO OUTFILE ] [FORMAT ...
- 关于watch
watch和computed是姊妹篇,前言同上. 为啥姊妹呢,因为computed初始化完了就是初始化watch: function initWatch (vm, watch) { for (var ...
- gateway 网关接口防篡改验签
gateway 网关接口防篡改验签 背景:为了尽可能降低接口在传输过程中,被抓包然后篡改接口内的参数的可能,我们可以考虑对接口的所有入参做签名验证,后端在网关依照相同的算法生成签名做匹配,不能匹配的返 ...
- angularjs 1.4.x 内部组件介绍
内部Services 1, $cacheFactory angular 内部缓存类,构建一个缓存对象. var cache = $cacheFactory('cacheId'); expect($ca ...
- (四)kafka基础术语
1 Topic Kafka消息分类的标签,是一个逻辑概念. 2 Partion 主题作为消息的归类,可以细分为一个或多个分区,分区可以看做是对消息的二次归类.分区可以有一个至多个副本,每个副本对应一个 ...
- c语言学习---gets()读取字符串,以及\0,fgets()put()fputs()
#include<stdio.h> //gets()读取字符串, 可以读取空格 int main() { char num[2] = "";//gets 也会造成内存污 ...