babel 插件开发原理理解

发布于 2023-11-16 07:28:50 字数 13032 浏览 29 评论 0

本文算是一个整理,借鉴了很多社区的文章,也加上了一些自己的理解。由于 babel 开发相关资料分散在各个文章里,这里做一个整理。这是业余项目 多端开发原理理解 的其中一篇文章。

了解 AST

参考资料: AST 抽象语法树

抽象语法树类似 DOM 树,通常有以下字段:

  • type: String : 类型
  • start: Number : 开始位置
  • end: Number : 结束位置
  • loc: Object : 位置的具体信息
  • 其他字段随 AST 节点的不同而不同

编译器的工作原理

参考资料: 懂编译真的可以为所欲为|不同前端框架下的代码转换

看下图:

具体到 babel 的编译,就是以下过程:

写法 1

代码字符串解析为 AST @babel/parser

const parse = require('@babel/parser').parse;

const ast = parse('const a = 1');

parse(code, options = {}) 有第二个参数 options ,默认值如下:

const defaultOptions = {
      sourceType: "script",
      sourceFilename: undefined,
      startLine: 1,
      allowAwaitOutsideFunction: false,
      allowReturnOutsideFunction: false,
      allowImportExportEverywhere: false,
      allowSuperOutsideMethod: false,
      plugins: [],
      strictMode: null,
      ranges: false,
      tokens: false,
      createParenthesizedExpressions: false
};

其中, sourceType 的值为:

  • script : (默认): 声明体(Statement)
  • module : 模块声明体(ModuleDeclaration)

除了常规的语法,Babel 可以转译的语法是有限的,扩展方式通过 options.plugins 配置:

  • estree
  • jsx
  • flow
  • doExpressions
  • objectRestSpread
  • decorators
  • classProperties
  • exportExtensions
  • asyncGenerators
  • functionBind
  • functionSent
  • dynamicImport

遍历并处理 AST 节点 @babel/traverse + @babel/types

遍历 AST 的工具:

const traverse = require('@babel/traverse').default;

处理 AST 的工具:

const t = require('@babel/types');

从 AST 还原为代码字符串 @babel/generator

const generator = require('@babel/generator').default;

const code = generator(ast).code;

写法 2

const result = babel.transform(code, {
    plugins: [
        [
            {
                visitor: {
                    ImportDeclaration(path, { opts }) {
                        console.log(opts); // output: { test: 1 }
                    }
                }
            },
            {
                test: 1
            }
        ]
    ]
});

console.log(result.code);

专题:熟悉所有 AST 节点

请准备一个 test.js ,初始化内容如下,并安装依赖:

const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;

const code = `xxx`;

const ast = parse(code);
// 加 sourceType 用于编译 es6 模块
// const ast = parse(code, { sourceType: 'module' });

traverse(ast, {});

打开文档: https://github.com/babel/babylon/blob/master/ast/spec.md

该文档介绍了所有的 AST 节点。上面代码中的 traverse 的第二个参数 key 值即为文档中的节点,如:

const code = `
    function test() {
        const a = 1;
        return a;
    }
`;

const ast = parse(code);

traverse(ast, {
    Identifier(path) {
        console.log(path.node.name);
    }
});

输出为:

test
a
a

请按照上面的写法,结合文档中的具体节点类型,写 demo 查看运行结果。重点观察 path.node 。经测算, @vdian/traverse 遍历 ast 时但并不完全支持 babylon 的文档涉及的所有 ast 节点,应该是支持了 95% 以上的节点,这点请注意。

专题:熟悉 @babel/typest 的 API

参考文档:

  1. babel 插件开发手册
  2. 官方文档的信息实在是太少了, 这里 是有人总结的 API 列表
  3. 这里是所有节点的定义

根据官方文档, t 有以下作用:构造(Builders)、验证(Validators)等。

1、构造(Builders): t.X()

构建器的方法名称就是您想要的节点类型的名称,除了第一个字母小写。

例如,如果您想建立一个 MemberExpression 您可以使用 t.memberExpression(...)

这些构建器的参数由节点定义决定。

节点定义如下所示:

defineType("MemberExpression", {
      builder: ["object", "property", "computed"],
      visitor: ["object", "property"],
      aliases: ["Expression", "LVal"],
      fields: {
            object: {
                validate: assertNodeType("Expression")
            },
            property: {
                validate(node, key, val) {
                    let expectedType = node.computed ? "Expression" : "Identifier";
                    assertNodeType(expectedType)(node, key, val);
                }
            },
            computed: {
                default: false
            }
      }
});

在这里你可以看到关于这个特定节点类型的所有信息,包括如何构建它,遍历它,并验证它。

通过查看 builder 属性, 可以看到调用生成器方法所需的 3 个参数,每个参数可用的值可以从 fileds 属性中找到。

如:

t.binaryExpression('*', t.identifier('a'), t.identifier('b'));

创建了一个二元表达式,转为 code 如下:

a * b

2、验证(Validators): t.isX + t.assertX

如:

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" })
t.assertBinaryExpression(maybeBinaryExpressionNode);

下面是各种举例:

检查节点类型

如果你想检查节点的类型,最好的方式是:

BinaryExpression(path) {
    if (t.isIdentifier(path.node.left)) {
        // ...
    }
}

你同样可以对节点的属性们做浅层检查:

BinaryExpression(path) {
    if (t.isIdentifier(path.node.left, { name: "n" })) {
        // ...
    }
}

功能上等价于:

BinaryExpression(path) {
    if (
        path.node.left != null &&
        path.node.left.type === "Identifier" &&
        path.node.left.name === "n"
    ) {
        // ...
    }
}

专题:熟悉 path 的 API

path 顾名思义是“路径”,代码中指的是:

traverse(ast, {
    Identifier(path) {
        console.log(path.node.name);
    }
});

访问

获取节点: path.node

// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {
    path.node.left;
    path.node.right;
    path.node.operator;
}

访问节点属性内部的 pathpath.get

BinaryExpression(path) {
    path.get('left');
}
Program(path) {
    path.get('body.0');
}

检查路径类型(和 t 相同)一个路径具有相同的方法检查节点的类型:

BinaryExpression(path) {
    if (path.get('left').isIdentifier({ name: "n" })) {
        // ...
    }
}

就相当于:

BinaryExpression(path) {
    if (t.isIdentifier(path.node.left, { name: "n" })) {
        // ...
    }
}

检查标识符(Identifier)是否被引用

Identifier(path) {
    if (path.isReferencedIdentifier()) {
        // ...
    }
}

或者:

Identifier(path) {
    if (t.isReferenced(path.node, path.parent)) {
        // ...
    }
}

找到特定的父路径

有时你需要从一个路径向上遍历语法树,直到满足相应的条件。对于每一个父路径调用 callback 并将其 NodePath 当作参数,当 callback 返回真值时,则将其 NodePath 返回。.

path.findParent((path) => path.isObjectExpression());

如果也需要遍历当前节点:

path.find((path) => path.isObjectExpression());

查找最接近的父函数或程序:

path.getFunctionParent();

向上遍历语法树,直到找到在列表中的父节点路径

path.getStatementParent();

停止遍历,如果你的插件需要在某种情况下不运行,最简单的做法是尽早写回。

BinaryExpression(path) {
    if (path.node.operator !== '**') return;
}

如果您在顶级路径中进行子遍历,则可以使用 2 个提供的 API 方法:

  • path.skip()
  • path.stop()

处理

用一个节点替换单节点: path.replaceWith(node)

BinaryExpression(path) {
    path.replaceWith(
        t.binaryExpression("**", path.node.left, t.numberLiteral(2))
    );
}
function square(n) {
-   return n * n;
+   return n ** 2;
  }

用多节点替换单节点: path.replaceWithMultiple([])

ReturnStatement(path) {
    path.replaceWithMultiple([
        t.expressionStatement(t.stringLiteral("Is this the real life?")),
        t.expressionStatement(t.stringLiteral("Is this just fantasy?")),
        t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),
    ]);
}
function square(n) {
-   return n * n;
+   "Is this the real life?";
+   "Is this just fantasy?";
+   "(Enjoy singing the rest of the song in your head)";
  }

注意:当用多个节点替换一个表达式时,它们必须是声明。 这是因为 Babel 在更换节点时广泛使用启发式算法,这意味着您可以做一些非常疯狂的转换,否则将会非常冗长。

用字符串源码替换节点

FunctionDeclaration(path) { 
    path.replaceWithSourceString(function add(a, b) { return a + b; }); 
}
+ "Because I'm easy come, easy go.";
  function square(n) {
    return n * n;
  }
+ "A little high, little low.";

注意:这里同样应该使用声明或者一个声明数组。 这个使用了在用多个节点替换一个节点中提到的相同的启发式算法。

插入到容器(container)中

ClassMethod(path) { 
    path.get('body').unshiftContainer(
        'body',
        t.expressionStatement(t.stringLiteral('before'))
    ); 
    
    path.get('body').pushContainer(
        'body', 
        t.expressionStatement(t.stringLiteral('after'))
    ); 
}
class A {
  constructor() {
+   "before"
    var a = 'middle';
+   "after"
  }
 }

删除一个节点: path.remove()

FunctionDeclaration(path) {
    path.remove();
}
- function square(n) {
-   return n * n;
- }

替换父节点: path.parentPath.replaceWith

Scope(作用域)

检查本地变量是否被绑定

FunctionDeclaration(path) {
  if (path.scope.hasBinding("n")) {
    // ...
  }
}

这将遍历范围树并检查特定的绑定。

您也可以检查一个作用域是否有自己的绑定:

FunctionDeclaration(path) {
    if (path.scope.hasOwnBinding("n")) {
        // ...
    } 
}

创建一个 UID

这将生成一个标识符,不会与任何本地定义的变量相冲突。

FunctionDeclaration(path) {
    path.scope.generateUidIdentifier("uid");
    // Node { type: "Identifier", name: "_uid" }
    path.scope.generateUidIdentifier("uid");
    // Node { type: "Identifier", name: "_uid2" }
}

提升变量声明至父级作用域

有时你可能想要推送一个 VariableDeclaration ,这样你就可以分配给它。

FunctionDeclaration(path) {
    const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
    path.remove();
    path.scope.parent.push({ id, init: path.node });
}
- function square(n) {
+ var _square = function square(n) {
    return n * n;
- }
+ };

重命名绑定及其引用

FunctionDeclaration(path) {
    path.scope.rename("n", "x");
}
- function square(n) {
-   return n * n;
+ function square(x) {
+   return x * x;
}

或者,您可以将绑定重命名为生成的唯一标识符:

FunctionDeclaration(path) {
    path.scope.rename("n");
}
- function square(n) {
-   return n * n;
+ function square(_n) {
+   return _n * _n;
  }

专题:熟悉 state

state 负责接收 plugin 中的传参。

const  { opts } = state;

插件配置举例:

{
    "plugins": [
        ["my-plugin", {
            "option1": true,
            "option2": false
        }]
    ]
}

babel 插件解析如下:

{
    visitor: {
        FunctionDeclaration(path, state) {
            console.log(state.opts);
            // { option1: true, option2: false }
        }
    }
}

参考资料

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

听风念你

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

13886483628

文章 0 评论 0

流年已逝

文章 0 评论 0

℡寂寞咖啡

文章 0 评论 0

笑看君怀她人

文章 0 评论 0

wkeithbarry

文章 0 评论 0

素手挽清风

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文