做好事不留名的 sourcemap

当我们在开发代码的时候,遇到错误的时候可以在控制台定位到具体的问题,就像这样:问题在于,由于打包动作会将我们的原始代码进行编译、压缩,最后在产物中早已没有我们的原始代码,打开产物,我们可以见到的只有这样的代码:既然如此,为什么我们可以通过控制台,在原始代码中定位到错误位置呢?答案就是本文的主角:source map。在前端工程体系中,一份代码从开发到上线,大多需要经过打包编译的步骤,为的是:

  1. 将 jsx, tsx, ts 之类的文件类型转译成 runtime 可以识别的 js

  2. 将 js 转译成适用范围更广的 es5

  3. 将多个 js 文件压缩成一个最终的产物,并对代码进行一定程度的混淆

经过以上三个步骤,我们的代码已经变得面目全非。原始信息的丢失不利于对代码进行追踪,如果此时遇到错误,压缩过后的代码会让我们无从下手。但是,如果我们的打包产物中有这样的文件,我们便能利用这些文件还原出原始代码:这些就是 sourcemap,那么 sourcemap 是怎么生成的?我们应该怎么使用 sourcemap?

sourcemap 原理

sourcemap 的构成

为了更清晰的描述 sourcemap 的生成,我用一个最简单的 case 来编译并生成 sourcemap

// input
const example = () => {console.log('example');
}// output
"use strict";var example = function example(){console.log("example");
};

利用 babel 将上述代码转为 es5 的同时,我们可以得到一份 sourcemap:

{"version": 3,"sources": ["src/example.js"],"names": ["example","console","log"],"mappings": ";;AAAA,IAAMA,OAAO,GAAG,SAAVA,OAAU,GAAM;AACpBC,EAAAA,OAAO,CAACC,GAAR,CAAY,SAAZ;AACD,CAFD","sourcesContent": ["const example = () => {\n  console.log(\"example\");\n};\n"]
}

可以看到其有多个属性,分别代表着:

  1. version: source map 的版本号。

  2. sources: 转换前的文件。该项是一个数组,可能存在多个文件合并成一个文件。

  3. names: 转换前的所有变量名和属性名。

  4. mappings: 记录位置信息的字符串。

  5. sourceContent: 原始内容。

其中最重要的,便是记录着原始代码和编译后代码映射关系的 mappings 字段。

mappings 是如何记录映射的

可以花两分钟简单思考一下,如果是你来设计 sourcemap,你会如何记录一份原始代码到编译后代码的映射?很简单,我将编译后的每一个单词,对应的原始位置都记录下来就可以了,需要注意的是,由于存在多个文件编译成一个文件的情况,所以我们需要记录下原始文件名:

编译后的位置(行/列) 编译后单词 原始文件名 原始位置(行/列) 原始单词
0, 0 var src/example.js 0, 0 const
0, 4 example src/example.js 0, 6 example
0, 11 = src/example.js 0, 13 =
0, 14 function src/example.js 0, 16 (
0, 23 example src/example.js 0, 6 example
0, 30 ( src/example.js 0, 16 (
0, 33 { src/example.js 0, 22 {

到这里,我们已经将第一行代码的原始信息记录下来了,可以表示为:

0|0|src/example.js|0|0, 0|4|src/example.js|0|6, 0|11|src/example.js|0|13, 0|14|src/example.js|0|16, 0|23|src/example.js|0|16, 0|30|src/example.js|0|16, 0|33|src/example.js|0|22

同样的,第二行代码与第三行代码的映射关系可以用相同的方式记录下来。当我们完成了映射关系的记录后,便需要考虑一个现实问题:只有 23 个字符的原始信息,我们需要用 150 个字符来记录其映射关系。有没有什么办法,可以用更少的字符记录呢?

现在,我们对照 sourcemap 的做法,将上面的信息进行逐层的优化:

优化

省去输出文件中的行号,改用 ; 来标识换行

利用; 来标识换行,我们可以将上述的编码节省为:

0|src/example.js|0|0, 4|src/example.js|0|6, 11|src/example.js|0|13, 14|src/example.js|0|16, 23|src/example.js|0|16, 30|src/example.js|0|16, 33|src/example.js|0|22;

用索引标识变量名

前面我们提到 sourcemap 中的 names 数组,在 sourcemap 中,它会将变量名在 names 数组中的索引也记录下来,所以编码会变成如下:

0|src/example.js|0|0, 4|src/example.js|0|6|0, 11|src/example.js|0|13, 14|src/example.js|0|16, 23|src/example.js|0|16|0, 30|src/example.js|0|16, 33|src/example.js|0|22;

用索引来代替文件名

使用 sources 属性记录下来的原始文件数组,在记录原始信息时用索引代替,如 src/example.js 在 sources 中的索引为 0,所以可以进一步简化为:

0|0|0|0, 4|0|0|6|0, 11|0|0|13, 14|0|0|16, 23|0|0|16|0, 30|0|0|16, 33|0|0|22;

用相对位置来代替绝对位置

当文件内容巨大时,上面精简后的代码也有可能某些数字会随着增加而变得很长,如果一行的位置记录了某个位置,那么根据这一位置进行相对定位是可以到达一行内的任意位置。如:

编译后的位置(列) 编译后单词 原始文件名 原始位置(行/列) 原始单词
0 var src/example.js 0, 0 const
4(上一个位置+4) example src/example.js 0, 6 example
7(上一个位置+7) = src/example.js 0, 7 =
3(上一个位置+10) function src/example.js 0, 3 (
9(上一个位置+9) example src/example.js 0, -10 example
7(上一个位置+7) ( src/example.js 0, 10 (
3(上一个位置+3) { src/example.js 0, 6 {

所以我们的 mappings 继续被简化为:

0|0|0|0, 4|0|0|6|0, 7|0|0|7, 3|0|0|3, 10|0|10, 9|0|0|-10, 7|0|0|10, 3|0|0|6;

VLQ 编码

如果我们可以想办法去掉每个单词之间的分隔符(在我们的例子中是 | ),我们可以进一步省下大量的字符。当然,限制我们去掉这个分隔符的问题是,我们无法在没有分隔符的帮助下区分 10010 是 10|0|10 还是 100|1|0,但我们可以设计一套方法,让我们能够在去掉分隔符的情况下依然能够正确的分组。sourcemap 使用了这一套方法:

在二进制中,使用 6 个比特来记录一个数字,用其中一个字节来标识它是否结束(下方 C),再用一位标识正负(下方 S),剩下还有四位用来表示数值。用这样 6 个字节来表示我们需要的数字。

十进制 二进制
4 100
0 0
6 110

任意数字中,第一组的第一个比特就已经明确标明该数字的正负,所以后续比特不需要再标识,也就是说,第一组有 4 个比特来表示数值,后续每一组都有 5 个比特来表示数值(每组依然有一个比特标识是否结束) 我们用上述的简化过的 mappings 的第二项 4|0|0|6|0 为例:

所以它们应该被编码为:

注:如果是一个分组无法表达的数字,则会用第二个分组来容纳剩余部分,这里举个例子:23 的二进制为 10111,由于一个分组无法容纳,那么将 10111 分为两组,第一组是最后面的四位,既 10111,第二组是剩下的 10111,那么它最终会被编码为:101110 000001。

所以 4|0|0|6|0 最终被转化为 001000 000000 000000 001100 000000,随后再进行 base64 编码得到:

001000 000000 000000 001100 000000
I      A      A      M      A

之所以要用 6 个比特为一组记录一个数字,正是因为每一个 base64 编码最多可以表示二进制 6 位,所以通过这样的编码,我们将 4|0|0|6|0 转化为了 IAAMA。至此,我们便了解了 sourcemap 的原理和生成方式。

webpack 中不同的 sourcemap

我们知道 sourcemap 是用来记录编译后代码到原始代码的映射关系,所以理论上每一个编译过程都可以产生一份 sourcemap,如 TS(X) 转 JS,ES6 转 ES5,代码压缩等等。经过这么多过程,我们需要将每一步生成的 sourcemap 合并起来才能最终得到一份生产环境代码到开发环境代码的 sourcemap。我们可以用社区上现有的轮子手动实现 sourcemap 的合并:https://www.npmjs.com/package/merge-source-map[1]

不过这样还是有一些麻烦,在前段工程体系中,webpack 这类打包工具是不可或缺的,webpack 本身也提供了 sourcemap 的生成,而且不需要我们关注其中 sourcemap 合并的细节。在 webpack 配置中,用 devtool 属性来标识使用何种模式生成 sourcemap。devtool 中有多种类型可以使用,如 eval , inline, cheap... 以这一段程序为例子:

// index.js
import a from "./a";
import b from "./b";b(a);// a.js
export default "a";// b.js
export default (str) => {throw new Error(str);
};

source-map

该模式会生成一份独立的.map 文件。其和我们前面讲述的一样。它是最详细,同时也是耗时最久的模式。

inline (如 inline-source-map)

该模式不会生成一份独立的.map 文件,而是用 base64 编码将 sourcemap 进行编码后附在编译后代码的末处。缺点是这样会使得编译后代码的体积变得庞大,其他方面则和 source-map 模式一样。

eval

源码以字符的形式被 eval(…) 来调用,不会生成 sourceMap 信息,只会通过一个附着在各个模块后的 sourceURL 来存储原始文件的位置,同时,我们只能在控制台中看到经过 webpack 处理的编译后代码,所以它并不能反映真实的行号:

eval-source-map

源码以字符的形式被 eval(…) 来调用,同时生成 sourceMap 信息,sourcemap 信息(以 base64 编码的形式)和 sourceURL 被附着在各个模块后,在这个模式下,我们可以在控制台中看到原始的代码。

cheap-source-map

生成的 sourcemap 只有行信息,不会记录列信息:但是,细心的你可能发现了,cheap-source-map 模式下的 sourcemap 依然没有映射到真正的原始代码(原本的箭头函数被 babel 编译成了 function)。cheap-source-map 记录下的是与被 loader(此处是 babel-loader)转化后的代码之间的映射。

cheap-module-source-map

使用 cheap-module-source-map 可以记录下 loader 转译前的信息:

nosources

在这个模式下,会生成不包含 sourcecontent 的 sourcemap,具体表现为有错误堆栈信息但没有具体的内容:

hidden

在这个模式下,会生成 sourcemap,但是不会将 sourcemapURL 信息附着在编译后代码中。此外还有一些模式的组合,具体可以是"^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$"

开发环境应该用哪种模式?生产环境呢?

开发环境下一般使用构建速度快,同时可以看到原始信息的 eval-source-map 模式。而在生产环境下,为了不泄露源代码,可以使用 hidden-source-map 模式。

为什么有时候 sourcemap 表现不准?

一个猜测:上面我们介绍了 webpack 中多种 sourcemap 模式。其中有些模式是以牺牲掉一部分信息来换取更好的构建速度的(如 eval、cheap-source-map)。如果 webpack 的 hot server 没有采用正确的 sourcemap 模式,就会出现定位不准的问题。这个时候通常更换一下 sourcemap 模式为cheap-module-source-map甚至source-map模式就 OK 了。同时,如果打包过程的某个环节在转译代码的时候将原始信息丢失了,也可能会出现最终合并成的 sourcemap 定位不准。


相关链接

  1. D-kylin/note[2]

  2. Rich-Harris/vlq[3]

  3. 细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息[4]

参考资料

[1]

https://www.npmjs.com/package/merge-source-map: https://www.npmjs.com/package/merge-source-map

[2]

D-kylin/note: https://github.com/D-kylin/note/blob/master/VLQ%E7%BC%96%E7%A0%81.md

[3]

Rich-Harris/vlq: https://github.com/Rich-Harris/vlq

[4]

细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息: https://segmentfault.com/a/1190000016987829

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

何为SourceMap?相关推荐

  1. sourceMap: devtool 模式以及SourceMapDevToolPlugin的使用

    1.  什么是sourceMap sourceMap 是一个信息文件, 维护着代码转换编译前后的映射关系; 2. devtool : 选项控制是否生成,以及如何生成 source map source ...

  2. 数据可视化 工具_卓越中心和数据可视化工具2020中的组织中的数据聚合

    数据可视化 工具 人工智能 (ARTIFICIAL INTELLIGENCE) Data consumption is rising steadily in 2020 with estimates s ...

  3. 稀疏性如何为AI推理增加难度

    稀疏性如何为AI推理增加难度 NVIDIA Ampere架构使数学运算加倍,以加速对各种神经网络的处理. 如果曾经玩过游戏Jenga,那么将有一些AI稀疏感. 玩家将木制积木交叉成一列.然后,每个玩家 ...

  4. 腾讯联姻开心网意欲何为

    今天杨长升在新浪科技上看到这样一条信息"腾讯日前已收购开心网部分股份,有意成为开心网大股东."据了解,早在8月就曾有消息称,腾讯已收购开心网部分股份,现有一位投资界人士处证实了最新 ...

  5. 如何为ccflow工作流引擎增加一个优先级PRI?

    为什么80%的码农都做不了架构师?>>>    如何为ccflow工作流引擎增加一个优先级PRI? 对于一条流程的优先级可分为 低,中,高三个级别,用这个状态来标示这条流程的紧急程度 ...

  6. ios单应用模式_如何为iOS 13暗模式设置应用

    ios单应用模式 Apple launched the much-awaited iOS 13 updates globally on September 19 across all iPhones ...

  7. jupyter笔记本_如何为Jupyter笔记本电脑设置PySpark

    jupyter笔记本 by Tirthajyoti Sarkar 由Tirthajyoti Sarkar 如何为Jupyter笔记本电脑设置PySpark (How to set up PySpark ...

  8. 如何为Android上的产品设计一款合适的图标

    如 果你已经完成了你的app,你一定会马上向其它人宣布这件事情.但是你需要注意一个很重要的问题,那就是app的图标.你的图标可能在项目启动之 前就已经设计好了,但我不喜欢这样,如果app没有完成实际上 ...

  9. 详解JVM内存管理与垃圾回收机制2 - 何为垃圾

    随着编程语言的发展,GC的功能不断增强,性能也不断提高,作为语言背后的无名英雄,GC离我们的工作似乎越来越远.作为Java程序员,对这一点也许会有更深的体会,我们不需要了解太多与GC相关的知识,就能很 ...

最新文章

  1. 强化学习(九)- 策略梯度方法 - 梯度上升,黑箱优化,REINFORCE算法及CartPole实例
  2. 2019-2020中国趋势报告,203页PPT解读16大机会
  3. 数据字典 加载到 web 项目的 application 全局
  4. 通过HTTP协议上传文件
  5. python configparse_python中ConfigParse模块的用法
  6. 2018-03-26
  7. 如何文件服务器安装打印机,如何安装打印机方法图解
  8. 【Spring】CGLIB动态代理
  9. linux挂载磁盘没有权限,linux肿么知道哪个盘没有被挂载
  10. laravel中修改默认时区
  11. Android动画居然还能这么写
  12. HBuilde H5开发,关于JSON的Storage存储
  13. oracle 表列统计信息,Oracle中收集表与列统计信息
  14. KGB知识图谱,利用科技解决传统知识图谱问题
  15. HDU - 4311 Meeting point-1(最小曼哈顿距离和)
  16. 项目需求|眼镜试戴app(ios)
  17. QGIS基础教程 (入门级)——下载安装、新建工程、加载数据
  18. 2022年牛客多校第三场补题记录
  19. Mac appStore下载软件报500错误的解决方法
  20. 【STM32】STM32的时钟系统

热门文章

  1. 游戏特效设计用什么制作的?
  2. Python爬取《哆啦A梦-伴我同行2》影评,手把手带你绘制词云图!
  3. c语言表达式用法,C语言 运算符、表达式和条件结构 用法总结
  4. kdbx后缀文件打开
  5. 微信小程序滚动播放内容
  6. vs2010C#项目-英文版打包中文框架出错的解决办法
  7. kuberbetes Pod 健康检查
  8. 天津滨海职业学院计算机证书从哪领,2016年9月天津计算机等级考试准考证领取地点电话...
  9. UE4之局域网游戏如何更改配置文件
  10. csapp-lab1