揭秘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',
    })
  ]
}
相关推荐
Jacob程序员2 小时前
leaflet绘制室内平面图
android·开发语言·javascript
eguid_12 小时前
JavaScript图像处理,常用图像边缘检测算法简单介绍说明
javascript·图像处理·算法·计算机视觉
sunly_2 小时前
Flutter:自定义Tab切换,订单列表页tab,tab吸顶
开发语言·javascript·flutter
咔咔库奇3 小时前
【TypeScript】命名空间、模块、声明文件
前端·javascript·typescript
NoneCoder3 小时前
JavaScript系列(42)--路由系统实现详解
开发语言·javascript·网络
又迷茫了3 小时前
vue + element-ui 组件样式缺失导致没有效果
前端·javascript·vue.js
哇哦Q4 小时前
原生HTML集合
前端·javascript·html
SoWhat~4 小时前
随遇随记篇
前端·javascript
爱上大树的小猪4 小时前
【前端SEO】使用Vue.js + Nuxt 框架构建服务端渲染 (SSR) 应用满足SEO需求
前端·javascript·vue.js
w(゚Д゚)w吓洗宝宝了6 小时前
单例模式 - 单例模式的实现与应用
开发语言·javascript·单例模式