带你玩转babel工具链(三)@babel/generator

2,092 阅读3分钟

一、前言

在前面两章, 我们一起学习了parsertraverse, 还差最后一步generator就可以把代码编译的整个流程串联起来。下面我们了解写@babel/generator的用法和原理吧

往期回顾:

二、基础案例

const parser = require('@babel/parser')
const generator = require('@babel/generator')

const ast = parser.parse(`
  const a: number = 1
`)


const output = generator.default(ast, {})

最后输出的内容如下, 产生了最终的codesourcemap image.png

选项

并且generator还支持一些参数

const output = generator.default(ast, {
  sourceMaps: true
  // ...
})
选项说明
auxiliaryCommentAfter添加注释到生成代码的最后
auxiliaryCommentBefore添加注释到生成代码的最前
comments生成的代码是否包含注释
compact生成的代码是否包含多于的空格
concise设置为true可减少空白(但不如opts.compact)
decoratorsBeforeExportdecoratorsBeforeExport为false, export @decorator class Bar {}, decoratorsBeforeExport为true,
@decorator
export class Foo {}
filename用于警告消息中
minified是否压缩
retainFunctionParens保留函数表达式周围的参数(可用于更改引擎解析行为)
retainLines尝试在输出代码中使用与源代码中相同的行号(有助于保留堆栈跟踪)
shouldPrintComment是一个函数,参数可以接收注释内容,如果函数返回true,就会保留注释,否则删除,类似comments选项
sourceMaps是否生成sourceMap
sourceRoot设置sourcemap url相对根路径

三、实现@babel/generator

下面的代码中,定义了各种AST的生成函数,其实就是使用策略模式这种设计模式,对于不同类型的节点使用不同的方法处理。

其中还实现了sourcemap的生成,借助了source-map这个库,分别在不同节点遍历时,创建一个map

const { SourceMapGenerator } = require('source-map');

class Printer {
  constructor (source, fileName) {
    // 记录生成的代码,不断拼接
    this.buf = '';
    
    this.sourceMapGenerator = new SourceMapGenerator({
      file: fileName + ".map.json",
    });

    this.fileName = fileName;
    // 设置源码内容
    this.sourceMapGenerator.setSourceContent(fileName, source);
    
    this.printLine = 1;
    this.printColumn = 0;
  }

  addMapping(node) {
    if (node.loc) {
      // 添加一个映射关系
      this.sourceMapGenerator.addMapping({
        generated: {
          line: this.printLine, // 这里是转换后的行数
          column: this.printColumn // 这里是转换后的列数
        },
        source: this.fileName, // 文件名
        original: node.loc && node.loc.start // 转换前的起始位置
      })
    }
  }

  // 增加一个空格
  space() {
    this.buf += ' ';
    this.printColumn ++;
  }

  // 换行
  nextLine() {
    this.buf += '\n';
    this.printLine ++;
    this.printColumn = 0;
  }

  // 遍历 Program下的节点,并调用相应的方法拼接
  Program (node) {
    this.addMapping(node);
    node.body.forEach(item => {
      // 执行对应节点的拼接方法
      this[item.type](item) + ';';
      // 行数+1
      this.printColumn ++;
      // 换行
      this.nextLine();
    });
  }

  // 遍历声明语句
  VariableDeclaration(node) {
      if(!node.declarations.length) {
        return;
      }
      this.addMapping(node);
      // let var const
      this.buf += node.kind;
      // 空格
      this.space();

      // 遍历变量名
      node.declarations.forEach((declaration, index) => {
        if (index != 0) {
          this.buf += ',';
          this.printColumn ++;
        }
        // 执行到 VariableDeclarator 拼接方法
        this[declaration.type](declaration);
      });
      // 添加分号
      this.buf += ';';
      // 列数+1
      this.printColumn ++;

  }
  VariableDeclarator(node) {
    this.addMapping(node);
    // 执行到 Identifier 拼接
    this[node.id.type](node.id);
    // 添加赋值
    this.buf += '=';
    // 列数+1
    this.printColumn ++;
    // 执行到 Identifier 或 NumericLiteral 等
    this[node.init.type](node.init);
  }
  Identifier(node) {
    this.addMapping(node);
    this.buf += node.name;
  }

  FunctionDeclaration(node) {
    this.addMapping(node);
    // 拼接函数
    this.buf += 'function ';
    this.buf += node.id.name;
    this.buf += '(';
    this.buf += node.params.map(item => item.name).join(',');
    this.buf += '){';
    this.nextLine();
    this[node.body.type](node.body);
    this.buf += '}';
    this.nextLine();
  }
  CallExpression(node) {
    this.addMapping(node);
    this[node.callee.type](node.callee);
    this.buf += '(';
    node.arguments.forEach((item, index) => {
        if(index > 0 ) this.buf += ', ';
        this[item.type](item);
    })
    this.buf += ')';
  }
  ExpressionStatement(node) {
    this.addMapping(node);

    this[node.expression.type](node.expression);
  }
  ReturnStatement(node) {
    this.addMapping(node);

    this.buf += 'return ';
    this[node.argument.type](node.argument); 
  }
  BinaryExpression(node) {
    this.addMapping(node);

    this[node.left.type](node.left);
    this.buf += node.operator;
    this[node.right.type](node.right);
  }
  BlockStatement(node) {
    this.addMapping(node);

    node.body.forEach(item => {
      this.buf += '    ';
      this.printColumn += 4;
      this[item.type](item);
      this.nextLine();
    });
  }
  NumericLiteral(node) {
    this.addMapping(node);
    this.buf += node.value;
  }
}

总结

@babel/generator实现起来较为简单,核心在于借助了策略模式,各个节点都有其对应的代码生成方法。

另外还使用了source-map这个库,生成代码映射。

参考

Babel 插件通关秘籍