揭秘SourceMap & Webpack 中的使用

前端开发者们日常开发、构建、调试、问题排查等场景,离不开非常重要的内容 - sourcemap,本文旨在帮大家理清sourcemap 的概念、生产原理、以及如何高效使用它排查问题,让我们在工程化的路上前进一大步。

更多内容可以查看揭秘 SourceMap

SourceMap 的来源?

Sourcemap 协议 最初由 Google 提出并率先在 Closure Inspector 实现,主要作用是将经过压缩、混淆、合并的产物代码还原到未打包的原始状态,帮助开发者在生产环境中精确定位问题发生的行、列位置。

在前端没有工程化的很早期、比如 jsp/php 作为开发语言 时代、并不需要 SourceMap,但到了后期前端 JavaScript 代码会直接在客户端加载运行、代码体积也越来越复杂,面临着代码泄露、应用性能等问题,工程化支持了文件压缩、代码混淆等能力,这些对用户非常有价值、但也严重影响了开发调试、排查问题、定位代码效率,这也正是 SourceMap 解决的问题。

虽然本文重点介绍 JavaScript SourceMap,但实际上source map 只是位置映射,所以可以用在任何代码上,包括 JavaScript、TypeScript、CSS/Sass/Less 等。

SourceMap 是什么?

SourceMap 是一个信息文件,里面储存着代码的位置信息。

使用场景主要分为两类:

  1. 本地开发调试、利用 sourcemap 快速定位代码;
  2. 线上生产环境、上传 sourcemap 到异常收集平台辅助快速分析线上问题、定位错误代码;

如下图、带浏览器本地调试代码时,根据 sourcemap 定位到原始代码:

至于为什么能定位到原始代码内容、则是因为main.bundle.js 文件中定义了 bundle.js.map文件:

在遇到线上问题/异常时、我们需要排查问题、定位代码,情况类似,只不过线上代码经过合并、压缩和混淆等处理会更困难、sourcemap 也就更重要。

基本概念和流程

主要概念

名词 定义
生成代码(Generated Code) 编译器生成的代码
源代码(Original Source) 未经过编译器处理的原始代码
VLQ(Variable-Length Quantity) 一种通用编码,使用任意数量的二进制八位组(八位字节)来表示任意大的整数
Base64 VLQ 基于VLQ 编码方法、将整数数值转换为 Base64 的编码算法,它先将任意大的整数转换为一系列六位字节码,再按 Base64 规则转换为一串字符(此处先不纠结具体规则、后面单独介绍)
Source Mapping URL 一个 URL、通过它可以引用到生成代码对应的源代码

以本地开发调试场景为例,应用加载生成代码、然后可以从指定的 Source Mapping URL 加载 source map、根据 sourcemap 文件内容映射回源代码:

生成代码内容如下:

根据 sourcemap 映射源代码后展示内容如下:

Chrome 停用SourceMap

除了在 Webpack 配置sourcemap 生成策略外、Chrome DevTools 也支持设置是否启用 SourceMap,点击控制台右上角设置按钮,取消启用 JavaScript source maps:

此时回到控制台、会发现只剩下构建后的生成代码,如果是生产环境、还会是压缩、混淆后的代码:

以掘金生产环境代码示例:

SourceMap 结构

以实际代码为例:

源代码:

javascript 复制代码
const cube = function(x) {
  return x * x * x;
};

console.log(cube(10));

export default cube;

map 文件内容如下:

生成代码:

javascript 复制代码
(()=>{"use strict";const n=function(n){return n*n*n};console.log(n(10))})();
//# sourceMappingURL=main.e472c6c8.js.map
swift 复制代码
{
	"version": 3,
	"file": "static/js/main.e472c6c8.js",
	"mappings": "mBAAA,MAAMA,EAAO,SAASC,GACpB,OAAOA,EAAIA,EAAIA,CACjB,EAEAC,QAAQC,IAAIH,EAAK,I",
	"sources": ["index.js"],
	"sourcesContent": ["const cube = function(x) {\n  return x * x * x;\n};\n\nconsole.log(cube(10));\n\nexport default cube;\n"],
	"names": ["cube","x","console","log"],
	"sourceRoot": "",
}

整个文件是一个 JSON 对象、以下字段分别表示SourceMap 如下信息:

  • version:SourceMap 版本、一个整数、当前版本为 3;
  • file:转换后的文件名;
  • sourceRoot:转换前的文件所在目录,如果与转换前的文件在同一目录,该项为空;
  • sources:转换前的文件,该项是一个数组时,表示可能存在多个文件合并;
  • sourcesContent:可选字符串数组,原始代码内容;
  • names:转换前的所有变量名和属性名;
  • mappings:包含编码映射数据的字符串、记录位置信息;

Base64 VLQ 编码

想要真的理解Mappings,需要掌握VLQ 编码策略,VLQ 是一种编码方式、特点是可以非常精简地表示很大的数值、其长度是可变的,每个VLQ 字符使用 6 个二进制位记录、之后进行转换(使用Base64 编码、参见下面字符表)。

  1. 如果数值在-15 至 15 之间,则用一个字符展示,6 个二进制位含义如下:
  • 第一位(最高位):表示是否"连续"(continuation),如果是1,代表这6个位后的6个位也属于同一个数;如果是0,表示该数到这6个位就结束;

  • 中间 2-5 位:始终用于数值记录,规则遵循二进制逻辑;

  • 第六位(最低位):取决于这6个位是否是某个数值的VLQ 编码的第一个字符;

    • 如果是,则这个位代表"符号"(sign),0为正,1为负;
    • 如果不是,则这个位没有特殊含义,作为数值的一部分计算。
  1. 如果数值超出 15 或-15,则需要多个字符展示、将数值转换为二进制后进行分组、而后分组完成记录、编码过程、得到 VQL 编码值:

    • 分组顺序为从后到前(反转),二进制最后一位后补一个符号位 0/1、而后从后到前、每 5 位划分为一个分组;
    • 最后一个分组连续标志位设置为 0,其余分组连续标志位都设置为 1;
    • 分组剩下空位补 0;
数值 转二进制 VLQ 编码(分组二进制并记录) Base64 转换 VQL 编码值(参照Base64 字符表)
10 1010 0(最高位) 1010 0(最低位) 010100 = 20 ⇒ U U
137 10001001 100010010 ⇒ [10010, 1000] ⇒ [110010, 001000] 110010 = y, 001000 = I yI
1200 10010110000 100101100000 ⇒ [00000, 01011, 10] ⇒ [100000, 101011, 000010] 100000 = g, 101011 = r, 000010 = C grC

Mappings 介绍

mappings 内容表示的信息可以拆分为3个模块:

  1. 行:以;为符号、每个分号前代表一行;
  2. 段:以,为符号、每个逗号前代表一个代码段;
  3. 值:每个段内容可能是长度为1、4、5、6 的一个 Base64 VLQ字符串;

具体到值的解读,以上面 source map 文件mBAAA 为例:

bash 复制代码
// 相当于 Array.split(';'),无分号表示仅一行,所有内容都在第0行展示
[
	"mBAAA,MAAMA,EAAO,SAASC,GACpB,OAAOA,EAAIA,EAAIA,CACjB,EAEAC,QAAQC,IAAIH,EAAK,I"
]
  • mBAAA:解码为[100110, 000001, 000000, 000000, 000000],代表生成代码的第0 行19 列,映射到sources[0]文件中的第0 行0 列,即对应const 到 const 定义的转换:

计算过程

100110 000001(构造分组) ⇒ 00110 00001(去连续符号)⇒ 00110 1(去除 补0) ⇒ 100110(去反转)⇒ 10011 (去尾部正/负 符号) ⇒ 10011 ⇒ 19(计算十进制值)⇒ 19(加偏移值、初始为 0)

基本规则如下:

  • MAAMA:解码为[001100, 000000, 000000, 001100, 000000],代表生成代码的第 0 行25 列,映射到sources[0]文件中的第0 行6 列,即变量 n到 cube 的变换:

计算过程

001100 ⇒ 0110(去符号位 & 去连续位)⇒ 6 (计算十进制值)⇒ 25(加偏移值19)

解码规则

解码计算规则如下:

  • 第一个编码(示例的100110 00001 和 001100):代表生成代码的第几列(行数通过分号 ; 来确定);
  • 第二个编码(2 个例子的000000):代表源码文件的索引,即该片段对标到 sources 数组的元素下标;
  • 第三个编码( 2 个例子的000000):表示转换前源代码的第几行;
  • 第四个编码(000000 和 001100):表示转换前源代码的第几列;
  • 第五个编码(第2个例子的 000000):表示转换前源代码的哪个变量名,通过下标索引names 数组内容,若无、可省略;

SourceMap 工程化

现代的工程构建工具基本都支持 sourcemap,以 Webpack 为例,Webpack 提供了两种设置 Sourcemap 的方式:

  1. 通过 devtool 配置项设置 Sourcemap 规则短语;
  2. 直接使用 SourceMapDevToolPluginEvalSourceMapDevToolPlugin 插件深度定制 Sourcemap 的生成逻辑;

devtool

devtool 支持 25 种字符串枚举值,包括 evalsource-mapeval-source-map 等,主要规则如:

^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

eval

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

source-map

devtool 包含 source-map 时,Webpack 才会生成 sourcemap 内容,如source-map 配置会生成独立的 sourcemap 文件,默认在生成代码文件尾部插入sourceMappingURL 进行关联:

hidden-source-map

也可以指定生成 sourcemap 文件但不进行关联,设置devtool:hidden-source-map即可,需要使用时可以通过浏览器-控制台-右键-add source-map 手动加载,通常线上生成环境可以添加此字段值、以提升代码安全。

inline-source-map

另外可以设置inline-source-map**,**sourcemap会以dataUrl 的形式内联在生成代码文件里:

cheap

sourcemap 处理映射较慢,为了提升构建性能,提供了 cheap-source-map 设置,代码映射只精确到行,可以简化 sourcemap 内容,减小 sourcemap 文件体积。

module

module 关键字只在 cheap 场景下生效,例如 cheap-module-source-mapeval-cheap-module-source-map。Webpack loader 处理代码后都会生成对应 sourcemap,默认 sourcemap 只能关联到bundle 层代码,之前的 sourcemap 并不具备,如果需要调试最初的源码、则需要添加 module 值。

nosources

默认 sourcemap 会直接把源码存储在sourceContent 字段里,可参考前文示例、这会导致文件体积较大。设置nosources-source-map 即可去除该字段。

SourceMapDevToolPlugin

相较于 devtool,SourceMapDevToolPlugin 插件实现了对sourcemap 的更精细控制:

  • 使用 testincludeexclude 配置项过滤需要生成 sourcemap 的 Bundle;
  • 使用 appendfilenamemoduleFilenameTemplatepublicPath 配置项设定sourcemap 文件的文件名、URL;
javascript 复制代码
{
  plugins: [
    new webpack.SourceMapDevToolPlugin({
      append: '\n//# sourceMappingURL=http://xx.byte.com/sourceMap/[url]', // 添加指定服务域名和路径加载 sourcemap 文件
      filename: '[file].map',
    })
  ]
}
相关推荐
it_remember8 分钟前
新建一个reactnative 0.72.0的项目
javascript·react native·react.js
敲代码的小吉米1 小时前
前端上传el-upload、原生input本地文件pdf格式(纯前端预览本地文件不走后端接口)
前端·javascript·pdf·状态模式
da-peng-song2 小时前
ArcGIS Desktop使用入门(二)常用工具条——数据框工具(旋转视图)
开发语言·javascript·arcgis
低代码布道师3 小时前
第五部分:第一节 - Node.js 简介与环境:让 JavaScript 走进厨房
开发语言·javascript·node.js
满怀10153 小时前
【Vue 3全栈实战】从响应式原理到企业级架构设计
前端·javascript·vue.js·vue
伟笑4 小时前
elementUI 循环出来的表单,怎么做表单校验?
前端·javascript·elementui
确实菜,真的爱4 小时前
electron进程通信
前端·javascript·electron
魔术师ID6 小时前
vue 指令
前端·javascript·vue.js
Clown956 小时前
Go语言爬虫系列教程 实战项目JS逆向实现CSDN文章导出教程
javascript·爬虫·golang
星空寻流年7 小时前
css3基于伸缩盒模型生成一个小案例
javascript·css·css3