一文彻底搞懂webpack devtool

10,631 阅读9分钟

为什么需要Source Map

首先根据谷歌开发者文档的介绍,Source Map一般与下列类型的预处理器搭配使用:

  • 转译器(Babel)
  • 编译器(TypeScript)
  • Minifiers(UglifyJS)

为什么呢?因为通常我们运行在浏览器中的代码是经过处理的,处理后的代码可能与开发时代码相差很远,这就导致开发调试和线上排错变得困难。这时Source Map就登场了,有了它浏览器就可以从转换后的代码直接定位到转换前的代码。在webpack中,可以通过devtool选项来配置Source Map。

配置项

了解了为什么需要Source Map,我们来了了解下webpack能生成哪些类型的Source Map。

从webpack官网上了解到,devtool的值有20+种

听起来很吓人,不过幸好它有固定的模式。

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

通过关键词的组合,就可以生成用于各种场景的Source Map。

理解quality

在学习各种值之前,我们需要先来了解一下表格中的quality列。配置某个属性值,我们是为了达到某个目的,而quality就是目的之一。

那么它是什么呢?它描述了我们在调试时能看到的源码内容

下面来看看它的取值以及含义

quality 含义
bundled code 模块未分离
generated code 模块分离,未经loader处理的代码
transformed code 模块分离,经loader处理过的代码
original source 你自己所写的代码
without source content 生成的Source Map中不包含sourcesContent
(lines only) 包含行信息,不包含列信息

理解devtool

quality就决定了我们调试时能看到的源码内容,所以选取devtool的值时,需要根据项目实际情况配合quality来选择。而devtool有20+个可选值,我们需要进一步来理解其组成原则。

特别提醒:指定devtool时,要与mode配合使用。

首先,上述模式中有三类关键词:

  • inline、hidden、eval
  • nosources
  • cheap[module]

俗话说,实践是检验真理的唯一标准学懂一样东西的最好方式,下面有一个🌰来实践一下。

这里是一个简单的demo,里面有连个文件,其中主文件main.js引入了一个a.js。为了更好的模拟实际场景,还使用了webpack中还使用了babel-loader来处理js文件。

具体如下图:

下面就来实操一波~

inline、hidden、eval

这几个模式是互斥的,描述的是Source Map的引入方式。

inline

Source Map内容通过base64放在js文件中引入。

hidden

代码中没有sourceMappingURL,浏览器不自动引入Source Map。

eval

生成代码和Source Map内容混淆在一起,通过eval输出。

nosources

使用这个关键字的Source Map不包含sourcesContent,调试时只能看到文件信息和行信息,无法看到源码。

cheap[module]

这个关键字用于指定调试信息的完整性

cheap

不包含列信息,并且源码是进过loader处理过的

这里可以看到,点击对应文件时,会跳转到对应文件,但是光标是在第一列(缺少列信息则只定位到第一列),并且箭头函数也被转换成了function。

cheap-module

不包含列信息,源码是开发时的代码

也是只能看到列信息,不过代码是原汁原味的开发时所写的代码。

Source Map是如何工作的

Source Map规范

根据Source Map v3规范,推荐的格式是:

{
    "version" : 3,
    "file": "out.js",
    "sourceRoot": "",
    "sources": ["foo.js", "bar.js"],
    "sourcesContent": [null, null],
    "names": ["src", "maps", "are", "fun"],
    "mappings": "A,AAAB;;ABCDE;"
}

下面来解释一下每个属性代表的含义。

属性 掘力值下限
version Source Map文件版本
file 该Source Map对应文件的名称
sourceRoot 源文件根目录,这个值会加在每个源文件之前
sources 源文件列表,用于mappings
sourcesContent 源代码字符串列表,用于调试时展示源文件,列表每一项对应于sources
names 源文件变量名和属性名,用于mappdings
mappings 位置信息

浏览器与source-map

这里以chrome浏览器为例(其他浏览器应该也是类似的喔~)。

加载source-map

浏览器加载source-map是通过js文件中的sourceMappingRUL来加载的,而且sourceMapping支持两种形式:文件路径或base64格式

加载source-map之后,在浏览器dev tool中的Sources tab就能看到对应的信息了。

这里重点讲解一下map文件中的sources字段和sourcesContent字段。

sources字段对应的是文件信息,会在浏览器的Sources中生成对应目录结构。之后再将sourcesContent中的内容对应填入上述生成的文件中。我们在调试时为啥能看到文件信息和源码内容,就是sources和sourcesContent共同作用的结果。

这里通过一个栗子来看看,我在sources中添加了一个文件hello.js,并且在sourcesContent中对应添加了内容hello~。下面是浏览器加载map文件后的结果。

mappdings详解

直接举个栗子🌰:

源码:input.js
i am handsome  
you are ugly
转换后代码:output.js
i am handsome you are
ugly

我们要如何记录这位置对应信息呢?这里可以参考这篇文章[1]

位置信息必须包含的有:输出位置(Output location)、源文件(Input)、源码位置(Input location)、源码。

所以根据上面的栗子,我们可以得出如下表格。

Output location Input Input location Character
L1,C0 index.js L1, C0 i
L1,C2 index.js L1, C2 a
L1,C3 index.js L1, C3 m
L1,C5 index.js L1, C5 h
L1,C6 index.js L1, C6 a
L1,C7 index.js L1, C7 n
L1,C8 index.js L1, C8 d
L1,C9 index.js L1, C9 s
L1,C10 index.js L1, C10 o
L1,C11 index.js L1, C11 m
L1,C12 index.js L1, C12 e
L1,C14 index.js L2, C0 y
L1,C15 index.js L2, C1 o
L1,C16 index.js L2, C2 u
L1,C18 index.js L2, C4 a
L1,C19 index.js L2, C5 r
L1,C20 index.js L2, C6 e
L2,C0 index.js L2, C8 u
L2,C1 index.js L2, C9 g
L2,C2 index.js L2, C10 l
L2,C3 index.js L2, C11 y

这个表格描述了每个字符的处理前后的对应位置信息,有了这个表格就可以做位置的映射了。

可以看到对于几个字符来说,需要存储的信息就已经很庞大了,所以这种记录方式还需要进行优化。

优化点如下:

  1. 可以把输入文件放入一个列表中,这样在位置信息中就可以使用列表索引来表示了。
  2. 对于源码,没有必要记录每个字符的对应信息,我们只需要记录变量、属性名就可以了(单词),可以使用一个列表来保存单词,位置信息只记录单词首个字符位置即可。
  3. 输出文件的行信息是相对重复的,所以可以使用;来分割每行输出代码,使用,来分割每个输出代码的位置信息。
  4. 源码的每个单词可以使用对于上一个单词的相对位置,这些位置信息(数字)可以更小些。

优化后如下表:

sources: [index.js]
names: [i, am, handsome, you, are, ugly]
Output location Input index Input location Name index
L1,C0 0 L1, C0 0
L1,C+2 +0 L1, C+2 +1
L1,C+3 +0 L1, C+3 +1
L1,C+9 +0 L2, C-5 +1
L1,C+4 +0 L2, C+4 +1
L2,C-18 +0 L2, C+4 +1

此时位置信息可以记录成

mappings: "0|0|1|0|0,2|0|1|2|1,3|0|1|3|1,9|0|2|-5|1,4|0|2|4|1;-18|0|2|4|1"
备注:
1. 数字使用分隔符“|”分割
2. 0|0|1|0|0代表 -> 0(输出单词列), 0(输入文件sources索引),10(输入单词行列),0(单词names索引)

使用VLQ进行优化

在上述mappings中,因为每一个位置不一定是一个数字的,所以必须使用|分隔符来分割数字。如果能省略分隔符,mappings的大小可以得到很大的优化。这时就需要引入VLQ(Variable Length Quantities)了。

VLQ的特点就是可以非常精简地表示很大的数值。其原理是使用6个二进制位来表示字符,其中最高位表示是否连续(0:不连续,1:连续),最低位表示是正还是负(0:正数,1:负数)。

举两个栗子🌰🌰:数字1与-23。

数字:1
二进制:1
VLQ编码:000010
Base64 VLQ: C
数字:-23
绝对值:23
二进制:10111
VLQ编码:101111 000001
Base64 VLQ: vB

这里拿-23来分解一下生成过程。

第一步:去23的二进制码 -> 10111
第二步:将10111分成两部分,第一部分是后四位,后面部分是五位为一部分 -> 1、0111 -> 00001、0111
第三步:按VLQ格式拼接 -> 101111 000001
其中,101111是 1【连续标识位】 + 0111 + 1【正负标识位】
000001是 0【连续标识位】+ 00001
第四步:对照Base64索引表(下表)

最后来看一下上述mappings优化之后长啥样吧。

优化前:
mappings: "0|0|1|0|0,2|0|1|2|1,3|0|1|3|1,9|0|2|-5|1,4|0|2|4|1;-18|0|2|4|1"
优化后
mappings: "AACAA,EACEC,GACGC,SAELC,IAEIC,lBAEIC"

项目中如何使用

通过上面的理论。我们对source-map以及webpack中devtool配置项已经有一些了解了,下面从实际出发,看看在项目不同环境中应该如何配置webpack。

webpack是如何推荐的

先直接来看看webpack官网的devtool[2]推荐:

Development

推荐使用:

  • eval
  • eval-source-map
  • eval-cheap-source-map
  • eval-cheap-module-source-map

在开发环境,我们比较在意的是开发体验,所以下面从源码级别、构建速度和列信息来对比。

devtool 源码级别 构建速度 列信息
eval webpack + loader处理后的代码 +
eval-source-map 源码 +
eval-cheap-source-map loader处理后的代码 -
eval-cheap-module-source-map 源码 -

Production

推荐使用:

  • none
  • source-map
  • hidden-source-map
  • nosources-source-map

在生产环境,我们比较在意的是安全性,所以下面从源码级别、安全性和列信息来对比。

devtool 源码级别 安全性 列信息
none - - -
source-map 源码 浏览器会加载source-map,调试时会暴露源码 +
hidden-source-map 源码 会生成map文件,但浏览器不会加载source-map。可以将map文件与错误上报工具结合使用 +
nosources-source-map 源码堆栈 没有sourcesContent,调试只能看到模块信息和行信息,不能看到源码 -

实际项目是如何配置

翻了一下项目组中的项目,开发环境使用的是eval-cheap-module-source-map,而生产环境这边多大数只需要知道报错的模块和行号就可以了,所以使用的是nosources-source-map

最后


关注「漫步大前端」, 第一时间获取优质文章。

参考资料

[1]

Source Maps under the hood: https://docs.microsoft.com/zh-cn/archive/blogs/davidni/source-maps-under-the-hood-vlq-base64-and-yoda#comment-626

[2]

webpack: https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/28/172f8f3771f18da1~tplv-t2oaga2asx-image.image