前言
上周在群里突然被 @
要我查看生产上出现的问题,由于这个项目比较老 (React15
),既没有埋点也没有接入错误监控,于是会得到如下的这样一个提示信息(以下错误是本地模拟的实际生产效果):
当你想要直接点击链接定位时,就会看到如下的形式:
这怎么说呢?不能说一模一样,只能说毫不相关()!表面上
针对老项目再想加入埋点和错误监控可能就比较困难了,特别是当看到其中各种无法让你理解的写法和逻辑,加之不同开发人员的迭代开发,一直在往上堆 x 山(),就更加没法做什么进一步的优化了,只能说能跑就行,不敢动,根本就不敢动!保持优雅,该打码就打码
那么还剩下的能够快速帮助我们定位详细错误信息的方式是什么呢?没错就是本文的主角 sourcemap,这也是为什么会有本篇与 sourcemap 相关的文章。
文中若存在不正确之处,可在评论区斧正!!!
sourcemap 的使用和规则
sourcemap 是什么?
简单的说,就是一个以 .map
为后缀的文件,例如:
而 .map
文件里面的内容以 json
形式存储了 源代码 打包转换后的位置信息,核心内容如下:
- version:版本号,目前
sourcemap
标准的版本为 3 - file:指打包构建后的
文件名
,即bundle
文件名 - sources:指当前这个
bundle
文件所包含的所有源码文件
,因为存在分包等优化策略,一个bundle
文件可能会包含多个源码文件
的内容 - sourcesContent:指上述
sources
中每个源码文件
所对应的源码内容字符 - names:指在代码在经历
混淆压缩
之前的变量名
,这个变量名包含导入模块名
、常用方法名
- mappings:直接进行翻译就是
映射
的意思,即根据以上信息实现的源码代码位置和构建产物之间的一一映射关系 - sourceRoot:指源码目录
sourcemap 有啥用?
上面已经提到了 .map
文件中以 json
形式存储的数据内容,就是包含着源代码与构建产物之间的映射关系,那么它的作用自然就是实现:运行时代码
和 开发时代码
都能拥有相同准确的信息提示。
常见的 开发时代码提示,如下:
常见的 运行时代码提示,如下:
可见 运行时代码提示 的代码提示信息不够详细准确,而且以上只是简单的模拟了运行时错误,而实际项目中大多数的运行时错误是在不同的场景下才会出现的,而此时由于处于生产环境中,在排查异常代码时就会体现出限制:
- 首先,运行时代码 和 开发时代码 不一致,导致错误信息也不同
- 其次,运行时代码 很难通过
debug
的方式进行调试
主要原因就是 开发时代码 到 运行时代码 的转变都需要经历以下几个处理阶段:
- 代码压缩,为了减小运行时代码的体积,会将源代码中的 换行符、无意义空格 等进行删除,使得代码紧凑在一起
- 代码混淆,实际上是指将源代码转换成一种功能上等价,但是难于阅读和理解的形式,例如开发时代码中定义的 "见名知意" 的 函数名、模块名、变量名 等转换为类似 "a、b、c、..." 等无意义的名字,使得即使运行时代码被人获取,也难以猜测其作用
- 代码分块(chunk split),在现代前端构建工具(webpack、vite 等)中都支持将多个源代码文件合并成一个 bundle 文件,目的就是减少 http 请求数量,以实现优化效果
因此,sourmap 就可以在这些处理阶段中构建出 运行时代码 和 开发时代码 代码的映射关系,使得运行时代码也能够像开发时代码一样提供给我们详细而准确的信息,帮助我们在生产环境中也能够快速定位到源代码中的位置。
如何快速生成 sourcemap?
前端构建工具很多,这里只列举最常用的两个:vite 和 webpack
vite 生成 sourcemap
vite
中只要通过设置 build.sourcemap
的选项配置即可,值类型包括:
- boolean:true | false
- 其默认值为 false,当设置为 true 时,就会生成单独的
.map
文件,并且在对应的 bundle 文件中通过 注释 来指明对应的.map
文件,如下:
- 'inline'
- 指定为该值 source map 将作为一个 data URI 附加在输出文件中,如下:
- 'hidden'
'hidden'
的工作原理与'true'
相似,只是 bundle 文件中相应的注释将不被保留
webpack 生成 sourcemap
webpack
中只要通过设置 devtool
的选项配置即可,值类型包括以下类型的 组合:
- (none) 默认值
- eval
- 会生成被
eval
函数包裹的模块内容,并在其中通过注释来注明是源文件位置,其中的sourceUrl
是用来来指定文件名 - 优点就是快,因为不用生成
.map
文件,并且 运行时代码 映射到 开发时代码 只需要提供对应的 源文件地址 - 缺点就是包含映射信息少,并且
eavl
函数因为安全性问题也是不建议使用的
- source-map
- 会生成单独的
.map
文件包含version、file、sources、sourcesContent、names、mappings、sourceRoot
等信息,需要进行 mapping 和 编码 工作 - 优点就是拥有单独的
.map
文件,使得 运行时代码 体积不会过大,并且能够提供详细的信息,包含文件名、行、列等信息 - 缺点就是慢,因为需要额外生成
.map
文件,并且随着模块内容的增多整体速度就越慢
- cheap
- 和
source-map
的方式不同,cheap
只会映射到源码的 行信息,即它 不会生成源码的列信息
,也不包含loader
的sourcemap
,因此相对来说会比source-map
的方式更快 - 优点就是速度更快,只映射到源码的 行信息 的原因是:通常在进行错误定位时,大多数情况下只需要关注到 行 就可以知道错误原因,而很少会关注到 列,因此列信息其实不是必要性的
- 缺点就是映射信息会不够精确,因为一个文件可能会经过不同
loader
的处理,而它又不生成loader
相关的sourcemap
,自然会导致最终产物的信息不够精确
- module
module
的方式生成的sourcemap
就会包含和loader
相关的sourcemap
信息- 需要
loader
相关的sourcemap
信息的原因在于:当一个文件被多个laoder
依次进行转换处理后,其内容会发生不同的变化(例如:新内容的行列 和 源代码的行列 信息不一致
),就会使得我们无法去调试最初始的代码内容
- inline
- 顾名思义,就是会将原本生成的
.map
文件的内容作为 DataURL(base64 形式
) 嵌入bundle
文件中,不单独生成.map
文件
- hidden
- 会生成单独的
.map
文件,但是相比于source-map
的形式,其会在对应的bundle
文件中隐藏sourceMappingURL
的路径
- nosources
- 在
source-map
生成的.map
文件中的sourceContent
存储的是源码内容,这样的好处是既可以根据文件路径来映射,也可以根据这部分内容来映射,换句话说source-map
提供了双重保险,但也增加了.map
文件体积 nosources
则是在能够保证文件路径可以准确建立映射的情况下,就可以把sourceContent
的内容给去除掉,使得.map
文件体积能够更小一些
以上和 webpack
相关的 devtool
的配置内容,eval
、source-map
都可以单独使用,也可以组合使用,但 module、inline、hidden、nosources、cheap
的方式一定是包含 source-map
的内容的,如果你记不住或写错了,webpack
会给你相应的提示信息:
不同环境的 sourcemap 怎么选?
这里的环境指的就是 开发环境 和 生产环境,由于不同的组合方式在 构建 和 重构建 时的速度不同,另外还需要考虑 .map
文件在线上可能带来的风险问题,因此必须要 权衡 使用 sourcemap
的组合方式,好在 webpack
文档中给我们提供给了相应的组合方式,如下:
devtool | performance | production | quality | comment |
(none) | build: fastest rebuild: fastest | yes | bundle | Recommended choice for production builds with maximum performance. |
eval |
build: fast rebuild: fastest | no | generated | Recommended choice for development builds with maximum performance. |
eval-cheap-source-map |
build: ok rebuild: fast | no | transformed | Tradeoff choice for development builds. |
eval-cheap-module-source-map |
build: slow rebuild: fast | no | original lines | Tradeoff choice for development builds. |
eval-source-map |
build: slowest rebuild: ok | no | original | Recommended choice for development builds with high quality SourceMaps. |
cheap-source-map |
build: ok rebuild: slow | no | transformed | |
cheap-module-source-map |
build: slow rebuild: slow | no | original lines | |
source-map |
build: slowest rebuild: slowest | yes | original | Recommended choice for production builds with high quality SourceMaps. |
inline-cheap-source-map |
build: ok rebuild: slow | no | transformed | |
inline-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | |
inline-source-map |
build: slowest rebuild: slowest | no | original | Possible choice when publishing a single file |
eval-nosources-cheap-source-map |
build: ok rebuild: fast | no | transformed | source code not included |
eval-nosources-cheap-module-source-map |
build: slow rebuild: fast | no | original lines | source code not included |
eval-nosources-source-map |
build: slowest rebuild: ok | no | original | source code not included |
inline-nosources-cheap-source-map |
build: ok rebuild: slow | no | transformed | source code not included |
inline-nosources-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | source code not included |
inline-nosources-source-map |
build: slowest rebuild: slowest | no | original | source code not included |
nosources-cheap-source-map |
build: ok rebuild: slow | no | transformed | source code not included |
nosources-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | source code not included |
nosources-source-map |
build: slowest rebuild: slowest | yes | original | source code not included |
hidden-nosources-cheap-source-map |
build: ok rebuild: slow | no | transformed | no reference, source code not included |
hidden-nosources-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | no reference, source code not included |
hidden-nosources-source-map |
build: slowest rebuild: slowest | yes | original | no reference, source code not included |
hidden-cheap-source-map |
build: ok rebuild: slow | no | transformed | no reference |
hidden-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | no reference |
hidden-source-map |
build: slowest rebuild: slowest | yes | original | no reference. Possible choice when using SourceMap only for error reporting purposes. |
sourcemap 如何生效?
要使得 sourcemap
发挥作用,单单只是生成对应的映射规则还不够,还需要一个 解析工具 负责将 源代码
和 sourcemap
规则真正进行映射,通常这个解析工具是 浏览器、异常监控系统(如:sentry) 和 手动映射。
浏览器
通常在现代浏览器中基本上会默认启用 sourcemap
映射功能,即只要对应的 bundle
文件中有 sourceMappingURL
或 sourceURL
等指向的注释内容即可,手动开启位置如下(大同小异):
Google 浏览器:
Firefox 浏览器: