• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍

    • 1.Babel 的介绍
    • 2.Babel 的编译流程
    • 3.Babel 的 AST
    • 4.Babel 的 API
    • 5.实战案例:插入函数调用参数
    • 6.JS Parser 的历史
    • 7.traverse 的 path、scope、visitor
    • 8.Generator 和 SourceMap 的奥秘
    • 9.Code- Frame 和代码高亮原理
    • 10.Babel 插件和 preset
    • 11.Babel 插件的单元测试
    • 12.Babel 的内置功能(上)
    • 13.Babel 的内置功能(下)
    • 14.Babel 配置的原理
    • 15.工具介绍:VSCode Debugger 的使用
    • 16.实战案例:自动埋点
    • 17.实战案例: 自动国际化
    • 18.实战案例:自动生成 API 文档
    • 19.实战案例: Linter
    • 20.实战案例: 类型检查
    • 21.实战案例: 压缩混淆
    • 22.实战案例: JS 解释器
    • 23.实战案例: 模块遍历
    • 24.Babel Macros
    • 25.如何调试 Babel 源码?
    • 26.手写 Babel:思路篇
    • 27.手写 Babel: parser 篇
    • 28.手写 Babel: traverse 篇
    • 29.手写 Babel: traverse -- path篇
    • 30.手写 Babel: traverse -- scope篇
    • 31.手写 Babel: generator篇
    • 32.手写 Babel: core篇
    • 33.手写 Babel: cli篇
    • 34.手写 Babel: 总结
    • 35.小册总结
    • 36.加餐:会了 babel 插件,就会写 prettier 插件

generator 是打印 AST 为目标代码,并生成 sourcemap。

这节我们实现一下 generator。

思路分析

generator 会遍历 AST 进行打印,对于每种 AST 我们是知道如何打印的,比如 while 语句:

先打印 while、再打印空格,再打印 ( ,然后打印 test 部分,之后打印 ),最后打印 block 部分。

那么实现了每种 AST 的打印就可以拼接出目标代码。

而 sourcemap 是记录源码位置和目标代码位置的关联,在打印的记录下当前打印的行列,就是目标代码位置,而源码位置 parse 的时候就有了,这样就生成了一个 mapping。

sourcemap 就是由一个个 mapping 组成的,打印每个 AST 节点的时候添加一下 mapping,最终就生成了 sourcemap。

代码实现

我们定义一个 Printer 类做打印,实现每种 AST 的打印逻辑:

class Printer {
    constructor(source, fileName) {
        this.buf = "";
        this.printLine = 1;
        this.printColumn = 0;
    }

    addMapping(node) {
        // 待实现
    }

    space() {
        this.buf += " ";
        this.printColumn++;
    }

    nextLine() {
        this.buf += "\n";
        this.printLine++;
        this.printColumn = 0;
    }

    Program(node) {
        this.addMapping(node);
        node.body.forEach((item) => {
            this[item.type](item) + ";";
            this.printColumn++;
            this.nextLine();
        });
    }

    VariableDeclaration(node) {
        if (!node.declarations.length) {
            return;
        }
        this.addMapping(node);

        this.buf += node.kind;
        this.space();
        node.declarations.forEach((declaration, index) => {
            if (index != 0) {
                this.buf += ",";
                this.printColumn++;
            }
            this[declaration.type](declaration);
        });
        this.buf += ";";
        this.printColumn++;
    }
    VariableDeclarator(node) {
        this.addMapping(node);
        this[node.id.type](node.id);
        this.buf += "=";
        this.printColumn++;
        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;
    }
}

这样递归进行打印就可以生成完整的目标代码,我们把它记录到了 this.buf 属性。

同时,我们在打印的时候记录了 printLine、printColumn 的信息,也就是当前打印到了第几行,这样在 addMapping 里面就可以拿到 AST 在目标代码中的位置,而源码位置是在 parse 的时候记录到 loc 属性的,有了这两个位置就可以生成一个 mapping。

sourcemap 的生成是使用 source-map 包,这个 mozilla 维护的,因为 sourcemap 的标准也是他们提出来的。

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;
    }
}

sourcemap 需要指定源文件名,这也是为什么我们要传入 fileName。

之后实现 addMapping 方法:

addMapping(node) {
    if (node.loc) {
        this.sourceMapGenerator.addMapping({
            generated: {
              line: this.printLine,
              column: this.printColumn
            },
            source: this.fileName,
            original: node.loc && node.loc.start
        })
    }
}

最后,我们定义 Generator 类,在 generate 方法里面调用 printer 的打印逻辑来生成目标代码,并且调用 this.sourceMapGenerator.toString() 来生成 sourcemap。

class Generator extends Printer {
    constructor(source, fileName) {
        super(source, fileName);
    }

    generate(node) {
        this[node.type](node);

        return {
            code: this.buf,
            map: this.sourceMapGenerator.toString(),
        };
    }
}

然后暴露出 generate 的 api:

function generate(node, source, fileName) {
    return new Generator(source, fileName).generate(node);
}

这样,我们就实现了 babel generator 的功能,也就是打印目标代码和生成 sourcemap。

可以在生成的代码中添加 sourceMappingURL 就可以映射回源码,可以通过打断点或者运行代码 throw error 的方式来测试 测试 sourcemap的功能。

//# sourceMappingURL=./xxx.map.json

总结

generator 是打印 AST 为目标代码,我们知道每种 AST 是如何打印的,那么递归打印 AST,拼接字符串,就可以生成目标代码。

sourcemap 是调试代码和线上报错定位源码必不可少的功能,我们基于 source-map 包来生成,记录一个个 mapping。

具体的 mapping 就是源代码位置和目标代码位置的关联,AST 在源码的位置记录在 loc 属性,而在目标代码的位置位置可以计算出来。这样就可以生成 sourcemap。

(代码在这里,建议 git clone 下来通过 node 跑一下)

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
30.手写 Babel: traverse -- scope篇
Next
32.手写 Babel: core篇