SouceceMap的原理

什么是 sourcemap

sourcemap 是关联编译后的代码和源码的,通过一个个行列号的映射。 比如编译后代码的第 3 行第 4 列,对应着源码里的第 8 行第 5 列这种,这叫做一个 mapping。 sourcemap 的格式如下:

javascript 复制代码
{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["a", "b"],
    mappings: "AAgBC,SAAQ,CAAEA;AAAEA",
      sourcesContent: ['const a = 1; console.log(a)', 'const b = 2; console.log(b)']
}
  • version:sourcemap 的版本,一般为 3
  • file:编译后的文件名
  • sourceRoot:源码根目录
  • names:转换前的变量名
  • sources:源码文件名
  • sourcesContent:每个 sources 对应的源码的内容
  • mappings:一个个位置映射

因为可能编译产物是多个源文件合并的,比如打包,一个 bundle.js 就对应了 n 个 sources 源文件。 重点是 mappings 部分: mappings 部分是通过分号; 和逗号 , 分隔的:

javascript 复制代码
mappings:"AAAAA,BBBBB;CCCCC"

一个分号就代表一行,这样就免去了行的映射。 然后每一行可能有多个位置的映射,用 , 分隔。 那具体的每一个 mapping 都是啥呢? 比如 AAAAA 一共五位,分别有不同的含义:

  • 转换后代码的第几列(行数通过分号 ; 来确定)
  • 转换前的哪个源码文件,保存在 sources 里的,这里通过下标索引
  • 转换前的源码的第几行
  • 转换前的源码的第几列
  • 转换前的源码的哪个变量名,保存在 names 里的,这里通过下标索引

然后经过编码之后,就成了 AAAAA 这种,这种编码方式叫做 VLQ 编码。 sourcemap 的格式还是很容易理解的,就是一一映射编译后代码的位置和源码的位置。 各种调试工具一般都支持 sourcemap 的解析,只要在文件末尾加上这样一行:

javascript 复制代码
//# sourceMappingURL=/path/to/source.js.map

运行时就会关联到源码: 除了调试的时候会使用 sourcemap,线上报错定位源码也需要用到: 开发时会使用 sourcemap 来调试,但是生产可不会,但是线上报错的时候确实也需要定位到源码,这种情况一般都是单独上传 sourcemap 到错误收集平台。 比如 sentry 就提供了一个 @sentry/webpack-plugin 支持在打包完成后把 sourcemap 自动上传到 sentry 后台,然后把本地 sourcemap 删掉。还提供了 @sentry/cli 让用户可以手动上传。 平时我们至少在这两个场景(开发时调试源码,生产时定位错误的源码位置 )下会用到 sourcemap。 sourcemap 只是位置的映射,可以用在任何代码上,比如 JS、TS、CSS 等,而且 TS 的类型也支持 sourcemap: 指定了 declaration 会生成 d.ts 的声明文件,还可以指定 declarationMap 来生成 sourcemap 这样在 VSCode 里我们就可以直接点击某个类型来跳转到源码里对应的地方了。 这也算 sourcemap 应用的另一个场景,用于生成的类型和源码中定义的关联。 知道了什么是 sourcemap,那 sourcemap 是怎么生成的呢?

sourcemap 的生成

编译工具在生成代码的时候也会生成 sourcemap: 其实 sourcemap 就是由一个个位置的映射组成的,关键就是要知道源码的哪个位置对应到了编译后代码的哪个位置:

通过 astexplorer.net 可以看到,AST 中保留了源码中的位置,这是 parser 在 parse 源码的时候记录的。 然后进行 AST 的各种转换之后会打印成目标代码,打印的时候是一行行一列列的拼接字符串,这时候就有了目标代码中的位置。

这两个位置一关联,那不就是一个 mapping 么? 这样就生成了 sourcemap。 当然 sourcemap 有对应的格式和编码,自己生成还是挺麻烦的,我们会用 source-map 这个包: source-map 可以用于生成和解析 sourcemap,它暴露了 SourceMapConsumer、SourceMapGenerator、SourceNode 3个类,分别用于消费 sourcemap、生成 sourcemap、创建源码节点。 生成 sourcemap 的流程是:

  1. 创建一个 SourceMapGenerator 对象
  2. 通过 addMapping 方法添加一个映射
  3. 通过 toString 转为 sourcemap 字符串
javascript 复制代码
const { SourceMapGenerator } = require('source-map');

const map = new SourceMapGenerator({
    file: "source-mapped.js"
});
  
map.addMapping({
    generated: {
        line: 10,
        column: 35
    },
    source: "foo.js",
    original: {
        line: 33,
        column: 2
    },
    name: "christopher"
});
  
console.log(map.toString());

消费 sourcemap 用 SourceMapConsumer 的 api。 可以调用 originalPositionFor 和 generatedPositionFor 分别用目标代码位置查源码位置和用源码位置查目标代码位置 还可以通过 eachMapping 遍历所有 mapping,对每个进行处理。

javascript 复制代码
const { SourceMapConsumer } = require('source-map');

const rawSourceMap = {
    version: 3,
    file: "min.js",
    names: ["bar", "baz", "n"],
    sources: ["one.js", "two.js"],
    sourceRoot: "http://example.com/www/js/",
    mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA"
};

(async function() {
    await SourceMapConsumer.with(rawSourceMap, null, consumer => {
        // 目标代码位置查询源码位置
        consumer.originalPositionFor({
            line: 2,
            column: 28
        })
        // { source: 'http://example.com/www/js/two.js',
        //   line: 2,
        //   column: 10,
        //   name: 'n' }
    
        // 源码位置查询目标代码位置
        consumer.generatedPositionFor({
            source: "http://example.com/www/js/two.js",
            line: 2,
            column: 10
        })
        // { line: 2, column: 28 }
    
        // 遍历 mapping
        consumer.eachMapping(function(m) {
            console.log(m);
        });    
    });
})();

这些 api 还是很容易理解的。 知道了位置从哪里来,知道了怎么用 source-map 的包生成 sourcemap,那就知道了平时我们用的 sourcemap 是怎么来的了。

webpack的sourcemap配置

想彻底掌握 sourcemap,还要搞懂 webpack 的 sourcemap 配置。 webpack 的 sourcemap 配置是比较麻烦的,比如这两个配置的区别:

  • eval-nosources-cheap-module-source-map
  • hidden-source-map

是不是分不清楚? 其实它是有规律的。 你把配置写错的时候,webpack 会提示你一个正则:

javascript 复制代码
^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

这个就是配置的规律,是几种基础配置的组合。 搞懂了每一种基础配置,比如 eval、nosources、cheap、module,按照规律组合起来,也就搞懂了整体的配置。 那这每一种配置都是什么意思呢? 我们分别来看一下。

eval

eval 的 api 是动态执行 JS 代码的。比如: 但有个问题,eval 的代码打不了断点。 怎么解决这个问题呢? 浏览器支持了这样一种特性,只要在 eval 代码的最后加上 //# sourceURL=xxx,那就会以 xxx 为名字把这段代码加到 sources 里。那不就可以打断点了么? 比如这样: 执行以后,你会发现 sources 多了xiumubai.js的文件: 它是可以打断点的,比如在 add 里打个断点,然后再执行 eval。 你会发现它断住了! 除了指定 source 文件外,还可以进一步指定 sourcemap 来映射到源码: 这样,动态 eval 的代码也能关联到源码,并且能打断点了! webpack 就利用了 eval 这个特性来优化的 sourcemap 生成的性能,比如你可以指定 devtool 为 eval: 生成的代码就是每个模块都被 eval 包裹的,并且有 sourceUrl 来指定文件名: 这样有啥好处呢? 快呀,因为只要指定个文件名就行,不用生成 sourcemap。sourcemap 的生成还是很慢的,要一个个 mapping 的处理,做编码之类的。 每个模块的代码都被 eval 包裹,那么执行的时候就会在 sources 里生成对应的文件,这样就可以打断点了. 不过这样只是把每个模块的代码分了出去,并没有做源码的关联,如果相关联源码,可以再开启 sourcemap. 你会发现生成的代码也是用 eval 包裹的,但除了 sourceUrl 外,还有 sourceMappingUrl: 再运行的时候除了 eval 的代码会生成文件放在 sources 外,还会做 sourcemap 的映射: webpack 的 sourcemap 的配置就利用了浏览器对 eval 代码的调试支持。 所以为什么这个配置项不叫 sourcemap 而叫 devtool 呢? 因为不只是 sourcemap 呀,eval 的方式也行。

source-map

source-map 的配置是生成独立的 sourcemap 文件: 可以关联,也可以不关联,比如加上 hidden,就是生成 sourcemap 但是不关联: 生产环境就不需要关联 sourcemap,但是可能要生成 sourcemap 文件,把它上传到错误管理平台之类的,用来映射线上代码报错位置到对应的源码。 此外,还可以配置成 inline 的: 这个就是通过 dataUrl 的方式内联在打包后的文件里:

cheap

sourcemap 慢主要是处理映射比较慢,很多情况下我们不需要映射到源码的行和列,只要精确到行就行,这时候就可以用 cheap。 不精确到列能提升 souremap 生成速度,但是会牺牲一些精准度:

module

webpack 中对一个模块会进行多次处理,比如经过 loader A 做一次转换,再用 loader B 做一次转换,之后打包到一起。 每次转换都会生成 sourcemap,那也就是有多个 sourcemap: 默认 sourcemap 只是能从 bundle 关联到模块的代码,也就是只关联了最后那个 sourcemap。 那如果你想调试最初的源码怎么办呢? 那就把每一次的 loader 的 sourcemap 也关联起来,这就是 module 配置的作用。这样就能一次性映射回最初的源码。 当你想调试最初的源码的时候,module 的配置就很有用了。

nosources

sourcemap 里是有 sourceContent 部分的,也就是直接把源码贴在这里,这样的好处是根据文件路径查不到文件也可以映射,但这样会增加 sourcemap 的体积。 如果你确定根据文件路径能查找到源文件,那不生成 sourceContent 也行。 比如 devtool 配置为 source-map,生成的 sourcemap 是这样的: 当你加上 nosources 之后,生成的 sourcemap 就没有 sourceContent 部分了: sourcemap 文件大小会小很多。 基础配置讲完了,接下来就是各种组合了,这个就比较简单了,就算组合错了,webpack 也会提示你应该按照什么顺序来组合。 它是按照这个正则来校验的:^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$ 不知道有没有同学会觉得这样写比较麻烦,能不能每个基础配置用 true、false 的方式配置呢? 确实可以,有这样一个插件: SourceMapDevToolPlugin 它有很多 option,比如 module、columns、noSources 等: 相当于是 devtool 的另一种配置方式,启用它需要把 devtool 设置为 false。 而且它可以控制更多东西,比如修改 sourcemap 的 url 和文件名等: 当你需要做更多的 sourcemap 生成方式的控制的时候,可以使用这个 webpack 插件。 总结一下:

  • eval:浏览器 devtool 支持通过 sourceUrl 来把 eval 的内容单独生成文件,还可以进一步通过 sourceMappingUrl 来映射回源码,webpack 利用这个特性来简化了 sourcemap 的处理,可以直接从模块开始映射,不用从 bund le 级别。
  • cheap:只映射到源代码的某一行,不精确到列,可以提升 sourcemap 生成速度
  • source-map:生成 sourcemap 文件,可以配置 inline,会以 dataURL 的方式内联,可以配置 hidden,只生成 sourcemap,不和生成的文件关联
  • nosources:不生成 sourceContent 内容,可以减小 sourcemap 文件的大小
  • module: sourcemap 生成时会关联每一步 loader 生成的 sourcemap,可以映射回最初的源码
相关推荐
范文杰3 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪3 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪3 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy3 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom4 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom4 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom4 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom4 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom4 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试
LaoZhangAI5 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端