Generator 和 SourceMap的奥秘

AST转换完之后就到了generate阶段,这个阶段的作用是生成目标代码和sourcemap的,它们是怎么生成的?sourcemap有啥作用?

本节来探索下generate的奥秘。

generate

generate是把AST打印成字符串,是一个从根节点递归打印的过程,对不同的AST节点做不同的处理,在这个过程中把抽象语法树省略的一些分隔符重新加回来。

比如while语句WhileStatement就是先打印while,然后打印一个空格和"(",然后打印node.test属性的节点,然后打印")",之后打印block部分。 比如条件表达式ConditionExpression就是分别打印node.test、node.consequent、node.alternate属性,中间插入? : 和空格: 通过这样的方式递归打印整个AST,就可以生成目标代码。

@babel/generator的src/generators下定义了每一种AST节点的打印方式,感兴趣的话可以看看每一种AST都是怎么打印的。

sourcemap

我们知道可以在generate的时候选择是否生成sourcemap,那为什么要生成sourcemap呢?

sourcemap的作用

babel对源码进行了修改,生成的目标代码可能改动很大,如果直接调试目标代码,那么可能很难应对到源码里。所以需要一种自动关联源码的方式,就是sourcemap。

我们平时用sourcemap主要用两个目的:

1.调试代码时定位到源码

chrome、firefox等浏览器支持在文件末尾加上一行注释

js 复制代码
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map

可以通过url的方式或者转成base64内敛的方式来关联sourcemap。调试工具会自动解析sourcemap,关联到源码。这样打断点、错误堆栈等都会对应到相应源码。

2.线上报错定位到源码

开发时会使用sourcemap来调试,但是生产不回,要是把sourcemap传到生产算是大事故了。但是线上报错的时候也需要定位到源码,这种情况一般都是单独上传sourcemap到错误收集平台。

比如sentry就提供了一个@sentry/webpack-plugin支持在打包完成后把sourcemap自动上传到sentry后台,然后把本地sourcemap删除掉。还提供了@sentry/cli让用户可以手动上传。

平时我们至少在这两个场景(开发时调试源码,生产时定位错误)下会用到sourcemap。

sourcemap的格式

js 复制代码
{
  version : 3,
   file: "out.js",
   sourceRoot : "",
   sources: ["foo.js", "bar.js"],
   names: ["src", "maps", "are", "fun"],
   mappings: "AAgBC,SAAQ,CAAEA"
}

上面的就是一个sourcemap文件,对应字段的含义如下:

  • version:soucemap的版本,目前版本是3
  • file:转换后的文件名
  • sourceRoot:转换前的文件所在目录。如果与转换前的文件在同一目录,则该项为空。
  • sources:转换前的文件。该项是一个数组,因为可能是多个源文件合并成一个目标文件。
  • names:转换前的所有变量名和属性名,把所有变量名提取出来,下面的mapping直接使用下标引用,可以减少体积。
  • mappings:转换前代码和转换后代码的映射瓜系的集合,用分号代表一行,每行的mapping用逗号分隔。

重点看mapping部分

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

每一个分号;表示一行,多个空行就是多个;,mapping通过,分割。

mapping有五位:

js 复制代码
 第一位是目标代码中的列数
 第二位是源码所在的文件名
 第三位是源码对应的行数
 第四位是源码对应的列数
 第五位是源码对应的 names,不一定有

每一位是通过VLQ编码的,一个字符就能表示行列数。

sourcemap通过names;的设计省略掉了一些变量名和行数所占的空间,又通过VLQ编码使得一个字符就可以表示行列数等信息。通过不大的空间占用完成了源码到目标代码的映射。

那么sourcemap的源码和目标代码的行列数是怎么来的呢?

其实我们在parse的时候就在AST节点中保存的loc属性,这就是源码中的行列号,在后面transform的过程中并不会去修改它,所以转换完以后节点中仍然保留有源码中的行列号信息,在generate打印成目标代码的时候会计算出新的行列号,这样两者关联就可以生成sourcemap。 具体生成sourcemap的过程是用mozilla维护的source-map这个包,其他工具做soucemap的解析和生成也是基于这个包。

source-map

source-map是用于生成和解析sourcemap,需要手动操作sourcemap的时候可以用,我们通过它的api来感受下babel是怎么生成sourcemap的。

source-map暴露了SourceMapConsumer、SourceMapGenerator、SourceCode3个类,分别用于消费sourcemap、生成sourcemap、创建源码节点。

生成sourcemap

生成sourcemap的流程是:

  1. 创建一个SourceMapGenerator对象
  2. 通过addMapping方法添加一个映射
  3. 通过toString转为sourcemap字符串
js 复制代码
var 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());
// '{"version":3,"file":"source-mapped.js",
//   "sources":["foo.js"],"names":["christopher"],"mappings":";;;;;;;;;mCAgCEA"}'

消费sourcemap

SourceMapConsumer.with的回调里面可以拿到consumer的api,调用originalPositionFor和generatedPositionFor可以分别用目标代码位置查源码位置和用源码位置查目标代码位置,还可以通过eachMapping遍历所有mapping,对每个进行处理。

js 复制代码
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"
};

const whatever = 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) {
    // ...
  });

  return computeWhatever();
});

babel就是用这些api来生成sourcemap的。

总结

generate就是递归打印AST成字符串,在递归打印的过程中会根据源码位置和计算出的目标代码的位置来生成mapping,加到sourcemap中。sourcemap是源码和目标代码的映射,用于开发时调试源码和生产时定位线上错误。

相关推荐
过云鱼1 个月前
Babel 快速上手
面试·babel·前端工程化
aoi4 个月前
Babel 示例插件:处理 AST 替换变量、箭头函数
javascript·babel
文艺理科生6 个月前
Webpack项目构建入门:babel的核心角色
前端·webpack·babel
阿镇吃橙子7 个月前
由浅入深 ——Vite工具链学习总结
vue.js·vite·babel
总之就是非常可爱7 个月前
提升前端开发效率:利用 Babel 实现 JavaScript 文件的定制化修改
前端·babel
月下点灯7 个月前
小白也能看懂的AST抽象语法树+babel插件开发教程
前端·javascript·babel
let_code8 个月前
Babel
前端·babel
ichimaru_xx8 个月前
node前端工具实战-svg引入整理工具
node.js·babel
每天写一个BUG8 个月前
简单玩一玩 Babel
前端·babel·前端工程化
yuansucai8 个月前
LocatorJS接入
babel