Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[多端开发系列] babel 插件开发原理理解 #4

Open
hoperyy opened this issue Apr 26, 2019 · 0 comments
Open

[多端开发系列] babel 插件开发原理理解 #4

hoperyy opened this issue Apr 26, 2019 · 0 comments

Comments

@hoperyy
Copy link
Owner

hoperyy commented Apr 26, 2019

[多端开发系列] babel 插件开发原理理解

本文算是一个整理,借鉴了很多社区的文章,也加上了一些自己的理解。

由于 babel 开发相关资料分散在各个文章里,这里做一个整理。

这是业余项目 多端开发原理理解 的其中一篇文章。

了解 AST

参考资料:AST 抽象语法树

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

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

编译器的工作原理

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

看下图:

image

具体到 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 }
        }
    }
}

参考资料

@hoperyy hoperyy changed the title babel 插件开发全面理解 babel 插件开发原理理解 May 10, 2019
@hoperyy hoperyy changed the title babel 插件开发原理理解 [多端开发系列] babel 插件开发原理理解 May 10, 2019
@hoperyy hoperyy mentioned this issue Feb 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant