服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - 编程技术 - Webpack 实战系列一:正确使用 Sourcemap

Webpack 实战系列一:正确使用 Sourcemap

2021-12-15 23:08Tecvan范文杰 编程技术

Sourcemap 协议最初由 Google 设计并率先在 Closure Inspector 实现,它能够将经过压缩、混淆、合并的代码还原回未打包状态,帮助开发者在生产环境中精确定位问题发生的行列位置。

Webpack 实战系列一:正确使用 Sourcemap

一、什么是 Sourcemap

Sourcemap 协议最初由 Google 设计并率先在 Closure Inspector 实现,它能够将经过压缩、混淆、合并的代码还原回未打包状态,帮助开发者在生产环境中精确定位问题发生的行列位置。

Webpack 实战系列一:正确使用 Sourcemap

发展至今,Sourcemap 已广泛受 Webpack、Rollup、Babel、Less、Typescript、Chrome、Safari、VS Code 等工具支持。

Webpack 实战系列一:正确使用 Sourcemap

参考:https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k

实现上,Sourcemap 由三部分组成:

  • 开发者编写的原始代码
  • 经过 Webpack、Rollup 等工程化工具压缩、转化、合并后的产物,且产物中必须包含指向 Sourcemap 文件地址的 //# sourceMappingURL=https://xxxx/bundle.js.map 指令
  • 记录原始代码与经过工程化处理代码之间位置映射关系 Map 文件

页面初始运行时只会加载编译构建产物,直到特定事件发生 —— 例如在 Chrome 打开 Devtool 面板时,才会根据 //# sourceMappingURL 内容自动加载 Map 文件,并按 Sourcemap 协议约定的映射规则将代码重构还原回原始形态,这既能保证终端用户的性能体验,又能帮助开发者快速还原现场,提升线上问题的定位与调试效率。

1.1 示例

以 Webpack 为例,设置 devtool = 'source-map' 即可同时打包出代码产物 xxx.js 文件与同名 xxx.js.map 文件,Map 文件通常为 JSON 格式,内容如:

  1. {
  2. "version": 3,
  3. "sources": [
  4. "webpack:///./src/index.js"
  5. ],
  6. "names": ["name", "console", "log"],
  7. "mappings": ";;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E",
  8. "file": "main.js",
  9. "sourcesContent": [
  10. "const name = 'tecvan';\n\nconsole.log(name)"
  11. ],
  12. "sourceRoot": ""
  13. }

各字段含义分别为:

  • version:指代 sourcemap 版本,目前最新版本为 3names:字符串数组,记录原始代码中出现的变量名
  • file:字符串,该 Sourcemap 文件对应的编译产物文件名
  • sourcesContent:字符串数组,原始代码的内容
  • sourceRoot:字符串,源文件根目录
  • sources:字符串数组,原始文件路径名,与 sourcesContent 内容一一对应
  • mappings:字符串数组,记录打包产物与原始代码的位置映射关系

使用时,浏览器会按照 mappings 记录的数值关系,将产物代码映射回 sourcesContent 数组所记录的原始代码文件、行、列位置,这里面最复杂难懂的点就在于 mappings 字段的规则。

1.2 源码映射与 VLQ

Sourcemap 最初版本生成的 .map 文件非常大,体积大概为编译产物的 10 倍;V2 引入 base64 编码等算法将之减少 20% ~ 30%;而最新版本 V3 又在 V2 基础上引入 VLQ 等算法,体积进一步压缩了 50%。这一系列进化造就了一个效率极高的 Sourcemap 体系,但伴随而来的则是较为复杂的 mappings 编码规则。

1.2.1 mappings 编码规则

举个例子,对于下面的代码:

Webpack 实战系列一:正确使用 Sourcemap

当 devtool = 'source-map' 时,Webpack 生成的 mappings 字段为:

  1. ;;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E

字段内容包含三层结构:

  • 以 ; 分割的「行映射」,每一个 ; 对应编译产物每一行到源码的映射,上例经过分割后:
  1. [
  2. // 产物第 1-5 行内容为 Webpack 生成的 runtime,不需要记录映射关系
  3. '', '', '', '', '',
  4. // 产物第 6 行的映射信息
  5. 'AAAA,IAAMA,IAAI,GAAG,QAAb',
  6. // 产物第 7 行的映射信息
  7. 'AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E'
  8. ]
  • 以 , 分割的「片段映射」,每一个 , 对应该行中每一个代码片段到源码的映射,上例经过分割后:
  1. [
  2. // 产物第 1-5 行内容为 Webpack 生成的 runtime,不需要记录映射关系
  3. '', '', '', '', '',
  4. // 产物第 6 行的映射信息
  5. [
  6. // 片段 `var` 到 `const` 的映射
  7. 'AAAA',
  8. // 片段 `name` 到 `name` 的映射
  9. 'IAAMA',
  10. // 等等
  11. 'IAAI', 'GAAG', 'QAAb'],
  12. // 产物第 7 行的映射信息
  13. ['AAEAC', 'OAAO', 'CAACC', 'GAAR', 'CAAYF', 'IAAZ', 'E']
  14. ]

第三层逻辑为片段映射到源码的具体位置,以上例 IAAMA 为例:

  • 第一位 I 该代码片段在产物中列数
  • 第二位 A 代表源码文件的索引,即该片段对标到 sources 数组的元素下标
  • 第三位 A 代表片段在源码文件的行数
  • 第四位 M 代表片段在源码文件的列数
  • 第五位 A 代表该片段对应的名称索引,即该片段对标到 names 数组的元素下标

上述第1、2层逻辑比较简单,唯一需要注意的是片段之间是一种相对偏移关系,例如对于上例第六行映射值:AAAA,IAAMA,IAAI,GAAG,QAAb,每一个片段的第一位 —— 即片段列数为 A,I,I,G,Q,分别代表:

  • A :第 A 列
  • I :第 A + I 列
  • I :第 A + I + I 列
  • G :第 A + I + I + G 列
  • Q :第 A + I + I + G + Q 列

这种相对偏移能减少 Sourcemap 产物的体积,提升整体性能。

而第三层的片段位置映射则用到了一种比较高效数值编码算法 —— VLQ(Variable-length Quantity)。

1.2.2 VLQ编码

参考:https://en.wikipedia.org/wiki/Variable-lengsth_quantity

VLQ 本质上是一种将整数数值转换为 Base64 的编码算法,它先将任意大的整数转换为一系列六位字节码,再按 Base64 规则转换为一串可见字符。VLQ 使用六位比特存储一个编码分组,例如:

Webpack 实战系列一:正确使用 Sourcemap

数字 7 经过 VLQ 编码后,结果为 001110,其中:

  • 第一位为连续标志位,标识后续分组是否为同一数字;
  • 第六位表示该数字的正负符号,0为正整数,1为负整数;
  • 中间第 2-5 为实际数值。

这样一个六位编码分组,就可以按照 Base64 的映射规则转换为 ABC 等可见字符,例如上述数字 7 编码结果 001110,等于十进制的 14,按 Base64 字码表可映射为字母 O。

Webpack 实战系列一:正确使用 Sourcemap

但是,分组中只有中间的 4 个字节用于表示数值,因此单个分组只能表达 「-15 ~ 15」 之间的数值范围,对于超过这个范围的整数需要组合多个分组共同表达同一数字,组合规则:

  • 第一个分组的最后一位为符号位,其它分组从 2-6 均为数值位
  • 取二进制值最后四位为第一个分组值,之后从后到前,每 5 位为一个划分为一个分组
  • 除最后一个分组外,其余分组的连续标志位都设置为 1

例如对于十进制 -17,其二进制为 10001 (取 17 的二进制) 共5位,首先从后到前拆分为两组,后四位 0001 为第一组,连续标志位为 1,符号位为 1,结果为 1,0001,1;剩下的 1 分配到第二个 —— 也是最后一个分组,连续标志位为 0,结果为 0,00001。按 Base64 规则 [100011, 000001] 最终映射为 jA。

  1. 十进制 二进制 VLQ Base64
  2. -17 => 1,0001 => 100011, 000001 => jA

同样的,对于更大的数字,例如 1200,其二进制为 10010110000,分组为 [10, 01011, 0000],从后到前编码,第一个分组为 1,0000,0;第二个分组为 1,01011;最后一个分组为 0,00010。按 Base64 映射为 grC。

  1. 十进制 二进制 VLQ Base64
  2. 1200 => 10;01011;0000 => 100000,101011,000010 => grC

Webpack 实战系列一:正确使用 Sourcemap

1.2.3 解码 mappings

结合 VLQ 编码知识,我们再回过来头来解读本章开头的例子,对于代码:

Webpack 实战系列一:正确使用 Sourcemap

编译生成 mappings:

  1. ;;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E

按行、片段规则分割后,得出如下片段:

  1. [
  2. // 产物第 1-5 行内容为 Webpack 生成的 runtime,不需要记录映射关系
  3. '', '', '', '', '',
  4. // 产物第 6 行的映射信息
  5. ['AAAA', 'IAAMA', 'IAAI', 'GAAG', 'QAAb'],
  6. // 产物第 7 行的映射信息
  7. ['AAEAC', 'OAAO', 'CAACC', 'GAAR', 'CAAYF', 'IAAZ', 'E']
  8. ]

以第 6 行 ['AAAA', 'IAAMA', 'IAAI', 'GAAG', 'QAAb'] 为例:

  • AAAA 解码结果为 [000000, 000000, 000000, 000000],即产物第 6 行「第0列」映射到 sources[0] 文件的「第0行」,「第0列」,实际对应 var 到 const 的位置映射
  • IAAMA 解码结果为 [001000, 000000, 000000, 001100, 000000],即产物第 6 行第4列映射到 sources[0] 文件的「第0行」,「第6列」,实际对应产物 name 到源码 name 的位置映射

其它片段以此类推。

二、使用 Sourcemap

Webpack 提供了两种设置 Sourcemap 的方式,一是通过 devtool 配置项设置 Sourcemap 规则短语;二是直接使用 SourceMapDevToolPlugin 或 EvalSourceMapDevToolPlugin 插件深度定制 Sourcemap 的生成逻辑。

下面我们先展开介绍比较晦涩的 devtool 配置项,理解 Webpack 所提供的各种 Sourcemap 功能规则。

2.1 使用devtooldevtool

支持 25 种字符串枚举值,包括 eval、source-map、eval-source-map 等,分开来看都特别晦涩,但仔细观察可发现这些值都是由 inline、eval、source-map、nosources、hidden、cheap、module 七种关键词组合而成,这些关键词各自代表一项 Sourcemap 规则。

2.1.1 eval

当 devtool 值包含 eval 时,生成的模块代码会被包裹进一段 eval 函数中,且模块的 Sourcemap 信息通过 //# sourceURL 直接挂载在模块代码内。例如:

  1. eval("var foo = 'bar'\n\n\n//# sourceURL=webpack:///./src/index.ts?")

eval 模式编译速度通常比较快,但产物中直接包含了 Sourcemap 信息,因此只推荐在开发环境中使用。

2.1.2 source-map

当 devtool 包含 source-map 时,Webpack 才会生成 Sourcemap 内容。例如,对于 devtool = 'source-map',产物会额外生成 .map 文件,形如:

  1. {
  2. "version": 3,
  3. "sources": [
  4. "webpack:///./src/index.ts"
  5. ],
  6. "names": [
  7. "console",
  8. "log"
  9. ],
  10. "mappings": "AACAA,QAAQC,IADI",
  11. "file": "bundle.js",
  12. "sourcesContent": [
  13. "const foo = 'bar';\nconsole.log(foo);"
  14. ],
  15. "sourceRoot": ""
  16. }

实际上,除 eval 之外的其它枚举值都包含该字段。

2.1.3 cheap

当 devtool 包含 cheap 时,生成的 Sourcemap 内容会抛弃「列」维度的信息,这就意味着浏览器只能映射到代码行维度。例如 devtool = 'cheap-source-map' 时,产物:

  1. {
  2. "version": 3,
  3. "file": "bundle.js",
  4. "sources": [
  5. "webpack:///bundle.js"
  6. ],
  7. "sourcesContent": [
  8. "console.log(\"bar\");"
  9. ],
  10. // 带 cheap 效果:
  11. "mappings": "AAAA",
  12. // 不带 cheap 效果:
  13. // "mappings": "AACAA,QAAQC,IADI",
  14. "sourceRoot": ""
  15. }

浏览器映射效果:

Webpack 实战系列一:正确使用 Sourcemap

虽然 Sourcemap 提供的映射功能可精确定位到文件、行、列粒度,但有时在「行」级别已经足够帮助我们达到调试定位的目的,此时可选择使用 cheap 关键字,简化 Sourcemap 内容,减少 Sourcemap 文件体积。

2.1.4 modulemodule

关键字只在 cheap 场景下生效,例如 cheap-module-source-map、eval-cheap-module-source-map。当 devtool 包含 cheap 时,Webpack 根据 module 关键字判断按 loader 联调处理结果作为 source,还是按处理之前的代码作为 source。例如:

Webpack 实战系列一:正确使用 Sourcemap

注意观察上例 sourcesContent 字段,左边 devtool 带 module 关键字,因此此处映射的是包含 class Person 的最原始代码;而右边生成的 sourcesContent 则是经过 babel-loader 编译处理的内容。

2.1.5 nosources

当 devtool 包含 nosources 时,生成的 Sourcemap 内容中不包含源码内容 —— 即 sourcesContent 字段。例如 devtool = 'nosources-source-map' 时,产物:

  1. {
  2. "version": 3,
  3. "sources": [
  4. "webpack:///./src/index.ts"
  5. ],
  6. "names": [
  7. "console",
  8. "log"
  9. ],
  10. "mappings": "AACAA,QAAQC,IADI",
  11. "file": "bundle.js",
  12. "sourceRoot": ""
  13. }

虽然没有带上源码,但 .map 产物中还带有文件名、 mappings 字段、变量名等信息,依然能够帮助开发者定位到代码对应的原始位置,配合 sentry 等工具提供的源码映射功能,可在异地还原诸如错误堆栈之类的信息。

2.1.6 inline

当 devtool 包含 inline 时,Webpack 会将 Sourcemap 内容编码为 Base64 DataURL,直接追加到产物文件中。例如对于 devtool = 'inline-source-map',产物:

  1. console.log("bar");
  2. //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOlsiY29uc29sZSIsImxvZyJdLCJtYXBwaW5ncyI6IkFBQ0FBLFFBQVFDLElBREkiLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgZm9vID0gJ2Jhcic7XG5jb25zb2xlLmxvZyhmb28pOyJdLCJzb3VyY2VSb290IjoiIn0=

inline 模式编译速度较慢,且产物体积非常大,只适合开发环境使用。

2.1.7 hidden

通常情况下,产物中必须携带 //# sourceMappingURL= 指令,浏览器才能正确找到 Sourcemap 文件,

当 devtool 包含 hidden 时,编译产物中不包含 //# sourceMappingURL= 指令。例如:

Webpack 实战系列一:正确使用 Sourcemap

两者区别仅在于编译产物最后一行的 //# sourceMappingURL= 指令,当你需要 Sourcemap 功能,又不希望浏览器 Devtool 工具自动加载时,可使用此选项。你也可以通过以下操作手动打开 Sourcemap:

Webpack 实战系列一:正确使用 Sourcemap

2.1.8 小结

总结一下,Webpack 的 devtool 值都是由以上七种关键字的一个或多个组成,虽然提供了 27 种候选项,但逻辑上都是由上述规则叠加而成,例如:

  • cheap-source-map:代表 「不带列映射」 的 Sourcemap
  • eval-nosources-cheap-source-map:代表 「以」 **eval** 「包裹模块代码」 ,且 **.map** 「映射文件中不带源码」 ,且 「不带列映射」 的 Sourcemap

其它选项以此类推。最后再总结一下:

对于开发环境,适合使用:

  • eval:速度极快,但只能看到原始文件结构,看不到打包前的代码内容
  • cheap-eval-source-map:速度比较快,可以看到打包前的代码内容,但看不到 loader 处理之前的源码
  • cheap-module-eval-source-map:速度比较快,可以看到 loader 处理之前的源码,不过定位不到列级别
  • eval-source-map:初次编译较慢,但定位精度最高

对于生产环境,则适合使用:

  • source-map:信息最完整,但安全性最低,外部用户可轻易获取到压缩、混淆之前的源码,慎重使用
  • hidden-source-map:信息较完整,安全性较低,外部用户获取到 .map 文件地址时依然可以拿到源码
  • nosources-source-map:源码信息确实,但安全性较高,需要配合 Sentry 等工具实现完整的 Sourcemap 映射

2.2 使用插件

上面介绍的 devtool 配置项本质上只是一种方便记忆、使用的规则缩写短语,Sourcemap 的底层处理逻辑实际由 SourceMapDevToolPlugin 与 EvalSourceMapDevToolPlugin 插件实现。

参考:https://webpack.js.org/plugins/source-map-dev-tool-plugin/

在 devtool 基础上,插件还提供了更多更细粒度的配置项,用于满足更复杂的需求场景,包括:

  • 使用 test、include、exclude 配置项设定对那些 bundle 生成 Sourcemap
  • 使用 append、filename、moduleFilenameTemplate、publicPath 配置项设定 Sourcemap 文件的文件名、URL

使用方法与其它插件无异,如:

  1. const webpack = require('webpack');
  2. module.exports = {
  3. // ...
  4. devtool: false,
  5. plugins: [new webpack.SourceMapDevToolPlugin({
  6. exclude: ['vendor.js']
  7. })],
  8. };

插件配置规则较简单,此处不赘述。

三、总结

至此,有关 Sourcemap 的大部分内容就讲解完毕了,读者们需要了解 Sourcemap 是一种高效位置映射算法,它将产物到源码之间的位置关系表达为 mappings 分层设计与 VLQ 编码规则,再通过 Chrome、Safari、VS Code、Sentry 等工具异地还原为接近开发状态的源码形式。

在 Webpack 场景下,通常只需要选择适当的 devtool 短语即可满足大多数场景需求,特殊情况下也可以直接使用 SourceMapDevToolPlugin 做更深度的定制化。

原文链接:https://mp.weixin.qq.com/s/-y35QBSIx2jMvG5dNklcPQ

延伸 · 阅读

精彩推荐
  • 编程技术真正聪明的程序员,总有办法不加班

    真正聪明的程序员,总有办法不加班

    工作效率提升了,就可以少加班了,聪明的程序员,总会有一堆可以提升编码效率的工具?当一种工具满足不了工作需求,就去探索新的,今天纬小创就给...

    今日头条12482021-03-04
  • 编程技术让开发效率倍增的 VS Code 插件

    让开发效率倍增的 VS Code 插件

    今天来分享一些提升开发效率的实用 VS Code 插件!Better Comments 扩展可以帮助我们在代码中创建更人性化的注释,有不同形式和颜色的注释供我们选择。 ...

    前端充电宝7132022-04-21
  • 编程技术Delphi - Indy idMessage和idSMTP实现邮件的发送

    Delphi - Indy idMessage和idSMTP实现邮件的发送

    这篇文章主要介绍了Delphi - Indy idMessage和idSMTP实现邮件的发送,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...

    JJ_JeremyWu6592020-09-22
  • 编程技术用户态 Tcpdump 如何实现抓到内核网络包的?

    用户态 Tcpdump 如何实现抓到内核网络包的?

    在网络包的发送和接收过程中,绝大部分的工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序 tcpdump 是那如何实现抓到内核态的包...

    开发内功修炼11612021-09-08
  • 编程技术简单、好懂的Svelte实现原理

    简单、好懂的Svelte实现原理

    本文会围绕一张流程图和两个Demo讲解,正确的食用方式是用电脑打开本文,跟着流程图、Demo一边看、一边敲、一边学...

    魔术师卡颂4822021-11-10
  • 编程技术2021年值得关注的React PDF 库

    2021年值得关注的React PDF 库

    今天,许多网络应用程序为其用户提供内置的PDF浏览选项。然而,选择一个并不容易,因为它们的功能远远超过显示PDF。在这篇文章中,我将评估5个React的...

    TianTianUp5232021-06-21
  • 编程技术AIOps,SRE工程师手中的利器

    AIOps,SRE工程师手中的利器

    AIOps开始成为一种极为重要的站点可靠性工程工具。它能够高效吸纳观察数据、参与数据以及来自第三方工具的数据,判断系统运行状态并保证其处于最佳...

    至顶网5972021-03-08
  • 编程技术从Context源码实现谈React性能优化

    从Context源码实现谈React性能优化

    这篇文章主要介绍Context的实现原理,源码层面掌握React组件的render时机,从而写出高性能的React组件,源码层面了解shouldComponentUpdate、React.memo、PureComponen...

    魔术师卡颂5312020-12-20