一、什么是源映射

为了提高性能,很多站点都会先压缩 JavaScript 代码然后上线,

但如果代码运行时出现错误,浏览器只会显示在已压缩的代码中的位置,很难确定真正的源码错误位置。

这时源映射就登场了。

源映射(Source Map)是一种数据格式,它存储了源代码和生成代码之间的位置映射关系。

源映射一般使用 .map 扩展名,源映射本质是一个 JSON 文本文档,其 MIME 类型也一般设为 application/json。

二、如何使用源映射

在 JavaScript 代码中添加注释:

//# sourceMappingURL=file.js.map

浏览器(最新版 Chrome、Firefox 和 Edge 均支持)就会加载 file.js.map 并自动计算代码的实际位置。

在 Chrome 开发面板(按F12打开)的设置(按F1打开)中,可以通过勾选 "Enable Source Maps" 选项来设置是否需要加载源映射。

源映射本身并不会影响代码的执行,只会在定位错误位置时被使用。

最早浏览器是通过 "@ sourceMappingURL" 标记地址的,但这引发了一些引擎和工具的问题(和 IE 的 @cc_on 冲突),所以现在改成了 "# sourceMappingURL"。

NodeJS 中的源映射

NodeJS 在显示错误堆栈时,并不会加载源映射,可以借助 source-map-support 这个包实现。

$ npm install source-map-support

然后在代码顶部加上:

require('source-map-support/register');

这时所有堆栈位置就会被更新成真正的源码位置。

VSCode 中的源映射

VSCode 支持在调试时使用源映射,在 .vscode/launch.json 中添加:

{
"configurations": [
{
"sourceMaps": true,
"outDir": "${workspaceRoot}/build"
}
]
}

注意必须设置 outDir,否则可能出现无法添加断点的问题。

三、如何生成源映射

现在很多生成工具都支持生成源映射,如 Uglify, Grunt, Gulp,可以参考生成工具的文档。

四、源映射格式详解

源映射本质是一个 JSON,格式如:

{
version: 3,
file: 'min.js',
names: ['bar', 'baz', 'n'],
sourceRoot: 'http://example.com/www/js/',
sources: ['one.js', 'two.js'],
sourcesContent: ['', ''],
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
}

主要包括以下字段:

  • version: Source Map(源映射)的版本号,目前统一使用版本 3。
  • file: (可选)生成文件的路径(相对于 Source Map(源映射) 本身路径)。
  • names: (可选)所有名称,如变量名、函数名,下文详细介绍。
  • sourceRoot: (可选)所有源文件的根路径(相对于 Source Map(源映射) 本身路径)
  • sources: 所有源文件的路径(相对于 sourceRoot)
  • sourcesContent: (可选)所有源文件的内容。
  • mappings: 所有映射点,下文详细介绍。

其中,所有相对路径的计算方式和网页中的相对地址相同。

所有地址可以是 http:// 开头的网址或者是本地文件地址。

sources

sources 是一个数组,这意味着一个文件可以从多个文件生成过来。

很多读者会觉得这里出现的路径太多,帮大家捋一捋:

假如源文件是 xld.js ,通过压缩生成了 xld.min.js 和 xld.min.js.map ,那么:

在 xld.min.js 中需要通过 // #sourceMappingURL=xld.min.js.map 指定它的源映射。

在 xld.min.js.map 中需要通过 file: xld.min.js 指定它生效的文件。file 并不是必须的字段,该字段只用于检验。

在 xld.min.js.map 中需要通过 sources: ["xld.js"] 指定真正的源文件地址。

sourceRoot

如果 sources 有很多且有相同的前缀,则可以统一提取到 sourceRoot 中。所以以下是等价的:

{
sources: ["a/foo.js", "a/bar.js"],
}
{
sourceRoot : "a",
sources: ["foo.js", "bar.js"],
}

mappings

mappings 是记录映射关系的核心。

从表面看,mappings 是一个字符串,里面由很多看似乱码的字符组成。

其实 mappings 是一个数组通过一定的方式编码得到的,这个数组包含了生成的文件中每行的映射点列表:

mappings = [ 
第 1 行的映射点列表,
第 2 行的映射点列表,
...
]

每行的映射点列表又是一个数组,包含了该行中所有列的映射点。

mappings = [
[ 第 1 行第 1 个映射点, 第 1 行第 2 个映射点, ... ] // 第 1 行的映射点列表
[ 第 2 行第 1 个映射点, 第 2 行第 2 个映射点, ... ] // 第 2 行的映射点列表
...
]

每个映射点又是一个数组,数组中包含了 5 个数字:

[ 生成文件的列, 源文件索引, 源文件行号, 源文件列号, 名称索引 ]

其中,名称索引可省略。源文件索引, 源文件行号, 源文件列号也可同时省略,

这表示映射点的数组长度可能是 1、4 或 5。

源映射所有行列号都是从 0 开始计数的,本文中所使用的行列号也都是从 0 开始计数的。

举个例子,比如现在有一个源映射如下:

 {
version: 3,
file: 'min.js',
names: ['bar', 'baz', 'n'],
sourceRoot: 'http://example.com/www/js/',
sources: ['one.js', 'two.js'],
sourcesContent: ['', ''],
mappings: [
[],
[],
[
[1, 0, 2, 5, 1],
[4, 0, 3, 6, 0] // #13 行
]
]
}

以 #13 行数据为例:#13 行出现在 mappings[2] 里面,因此它表示生成的文件第 2 行的信息。

#13 行包含了 5 个数字,分别表示生成文件的列 = 4, 源文件索引 = 0, 源文件行号 = 3, 源文件列号 = 6, 名称索引 = 0。

最终得到:生成的文件(即 min.js)中,行 2 列 4 的位置是从第 0 个源码(即 http://example.com/www/js/one.js)中行 3 列 6 的位置生成的,源码中相关的名称是 0(即 bar)。

通过多个映射点,可以一一定义生成的文件中每个位置对应的实际源码位置。

注意即使指定了某一行列的源码位置,也无法推断相邻行列的源码的位置,必须一一添加映射。

名称索引可以用于快速定位变量和函数压缩前的名字。

mappings 编码

为了节约存储空间,mappings 会被编码成一个字符串。

第一步:计算相对值

将映射点中每个数字替换成当前映射点和上一个映射点相应位置的差,如:

mappings: [
[
[1, 0, 2, 5, 1],
[, 0, , 6, 0]
],
[
[, 0, , 3, 0]
]
]

其中第一个映射点不变,以后每个映射点上每个数字都减去上一个映射点(允许跨行)对应位置的数字(如果映射点元素个数不足 5,则省略部分按 0 处理),最后得到:

mappings: [
[
[1, 0, 2, 5, 1], // 不变
[1, 0, 1, 1, -1] // 1 = 2 - 1, 0 = 0 - 0, 1 = 3 - 2, 1 = 6 - 5, 1 = 0 - 1
],
[
[3, 0, -1, -3, 0] // 3 = - , 0 = 0 - 0, -1 = - , -3 = 3 - 6, 0 = 0 - 0
]
]

第二步:合并数字

将 mappings 中出现的所有数字写成一行,不同映射点使用,(逗号)隔开,不同的行使用;(分号)隔开。

1 0 2 5 1 , 1 0 1 1 1 ; 3, 0 , -1, -3, 0

第三步:编码数字

对于每个数字,都使用 VLQ 编码 将其转为字母,具体转换方式为:

1. 如果数字是负数,则取其相反数。

2. 将数字转为等效的二进制。并在末尾补符号位,如果数字是负数则补 1 否则补 0。

3. 从右往左分割二进制,一次取 5 位,不足的补 0。

4. 将分好的二进制进行倒序。

5. 每段二进制前面补 1,最后一段二进制补 0。这样每段二进制就是 6 位,其值范围是 0 到 64(含0,不含64)。

6. 根据 Base64 编码表将每段二进制转为字母:

以 170 为例,

1)转为二进制即:10101010

2)170 是正数,右边补 0:101010100

3)从右往左分割二进制:10100,  1010。

4)不足 5 位的补 0:01010,  10100

5)倒序:10100, 01010

6)除最后一个前面补 0,其它每段前面补 1:110100, 001010

7)转为十进制:52, 10。

8)查表得到:0K

任意一个整数都能通过 VLQ 编码得到一串字母和数字表示的文本。

VLQ 编码最早用于MIDI文件,它可以非常精简地表示很大的数值。

第四步:合并结果

将第二步中的每个数字进行 VLQ 编码再拼接就是最终的结果。

CAEKC,CACCC;GADHA

五、源映射相关的工具和框架

为更好理解源映射,可以使用 源映射可视化 工具。

为了处理源映射,可以使用官方的 source-map 库。

同时推荐更好用的库:source-map-builder,它相比官方的库性能更高、具有更智能的推导功能。

六、参考链接

Source Map Revision 3 Proposal

http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/

http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html

源映射(Source Map)详解的更多相关文章

  1. JavaScript Source Map 详解

    源码地址: http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html 上周,jQuery 1.9发布. 这是2.0版之前的最后 ...

  2. 前端构建:Source Maps详解

    一.前言 当使用CoffeeScript.ClojureScript编写前端脚本时,当使用Less.Sacc编写样式规则时,是否觉得调试时无法准确找到源码位置呢?当使用jquery.min.js等经压 ...

  3. List、Set、Map详解及区别

    一.List接口 List是一个继承于Collection的接口,即List是集合中的一种.List是有序的队列,List中的每一个元素都有一个索引:第一个元素的索引值是0,往后的元素的索引值依次+1 ...

  4. Android源码下载方法详解

    转自:http://www.cnblogs.com/anakin/archive/2011/12/20/2295276.html Android源码下载方法详解 相信很多下载过内核的人都对这个很熟悉 ...

  5. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  6. Sass map详解

    作为一个CSS预处理器,Sass正受到越来越多的青睐,诸如Github.Codepen.CSS-Tricks.SitePoint.w3cplus等网站采用Sass组织.管理CSS文件,Sass正在逐渐 ...

  7. 【转】ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    原文地址:http://blog.csdn.net/a396901990/article/details/36475213 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量—— ...

  8. Spring Boot源码中模块详解

    Spring Boot源码中模块详解 一.源码 spring boot2.1版本源码地址:https://github.com/spring-projects/spring-boot/tree/2.1 ...

  9. ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

    简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量--onMeasure():决定View的大小 2.布局--onLayout():决定View在ViewGroup中的位置 3. ...

随机推荐

  1. Mysql 备份

    MySQL数据库备份命令   备份MySQL数据库的命令 mysqldump -hhostname -uusername -ppassword databasename > backupfile ...

  2. 详解 JavaScript的 call() 和 apply()

    定义 ECMAScript规范为所有函数都包含两个方法(这两个方法非继承而来), call 和 apply .这两个函数都是在特定的作用域中调用函数,能改变函数的作用域,实际上是改变函数体内 this ...

  3. ASP.NET列表信息以Excel形式导出

    1.从数据查出数据扔进table中: private DataTable getTable() { var dbHelper = applyBLL.CreateDataBase("VISAd ...

  4. php ajax 交互

    html 页面 <body> <button id="oBtn">点击我</button> <script type="text ...

  5. jQuery第二篇 (帅哥)

    1.1 jQuery操作DOM jQuery课程的目标:学会使用jQuery设计常见效果 选择器 基本选择器:#id ..class .element.* . 层级选择器: 空格.>.+.~ 基 ...

  6. 修改Credentials 密码

    今天,Leader 吩咐要修改管理账户的密码,我负责的Part是修改package和 Replication的Job的密码.仔细想了下,由于我们使用的Windows验证方式,而Job在执行时,是使用P ...

  7. vue中v-bind:class动态添加class

    1.html代码 <template v-for='item in names'> <div id="app" class="selectItem&qu ...

  8. MySql事务概述

    事务是访问并更新数据库中各种数据项的一个程序执行单元.在事务中的操作,要么都执行修改,要么都不执行,这就是事务的目的,也是事务模型区别于文件系统的重要特征之一. 严格上来说,事务必须同时满足4个特性, ...

  9. Android之JSON解析

    做个Android网络编程的同学一定对于JSON解析一点都不陌生,因为现在我们通过手机向服务器请求资源,服务器给我们返回的数据资源一般都是以JSON格式返回,当然还有一些通过XML格式返回,相对JSO ...

  10. C++中public、protected及private用法

    转自:http://www.jb51.net/article/54224.htm 初学C++的朋友经常在类中看到public,protected,private以及它们在继承中表示的一些访问范围,很容 ...