Source Map知多少?Golang手写SourceMap转换过程

news2024/9/21 5:28:55

文章目录

      • 一、问题背景
      • 二、Source Map 简介
        • 基本格式
        • 应用场景
      • 三、Source Map 的工作原理
      • 四、Source Map 的转换过程
        • 代码示例
        • 总结

本文从原理的角度入手对 Source Map 进行了较为深入的分析,并从业务需要的角度出发,手动编写根据 Source Map 映射编码前后代码行数的功能,示例语言为 Golang

一、问题背景

由于线上实际运行的生产代码是经过编译的转换代码,笔者需要对生产代码和源代码的日志行进行映射,帮助开发者更方便快捷地定位问题

在JS中一般通过 Source Map 进行生产代码和源代码的映射,抱着知其所以然的目的,笔者对 Source Map 的前世今生进行了了解,并一步步理解了 Source Map 的转换原理

二、Source Map 简介

随着 JavaScript 代码的日益复杂,越来越多的源码(包括函数库、框架等)需要经过转换/打包才能投入生产,一般而言转换的目的分为以下三种:

(1)压缩代码,降低存储和传输成本

(2)多文件合并,理由同上

(3)其他语言编译为 JavaScript

转换固然解决了以上问题,但由于线上代码是转换后的代码,一旦程序报错时调用栈中的信息也会是转换后的代码,增加了需要 debug 的开发者的负担

为了能够还原经过转换的代码,Source Map 应运而生

在这里插入图片描述

基本格式

Source Map 是一个存储代码转换前后位置对应关系的 JSON 格式文件,调试工具可以通过 Map 文件还原出转换后代码在转换前的行列信息,其基本格式如下:

{
  version : 3,
  file: "output.js",
  sourceRoot : "",
  sources: ["input_a.js", "input_b.js"],
  names: ["src", "maps", "are", "fun"],
  mappings: "AAgBC,SAAQ,CAAEA",
    sourcesContent: []
}
  • version: Source Map 版本

  • file:输出文件名

  • sourceRoot:输入文件所在目录

  • sources:输入文件名,可以有多个

  • names:转换前的变量&属性名集合

  • mappings:经过 Base64 VLQ 编码的字符串,记录位置映射关系

  • sourcesContent:转换前文件的内容

其中与转换过程比较相关的是 mappings 字段,在第三小节会重点解释

应用场景

一般可以使用前端打包工具如 Webpack 等生成 Source Map 文件,如使用 Webpack 可以通过在 webpack.config.js 中加入如下代码来生成:

在这里插入图片描述
编译后除了源码文件外会同时生成 Map 文件:

在这里插入图片描述

开发者拿到 Source Map 文件后一般不会手动处理,但浏览器得到 Source Map 文件后就可以对文件进行转换了,以 Chrome 为例,打开如下配置后浏览器会自动下载 Map 文件并对文件进行转换,方便开发者进行排错

在这里插入图片描述

即便调试工具帮忙完成了转换工作,但了解 Source Map 的工作原理也可以帮助我们更好的 debug

三、Source Map 的工作原理

Source Map 的目的是对转换前后的代码进行映射,那么流程可以抽象为一次文本转换输出

“feel the force” ⇒ 转换器 ⇒ “the force feel”

Map 文件要保存的核心是输入字符串的 n 行 m 列,对应于输出字符串的 n1 行 m1 列,如首字符 f 的映射关系为 f(0,0)=>(0,10)

通过这种简单的对应关系确实可以还原出源文件,但映射表中要存储的还包括输入文件名(因为不止一个源文件)等信息,会导致 Map 文件占用很大,也会损失传输性能

为节约存储空间,Source Map对于映射关系进行了编码上的优化,包括不以字符而以单词作为最小单位(单词集合存储在 Map 文件的 names 字段里)、以 index 取代源文件名(源文件序列放在 Map 文件的 srouces字段里)、以相对位置记录行数等

因为篇幅原因,更加详细的编码优化流程可以参考这篇文章

下面以一段转换前和一段转换后的代码实例讲解 Source Map 是如何进行映射的

// 转换前
function component() {
    const element = document.createElement('div');
    var x = 1;
    console.log("x: ", x);
    return element;
}
document.body.appendChild(component());
// 转换后
document.body.appendChild(function(){const e=document.createElement("div");return console.log("x: ",1),e}());
//# sourceMappingURL=main.js.map

两者对应的 Map 文件如下

{  
   "version":3,
   "file":"main.js",
"mappings":"AAOEA,SAASC,KAAKC,YAPhB,WACI,MAAMC,EAAUH,SAASI,cAAc,OAGvC,OADAC,QAAQC,IAAI,MADJ,GAEDH,CACT,CAE0BI",
   "sources":["webpack://webpack-demo/./src/index.js"],
   "sourcesContent":["function component() {\n    const element = document.createElement('div');\n    var x = 1;\n    console.log(\"x: \", x);\n    return element;\n  }\n  \n  document.body.appendChild(component());"],
    "names":["document","body","appendChild","element","createElement","console","log","component"],
    "sourceRoot":""
}

Map 文件中最重要的 mappings 字段,看似是一串无意义的字符,实际上存储了两个文件中的所有映射关系,其含义如下

首先,mappings 字段分为三层

第一层是行对应,以分号(; )表示,每个分号对应转换后源码的一行,第一个分号前的内容对应第一行

第二层是位置对应,以逗号(, )表示,每个逗号对应转换后源码的一个位置

第三层是位置转换,以 Base64 VLQ 编码表示,代表该位置对应的转换前的源码位

按照如上规则,以下字符串里没有出现分号(😉,是因为转换后的源码只有一行,而逗号(,) 则将转换后源码分割为 n 个位置

AAOEA,SAASC,KAAKC,YAPhB,WACI,MAAMC,EAAUH,SAASI,cAAc,OAGvC,OADAC,QAAQC,IAAI,MADJ,GAEDH,CACT,CAE0BI

接下来的部分比较难理解,在经过逗号分割以后,每个位置都由至多五个字符(五位不一定都有)组成,每个位置字符的含义如下:

  • 第一位,表示这个位置在(转换后的代码的)的第几列

  • 第二位,表示这个位置属于 sources 属性中的哪一个文件

  • 第三位,表示这个位置属于转换前代码的第几行

  • 第四位,表示这个位置属于转换前代码的第几列

  • 第五位,表示这个位置属于 names 属性中的哪一个变量

以第一组字符 “AAOEA” 为例,第一个 A 表示这个位置在转换后代码的第 A 列,这样可以确定是哪一个单词;第二个 A 代表这个位置在 sources 数组中的 index;第三个 O 代表这个位置属于转换前代码的第 O 行;第四个 E 代表这个位置属于转换前代码的第 E 列,第五个 A 代表在 names 数组中的 index

这五个字符实际都代表着一个数字,将字符映射为数字需要再经过一层 Base64 VLQ 编码的转换(编码原理可以参考这篇介绍),下一小节的代码中也会有所涉及

四、Source Map 的转换过程

如果从日常使用的角度出发,了解到工作原理这一层已经可以覆盖大多数场景了,但实际写下这段代码时有诸多在Source Map 的科普文中未曾点明和存在误解的点

代码示例

转换代码可以大致分为两个步骤,其一是处理行列的对应关系,其二是对 Base64 VLQ 编码的转换

第一步,Base64 VLQ 编码规则这篇文章有详细的解释,总结下来从 0-63 的数字分别可以用一个字符编码
在这里插入图片描述

初始化时可以将字符与数字的对应关系存入一个 map,实现方法如下

// 代码仅做逻辑展示用途
func CreateBase64Digit() {
	BASE64_DIGITS := []rune{}
	charCode := []rune("A")[0]
	for i := 0; i < 26; i++ {
		BASE64_DIGITS = append(BASE64_DIGITS, rune(int(charCode)+i))
	}
	charCode = []rune("a")[0]
	for i := 0; i < 26; i++ {
		BASE64_DIGITS = append(BASE64_DIGITS, rune(int(charCode)+i))
	}
	charCode = []rune("0")[0]
	for i := 0; i < 10; i++ {
		BASE64_DIGITS = append(BASE64_DIGITS, rune(int(charCode)+i))
	}
	BASE64_DIGITS = append(BASE64_DIGITS, rune('+'))
	BASE64_DIGITS = append(BASE64_DIGITS, rune('/'))
	BASE64_DIGITS_MAP = make(map[rune]int)
	for i := 0; i < len(BASE64_DIGITS); i++ {
		BASE64_DIGITS_MAP[BASE64_DIGITS[i]] = i
	}
}

第二步是处理行数对应关系,代码的输入是转换后的行数,输出是转换前对应的行数

注:这里处理的是一种特殊情况,即输入输出都仅有一个文件,且不考虑列数

const (
    VLQ_BASE_SHIFT = 5
    VLQ_BASE = 1 << VLQ_BASE_SHIFT
    VLQ_BASE_MASK = VLQ_BASE - 1
    VLQ_CONTINUATION_BIT = VLQ_BASE
)

result := [][][]int{}
var segmentsInLine [][]int
var numbersInSegment []int
var shift = 0
var continuation int
if patternList := strings.Split(mapping, ";"); len(patternList) >= line { // 分割行
	for j := 0; j < len(patternList); j++ {
		segmentsInLine = [][]int{}
		for _, segment := range strings.Split(patternList[j], ",") { // 分割列
			resultValue := 0
			numbersInSegment = []int{}
			for _, c := range []rune(segment) {
				if digit, ok := BASE64_DIGITS_MAP[c]; ok { // 处理Base64-VLQ编码, 转换为数字
					continuation = digit & VLQ_CONTINUATION_BIT
					digit &= VLQ_BASE_MASK
					resultValue += digit << shift
					shift += VLQ_BASE_SHIFT
					if continuation == 0 {
						negate := (resultValue & 1) == 1
						resultValue >>= 1
						if negate {
							numbersInSegment = append(numbersInSegment, -resultValue)
						} else {
							numbersInSegment = append(numbersInSegment, resultValue)
						}
						resultValue = 0
						shift = 0
					}
				} else {
					return -1
				}
			}
			segmentsInLine = append(segmentsInLine, numbersInSegment)
		}
		result = append(result, segmentsInLine)
	}
	// 根据转换结果确定实际代码位置
	beforeLine := 
	for t := 0; t < line; t++ { // 遍历所有行直到指定行数
		if len(result[t]) > 0 {
			for l := range result[t] {
				if len(result[t][l]) > 2 { // 找到第三个字符(代表行数),并进行累加
				      beforeLine += result[t][l][2]
				}
			}
		}
	}
}

这段代码里需要特殊注意的地方是下面这段:

beforeLine := 0
for t := 0; t < line; t++ { // 遍历所有行直到指定行数
	if len(result[t]) > 0 {
		for l := range result[t] {
			if len(result[t][l]) > 2 { // 找到第三个字符(代表行数),并进行累加
				beforeLine += result[t][l][2]
			}
		}
	}
}

result 数组是以行、列、字符的维度存储转后的数字的数组,上述代码的逻辑是,如果想要找到转换后第 n 行代码对应于转换前第几行代码,需要遍历第0行到第n-1行的所有位置,并且累加其行数,才能得到最终的对应行数

这是由于为了缩减表示位数,所有行数都是相对位置,因此需要对前面的行数进行累加,这一点基本没有文章说明,更完整的计算方法可以直接阅读官方的英文文档

总结

Source Map 的作用不仅是方便开发者 debug 代码,在日益复杂的生产环境下,其也可以用来还原原始代码的堆栈信息等,有很广泛的应用场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/21923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot集成Mybatis项目实操

本文为《从零打造项目》系列第三篇文章&#xff0c;首发于个人网站。 《从零打造项目》系列文章 比MyBatis Generator更强大的代码生成器 SpringBoot项目基础设施搭建 前言 基于 orm-generate 项目可以实现项目模板代码&#xff0c;集成了三种 ORM 方式&#xff1a;Mybatis、M…

35m预应力简支梁桥毕业设计 课程设计-桥梁工程(计算书、8张CAD图)

35m预应力简支梁桥毕业设计 目 录 1、引言 1 2、桥型方案比选 2 2&#xff0e;1 桥梁设计原则 2 2.2方案一&#xff1a;25m预应力钢筋混凝土T梁桥 2 2.3方案二&#xff1a;25m预应力钢筋混凝土小箱梁 4 2.4桥墩方案比选 4 3、上部结构设计计算 5 3&#xff0e;1 设计资料及构造…

考研数据结构填空题整合

考研数据结构填空题整合 目录考研数据结构填空题整合一、ZYL组ZYL组一ZYL组二ZYL组三ZYL组四ZYL组五ZYL组六ZYL组七ZYL组八二、TJP组TJP组一TJP组二TJP组三三、LZH组LZH 组一LZH 组二LZH 组三LZH 组四LZH 组五LZH 组六LZH 组七四、LB组LB组一LB组二LB组三LB组四LB组五LB组六LB组…

FPGA实现精简版UDP通信,占资源很少但很稳定,提供2套工程源码

目录1.高端、中等和精简版UDP通信的选择2.精简版UDP通信实现方案3.工程1介绍及资源占用率和性能表现4.工程2介绍及资源占用率和性能表现5.上板调试验证6.福利&#xff1a;工程代码的获取1.高端、中等和精简版UDP通信的选择 FPGA实现UDP协议可难可易&#xff0c;具体根据项目需…

Python 函数转命令行界面库 -- Argsense CLI

argsense 是一个 python 命令行界面库, 是 click, fire, typer 之外的又一个选项. argsense 最大的特点是极低的侵入性设计和近乎零成本的上手难度, 如果你熟悉 python 函数是如何传参的 (这是大部分 python 初学者已经掌握的知识), 那么你就可以很快上手 argsense. 特性一览 …

大数据(9e)图解Flink窗口

文章目录1、代码模板1.1、pom.xml1.2、log4j.properties1.3、Java模板2、按键分区&#xff08;Keyed&#xff09;、非按键分区&#xff08;Non-Keyed&#xff09;2.1、Keyed2.2、Non-Keyed3、窗口的分类3.1、基于时间的窗口3.2、基于事件个数的窗口4、窗口函数5、示例代码5.1、…

TIA博途_水处理项目中开启累计运行时间最短的泵_程序示例

TIA博途_水处理项目中开启累计运行时间最短的泵_程序示例 需求: 有N台水泵,每个水泵统计累计运行时间。当满足条件时,根据设定开泵的数量,启动累计运行时间最短的对应数量的泵。故障切换时,也切换到运行时间最短的泵。 具体方法可参考以下内容: 如下图所示,打开TIA博途后…

【毕业设计】62-基于单片机的防酒驾\酒精浓度检测系统设计研究(原理图、源代码、仿真工程、低重复率参考设计、PPT)

【毕业设计】62-基于单片机的防酒驾\酒精浓度检测系统设计研究&#xff08;原理图、源代码、仿真工程、低重复率参考设计、PPT&#xff09;[toc] 资料下载链接 资料下载链接 资料链接&#xff1a;https://www.cirmall.com/circuit/33758/ 包含此题目毕业设计全套资料&#xf…

国科大课程自动评价脚本JS

国科大课程一键评估 操作流程&#xff1a; 方法 打开F12点击console/控制台复制粘贴下面代码回车 for(var i 0; i<1000; i) { if($("input[nameitem_"i"]").length) $("input[nameitem_"i"]").get(Math.round(Math.random()*2)…

C++11--lambda表达式--包装器--bind--1119

1.lambda表达式 lambda表达式书写格式&#xff1a;[捕捉列表] (参数列表) mutable -> 返回值类型 { 比较的方法 } int func() {int a, b, c, d, e;a b c d e 1;// 全部传值捕捉auto f1 []() {cout << a << b << c << d << e << …

BLE学习(3):ATT和GATT详解

本文章将介绍在面向连接的蓝牙模式中&#xff0c;ATT(attribute protocol,属性协议)和GATT(generic attribute profile,通用属性配置文件)这两个重要的协议层&#xff0c;它与蓝牙的数据传输密切相关。 1 设备之间如何建立连接(Gap层) 若BLE设备之间要进行数据传输&#xff0…

Qt5 QML TreeView currentIndex当前选中项的一些问题

0.前言 Qt5 QML Controls1.4 中的 TreeView 存在诸多问题&#xff0c;比如节点连接的虚线不好实现&#xff0c;currentIndex 的设置和 changed 信号的触发等。我想主要的原因在于 TreeView 是派生自 BasicTableView&#xff0c;而 TableView 内部又是由 ListView 实现的。 正…

二、openCV+TensorFlow入门

目录一、openCV入门1 - 简单图片操作2 - 像素操作二、TensorFlow入门1 - TensorFlow常量变量2 - TensorFlow运算本质3 - TensorFlow四则运算4 - tensorflow矩阵基础5 - numpy矩阵6 - matplotlib绘图三、神经网络逼近股票收盘均价&#xff08;案例&#xff09;1 - 绘制15天股票K…

编译原理 x - 练习题

简答题逆波兰后缀表达式和三元式序列源程序翻译成中间代码DAG优化正则文法 构造正则表达式正规式 改 上下文无关文法表示DFA有限状态机图移进-规约消除左递归文法-最左推导-短语LL(1)文法LR(0) | SLR(1)文法简答题 编译过程可分为前端和后端&#xff0c;描述一下前端和后端分别…

【设计模式】装饰者模式:以造梦西游的例子讲解一下装饰者模式,这也是你的童年吗?

文章目录1 概述1.1 问题1.2 定义1.3 结构1.4 类图2 例子2.1 代码2.2 效果图3 优点及适用场景3.1 优点3.2 适用场景1 概述 1.1 问题 众所周知&#xff0c;造梦西游3有四个角色&#xff0c;也就是师徒四人&#xff0c;这师徒四人每个人都有自己专属的武器和装备。假定我们以及设…

推荐10个Vue 3.0开发的开源前端项目

Vue 是一款用于构建用户界面的 JavaScript 框,它基于标准 的HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,用以帮助开发者高效地开发用户界面。目前,Vue 3.0正式版也发布了两年的时间,越累越多的开发者也用上了Vue 3.0。 对比Vue2.x,Vue 3.0在…

并发bug之源(二)-有序性

什么是有序性&#xff1f; 简单来说&#xff0c;假设你写了下面的程序&#xff1a; java int a 1; int b 2; System.out.println(a); System.out.println(b);但经过编译器/CPU优化&#xff08;指令重排序&#xff0c;和编程语言无关&#xff09;后可能就变成了这样&#x…

【DDR3 控制器设计】(7)DDR3 的用户端口读写模块设计

写在前面 本系列为 DDR3 控制器设计总结&#xff0c;此系列包含 DDR3 控制器相关设计&#xff1a;认识 MIG、初始化、读写操作、FIFO 接口等。通过此系列的学习可以加深对 DDR3 读写时序的理解以及 FIFO 接口设计等&#xff0c;附上汇总博客直达链接。 【DDR3 控制器设计】系列…

CSS---复合选择器

目录 一&#xff1a;复合选择器的介绍 二、复合选择器的讲解 &#xff08;1&#xff09;后代选择器 &#xff08;2&#xff09;子元素选择器 &#xff08;3&#xff09;并集选择器 &#xff08;4&#xff09;链接伪类选择器 &#xff08;5&#xff09;focus伪类选择器 一&…

基于SpringBoot的线上买菜系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SpringBoot 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目…