随着时代的发展,JaScript 脚本正变得越来越复杂。大部分源码(尤其是各种函数库和框架)都要经过转换,才能投入生产环境。
常见的源码转换,主要是以下三种情况:
(1)压缩,减小体积。比如 jQuery 1.9 的源码,压缩前是 252KB,压缩后是 32KB。 (2)多个文件合并,减少 HTTP 请求数。 (3)其他语言编译成 JaScript。这三种情况,都使得线上实际运行的代码不同于开发时的代码,调试代码排查问题就变得困难重重。
通常,JaScript 的解释器会告诉你,第几行第几列代码出错了。但是,这对于转换后的代码毫无用处。举例来说,jQuery@1.9 压缩后只有 3 行,每行 3 万个字符,所有内部变量都改了名字。你看着报错信息,感到毫无头绪,根本不知道它所对应的原始位置。
这就是 Source map 想要解决的问题。
编译后的 Vue.js 的源码 
简单来说 Source map 就是一个存储信息的文件,里面储存着位置信息。
Source map 英文释义:源程序映射。位置信息:转换后的代码 对应的 转换前的代码 位置映射关系。有了 Source map,就算线上运行的是转换后的代码,调试工具中也可以直接显示转换前的代码。这极大的方便了我们开发者调试和排错。
三、如何使用 Source map只要在转换后的代码尾部,加上一行如下代码即可。
//# sourceMappingURL=main.js.map注意
= 后的名称,依据对应 map 文件名定义;map 文件可以放在网络上,也可以放在本地; 四、如何生成 Source map 4.1 概念借助打包工具,在打包编译生成目标代码的同时,生成 Source map(也就是目标代码和源代码的映射关系文件)。
4.2 演示我用比较常见的打包工具:webpack,来演示一下如何生成 Source map。
NodeJs 请自行安装,版本需大于 8。
1. 创建一个 main.js 文件
alert('tomato')2. 初始化项目 + 安装依赖
npm init -y npm i webpack@5 webpack-cli -D3. 创建一个 webpack.config.js 配置文件
const path = require('path') module.exports = { // 1.入口文件 从那个文件打包入口文件(相对路径) entry: './main.js', // 2.输出内容 output: { filename: 'main.js', // 一个是文件名 path: path.resolve(__dirname, 'dist'), // 一个是输出路径(绝对路径) }, // 3. 加载器 // module: { // rules: [], // }, // 4. 插件 // plugins: [], // 5. 配置 devtool: 'source-map', // 6. 模式 // development mode: 'production', }4. 目前的文件结构

5. 开始打包
npx webpack注意事项:
示例中 webpack 使用的版本为 webapck@5。在 webpack 中,devtool 可以配置的属性值有很多,本次演示就以 source-map 为例。(其他属性值后续会做讲解)6. 输出:
执行完上述命令后,会生成一个 dist 文件夹,其中有两个文件:
目标文件:main.jsSource map 文件:main.js.map 五、Source map 文件介绍 5.1 文件结构就以我们上述案例生成的 main.js.map 为例,我们来了解一下,它的内容结构是怎样的。
{ // version:Source map 的版本,目前为 3。 "version": 3, // file:转换后的文件名。 "file": "main.js", // sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。 "sourceRoot": "", // sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。 "sources": ["webpack://app/./main.js"], // sourcesContent:原始代码 "sourcesContent": ["alert('tomato')\r\n"], // names:转换前的所有变量名和属性名。 "names": ["alert"], // mappings:记录位置信息的字符串,下文详细介绍。 "mappings": "AAAAA,MAAM" }Source map 文件类似 JSON 格式,它存在 7 个属性。
以下是对每个属性的解释:
version:Source map 的版本,目前为 3。
Source map 也是从无到有逐渐发展过来的。对比新旧的版本,它们存储信息的编码方式存在差异。目前主流的版本为 3, 它采用的编码方式是 Base64 VLQ ,对标历史版本,文件体积精简很多。file:转换后的文件名。
sourceRoot:转换前的文件所在的目录。
如果与转换前的文件在同一目录,该项为空。
sources:转换前的文件。
该项是一个数组,表示可能存在多个文件合并成一个目标文件。
sourcesContent:原始代码
sourcesContent 属性会存储对应的源代码。
names:转换前的所有变量名和属性名。
mappings:记录位置信息的字符串,。
存储位置信息的属性,下文详细介绍。
5.2 mappings 介绍 5.2.1 mappings 三个特点mappings 本身是一个字符串,其中有三个特点:
分号:每一个分号对应转换后源码的一行。逗号:每个逗号对应转换后代码的一个位置。字符:逗号或者分号之间的字符是以 Base64 VLQ 编码规则存储的位置信息。举例来说,假定 mappings 属性的内容如下:
"mappings":"AAAAA,BBBBB;CCCCC"就表示:
转换后的代码有两行。第一行有两个位置,第二行有一个位置。 5.2.2 字符的五个位置?例如 AAAAA, 它对应那些信息呢?
第一位,表示这个位置在(转换后的代码的)的第几列。
第二位,表示这个位置属于 sources 属性中的哪一个文件。
第三位,表示这个位置属于转换前代码的第几行。
第四位,表示这个位置属于转换前代码的第几列。
第五位,表示这个位置属于 names 属性中的哪一个变量。
5.2.3 字符解析?单纯存储 A 是没什么意义,我们重点关注 A 代表什么?
Source map 使用的是 Base64 VLQ 编码规则,具体的规则后续会写,目前直接演示转换结果:
转换演示:
示例一: AAAAA [0,0,0,0,0] 示例二: ubAAAA [439,0,0,0,0] 示例三: MAOA,MALAA,QAAQC,IAAI,GAKN,K [6,0,7,0], [6,0,-5,0,0], [8,0,0,8,1], [4,0,0,4], [3,0,5,-6], [5]转换方法:
用工具解析 点击这里;
注意事项:
注意,并不是一个字母对应一个位置。是一组字母表示一个位置。例如 ubAAAA 解析为: [439,0,0,0,0];
我自己学习到这里的时候,想到这么一个问题:所有的字母加特殊符号,就算考虑大小写,它们能记录的情况肯定是有限的,而任意一个源码,一行就有上万行。
很显然一个字母对应一个位置,并不能记录上万行的内容,所以这里是一组字母对应一个位置。
5.2.4 真实案例讲解映射关系上方讲述了一些概念知识,但是概念不是很好理解,至少我是有些绕的,现在用实际案例去演示一下如何解析的。
注意,为了节省空间,Source map 中使用的 Base64 VLQ 编码除了第一的字符外,剩余的计算都是相对位置的计算。
也就是相对于上次记录的位置的偏移量。
5.2.4.1 案例一:源码:
alert('tomato')打包输出的代码:
alert('tomato') //# sourceMappingURL=main.js.map打包输出的 Source map 文件:
{ "version": 3, "file": "main.js", "mappings": "AAAAA,MAAM", "sources": [ "webpack://app/./main.js" ], "sourcesContent": [ "alert('tomato')\r\n" ], "names": [ "alert" ], "sourceRoot": "" }解析:
1. 输出的 'mappings': "AAAAA,MAAM" 2. 对应的数字为: [0,0,0,0,0], [6,0,0,6] 3. 对应的含义分别为: 转换后代码的第0列,第0个引入的文件,转换前的第0行,转换前第0列,第0个变量; 对应的就是 alert 转换后代码的第5列,第0个引入的文件,转换前的第0行第0列。没有变量; 对应的就是 ('tomato') 5.2.4.2 案例二:这个案例在案例一的基础上,添加一行注释,对比一下差异。 重点看一下源码
源码:
// lazy-tomato alert('tomato')打包输出的代码:
alert('tomato') //# sourceMappingURL=main.js.map打包输出的 sourcemap 文件:
{ "version": 3, "file": "main.js", "mappings": "AACAA,MAAM", "sources": [ "webpack://app/./main.js" ], "sourcesContent": [ "// lazy-tomato\r\nalert('tomato')\r\n" ], "names": [ "alert" ], "sourceRoot": "" }解析:
1. 输出的 'mappings': (历史的是 "AAAAA,MAAM") "AACAA,MAAM" 2. 对应的数字为: (历史的是 [0,0,0,0,0], [6,0,0,6]) [0,0,1,0,0], [6,0,0,6] 3. 对应的含义分别为: 转换后代码的第0列,第0个引入的文件,*转换前的第1行*,转换前第0列,第0个变量; 对应的就是 alert 转换后代码的第5列,第0个引入的文件,转换前的第0行第0列。没有变量; 对应的就是 ('tomato')可以看到对比案例一,案例二的差异:转换前的代码行数加一了。
5.2.4.3 案例三:源码:
function fn(a, b) { return a + b } console.log(fn(1, 4))打包输出的代码:
console.log(5) //# sourceMappingURL=main.js.map打包输出的 Source map 文件:
{ "version": 3, "file": "main.js", "mappings": "AAGAA,QAAQC,IAFCC", "sources": [ "webpack://app/./main.js" ], "sourcesContent": [ "function fn(a, b) {\r\n return a + b\r\n}\r\nconsole.log(fn(1, 4))\r\n" ], "names": [ "console", "log", "a" ], "sourceRoot": "" }解析
1. 输出的 'mappings': "AAGAA,QAAQC,IAFCC" 2. 对应的数字为: [0,0,3,0,0], [8,0,0,8,1], [4,0,-2,1,1] 3. 对应的含义分别为: 转换后代码的第0列,第0个引入的文件,转换前的第3行,转换前第0列,第0个变量; 对应的就是 console 转换后代码的第8列,第0个引入的文件,转换前的第0行,转换前第8列。第1个变量; 对应的就是 log 转换后代码的第4列,第0个引入的文件,转换前的第-2行,转换前第1列。第1个变量;会发现第三个数据无法对应,我理解它对应的是函数 fn 的执行结果,也就是 5;
数据无法对应,网上猜想的解释:为了加快编译速度,Source map 对于一些语法是不会计算偏移的,而是直接返回之前的偏移位置。准确的原因,待确定 六、拓展 6.1 Base64 VLQ 编码规则具体的 Base64 VLQ 编码规则 点击这里
6.2 webpack 中 devtool 的多种配置官方文档地址点击这里
查看官方文档,可以了解到,devtools 的配置项可以达到 10-20 种左右的情况。其实并不需要记住那么多情况,本质上是一些配置项的排列组合。
配置项如下:
source-map:生成 sourcemap 文件,可以配置 inline,会以 dataURL 的方式内联,可以配置 hidden,只生成 sourcemap,不和生成的文件关联;hidden:是否会在打包后文件的末尾增加 sourceURL;inline:不产生独立的 .map 文件,把 source-map 的内容以 dataURI 的方式追加到目标文件末尾; eval:浏览器 devtool 支持通过 sourceUrl 来把 eval 的内容单独生成文件,还可以进一步通过 sourceMappingUrl 来映射回源码,webpack 利用这个特性来简化了 sourcemap 的处理,可以直接从模块开始映射,不用从 bundle 级别。cheap:只映射到源代码的某一行,不精确到列,可以提升 sourcemap 生成速度;nosources:不生成 sourceContent 内容,可以减小 sourcemap 文件的大小;module: sourcemap 生成时会关联每一步 loader 生成的 sourcemap,可以映射回最初的源码; 具体的组合效果,可自行尝试。webpack的 devtool 配置项排列顺序,规则:^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$。 6.3 本地调试线上代码我以一个 Vue2 项目为例:
在一个普通 Vue2 项目中,添加一行报错;

npm run build 打包一下我们的工程。
将打包输出的 dist 文件夹中的 .map 文件,剪切出来存放到本地。然后上传 dist 其他文件到服务器上,用以模拟调试线上代码的情况。
这里生成 Source map 的配置,可以自己灵活配置。 查看线上代码: 报错:
对应源码:
手动添加 Source map 文件
右键 - Add source map;通过 file 协议选择本地的 map 文件,先在浏览器地址栏中输入确保可以访问到。文件路径示例例:file:///C:/Users/17607/Desktop/study/chunk-vendors.7723b084.js.map可以直接将 Source map 文件拖拽到谷歌浏览器中,即可得到这个文件路径
回到控制台发现已经映射到源码了
已经映射到源码了
6.4 Vue 中如何修改 webpack 的 devtool 配置项
Vue 项目,如果使用的是 webpack作为打包工具,想要自定义 devtool 配置,可用如下方式:
官方解释点击这里
// vue.config.js module.exports = { chainWebpack: (config) => { config .when(process.env.NODE_ENV === 'development, (config) => config.devtool('hidden-source-map')) }, } // 2. 当然还有一个属性 productionSourceMap,可以设置是否生成 Source map。 // productionSourceMap 6.5 其他打包工具上述演示生成 Source map,使用的是 webpack 来演示的,了解一下其他打包工具,如何生成 Source map;
rollup1.新建一个打包配置文件: rollup.config.js
module.exports = { input: './main.js', output: { file: './bundle.js', format: 'cjs', sourcemap: true, }, }2.安装依赖,开始打包
npm i -g rollup rollup -c # 注意一下,注意 NodeJs 版本不要太低。 6.5 reverse-sourcemap提到这么一个 npm 依赖,是因为很久之前,番茄我有一次电脑进水了,导致本地 git 仓库丢失了部分 commit 的记录。(大白话来说,代码丢失了)
丢失的代码行数还是比较多的,一整天的工作成果;在无法找回源代码的情况下,我发现最新 Source map 文件还存在。最后我通过这个工具,反编译 Source map 文件,找回了大部分我丢失的代码。使用案例:
# 1. 全局安装此依赖 npm install --global reverse-sourcemap # 2. 指定编译文件后输出的文件目录,指定编译什么文件; reverse-sourcemap --output-dir outDir main.js.map阅读源码:
看一下 reverse-sourcemap 的源码,源码就一个 js 文件,如下:
reverse-sourcemap/index.js
'use strict'; const path = require('path'); const sourceMap = require('source-map'); /** * @param {string} input Contents of the sourcemap file * @param {object} options Object {verbose: boolean} * * @returns {object} Source contents mapped to file names */ module.exports = (input, options) => { const consumer = new sourceMap.SourceMapConsumer(input); return consumer.then((response) => { let map = {}; if (response.hasContentsOfAllSources()) { if (options.verbose) { console.log('All sources were included in the sourcemap'); } debugger console.log(response) response.sources.forEach((source) => { const contents = response.sourceContentFor(source); map[path.normalize(source).replace(/^(\.\.[/\\])+/, '').replace(/[|\,+()?$~%'":*?{}]/g, '').replace(' ', '.')] = contents; }); } else if (options.verbose) { console.log('Not all sources were included in the sourcemap'); } return map; }).catch((e) => { console.log(e); return {}; }); };小结: 浏览了一下 reverse-sourcemap 的源码,使用了 source-map 提供的一个对象,来实现的文件转换。
6.6 source-map待补充…
七、参考和学习过的文章 阮一峰_JaScript Source Map 详解;zxg_神说要有光_彻底搞懂 Webpack 的 sourcemap 配置原理;SourceMap解析;VLQ & Base64 VLQ 编码方式的原理及代码实现;感谢ღ( ´・ᴗ・` )
end 以上就是番茄我对 Source map 的收获总结了。希望自己越来越强,加油!