专栏首页前端e进阶前端面试官: 你知道source-map的原理是什么吗?

前端面试官: 你知道source-map的原理是什么吗?


介绍 webpack的source-map原理


为什么要有source-map?

答:因为目前我们开发时候的源码跟通过webpack构建混淆压缩后的生产环境部署的代码不一样,sourceMap就是一个文件,里面储存着位置信息。


source-map,怎么来的 ?它在哪儿?

答:通过webpack等工具,我们可以使用 sourceMap,它跟构建后的文件同在一个目录下~


source-map,它怎么用?

答:在webpack的development模式下,会自动开启source-map

例如: 下面的代码没有声明过log函数,结果调用了

componentDidMount() {        log(this.props); }

最终精准报错,代码所处的文件,还有行数,这是source-map的最重要作用

可是我们的代码经过webpack打包后,即便是development模式,也会被混淆。真正的内存中的代码,已经是下面这种形式了

那么是它是怎么给我们精准报错定位到源文件地址和行数的呢?


我们通过source面板打开了文件,全局搜索了source

此时发现原来开启source-map后的路径映射,是直接保存在文件中的,但是我看到在老版本的webpack中,是以base64的形式保存在文件中的

⚠️:在webpack配置中设置devtool: 'none',那么就会关闭source-map


development模式会自动开启source-map,是为了让我们方便调试,定位错误,但是production模式,会自动关闭

众所周知,代码加密,也是有代价的,会降低代码运行效率,所以前端安全就很难做。特别是Electron、安卓这种客户端,都是可以反编译的,只要有编译,就基本上都有反编译。所以即便是whatsApp这种,也是大部分代码裸奔。当然,还是有很多手段可以提高反编译成本的


下面是source-map各种不同配置的速度

当我在production模式下,加入 devtool:"source-map"选项,打包构建后,代码里多了很多个.map文件,这些个map文件里,就包含了源码路径

这里可以看到,每个文件模块,都有一个对应的.map文件,里面存放着他们的源码路径

sourcemap文件的格式:

{    version : 3, //SourceMap的版本,目前为3    sources: ["***.js,***.js"], //转换前的文件,该项是一个数组,表示可能存在多个文件合并    names: ["***", "***", "**", "***"], //转换前的所有变量名和属性名    mappings: "AACvB,gBAAgB,EAAE;AAClB;", //记录位置信息的字符串    file: "out.js", //转换后的文件名    sourcesContent: " \t// The module cache\n", //转换后的代码    sourceRoot : "" //转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空}

其他的应该都很好解释,重点是mappings

"AACvB,gBAAgB,EAAE;AAClB;"为例:

  • 每个分号对应转换后源码的一行;
  • 每个逗号对应转换后源码的一个位置;
  • AACvB代表该位置转换前的源码位置,以VLQ编码表示;

位置对应的原理

位置关系的保存经历了诸多步骤和优化,这个不详细说了,想看的可以看这里,我们只说最后的结果。

在每个位置中:

  • 第一位,表示这个位置在【转换后代码】的第几列。
  • 第二位,表示这个位置属于【sources属性】中的哪一个文件。
  • 第三位,表示这个位置属于【转换前代码】的第几行。
  • 第四位,表示这个位置属于【转换前代码】的第几列。
  • 第五位,表示这个位置属于【names属性】的哪一个变量。

举例

假设现在有a.js,内容为feel the force,处理后为b.js,内容为the force feel

the为例,它在输出中的位置是(0,0),a.jssources的第1个(这里只是举例),输入中的位置是(0,5),thenames的第2个(这里只是举例)。

那么映射关系为: 0 1 0 5 2

最后将 01052 表示为 Base64 VLQ 即可。

说明:

  • 所有的值都是以0作为基数
  • 第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位
  • 每一位都采用VLQ编码表示,由于VLQ编码是可变长的,所以每一位可以由多个字符构成
  • 为什么不保存转换后代码的行号,因为我们输出的文件总是一行,这样输出的行号就可以省略,因为都是0,没必要写出来
  • 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述

相对位置是啥呢,看示意图:

第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the的输出位置为(0,-10),因为thefeel的左边数10下才能到这个位置。

VLQ编码

VLQVariable-length quantity 的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式。这种编码最早用于MIDI文件,后来被多种格式采用,它的特点就是可以非常精简地表示很大的数值,用来节省空间。

这种编码需要用最高位表示连续性,如果是1,代表这组字节后面的一组字节也属于同一个数;如果是0,表示该数值到这就结束了。

这样干巴巴说不太容易懂,还是举个栗子说明一下吧。

如何对数值137进行VLQ编码:

步骤

结果

将137改写成二进制形式

10001001

七位一组做分组,不足的补0

0000001 0001001

最后一组开头补0,其余补1

10000001 00001001

所以,137的VLQ编码形式为10000001 00001001

Base64 VLQ

与一般的VLQ的区别:

  • 一个Base64字符只能表示 6bit(2^6)的数据
  • Base64 VLQ需要能够表示负数,于是用最后一位来作为符号标志位。
  • 由于只能用6位进行存储,而第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了。

表示正负的方式:

  • 如果这组数是某个数值的VLQ编码的第一组字节,那它的最后一位代表"符号",0为正,1为负;
  • 如果不是,这个位没有特殊含义,被算作数值的一部分。

我们再来举个栗子说明下使用方法。

如何对数值137进行Base64 VLQ编码:

步骤

结果

将137改写成二进制形式

10001001

127是正数,末位补0

100010010

五位一组做分组,不足的补0

01000 10010

将组倒序排序

10010 01000

最后一组开头补0,其余补1

110010 001000

转64进制

y和I

所以 137 通过Base64 VLQ表示为yl

可以看出:

  • Base64 VLQ中,编码顺序是从低位到高位
  • 而在VLQ中,编码顺序是从高位到低位

对于做前端异常监控来说,source-map是很有必要的,但是对于性能要求极高的项目,那么可能还是要自己去实现一套独特的监控方式。

部分内容来自参考文章:

https://segmentfault.com/a/1190000020213957

本文分享自微信公众号 - 前端e进阶(gh_ffbd834383c0)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [第21期] 全面了解 React Suspense 和 Hooks

    有些朋友可能会想, 数据早点获取回来,页面就能快点渲染出来呀, 提升用户体验, 何乐而为不为?

    用户6900878
  • [第36期]手把手教你写几个实用的AST插件

    代码地址:https://github.com/reactjs/react-codemod

    用户6900878
  • [第14期] [长文预警] 掌握React 渲染原理及性能优化

    我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。

    用户6900878
  • vuejs之v-for

    西西嘛呦
  • Hibernate之关联关系映射(一对一主键映射和一对一外键映射)

    1:Hibernate的关联关系映射的一对一外键映射:   1.1:第一首先引包,省略   1.2:第二创建实体类:     这里使用用户信息和身份证信息的关系...

    别先生
  • 交易Transaction【区块链生存训练】

    日常生活中,我们每天都会与他人进行各种交易,对于“交易”这个概念感觉再熟悉不过了。比如:今天我去吃凉皮,支付给商家5元钱,非常简单吧,通常的交易记录可以是这样的...

    申龙斌
  • Netty源码解析—客户端启动

    我们再回到init(Channel channel)方法,初始化Channel配置

    luozhiyun
  • HLS中循环的并行性(1)

    Vitis HLS尽可能地探测代码中的并行性,以降低Latency。但对于for循环,即使两个for循环是相互独立、毫无关联的,在默认情形下,工具也不会对其进行...

    Lauren的FPGA
  • LINUX下gdb无法debug,提示ImportError: No module named 'libstdcxx'

    Ubuntu下使用gdb调试C++程序,提示:ImportError: No module named ‘libstdcxx’。貌似CentOS没有这样的问题。

    卡尔曼和玻尔兹曼谁曼
  • 从一个多层嵌套循环中直接跳出

    Java中如何从一个多层嵌套循环中退出,例如下面,有两个循环,break只能退出一个for循环,不能直接跳过第二个for循环

    硬核编程

扫码关注云+社区

领取腾讯云代金券