• 从 AST 到 自定义 Babel 插件


    从 AST 到 自定义 Babel 插件

    本文会将介绍AST抽象语法树的概念和基本原理、AST抽象语法树的遍历和生成、如何使用babel插件进行代码转换以及如何自定义 babel 插件。

    实现我们先介绍一下抽象语法树(Abstract Syntax Tree,AST)

    1. 抽象语法树(Abstract Syntax Tree,AST)

    什么是抽象语法树(Abstract Syntax Tree,AST)? 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的⼀种抽象表示, 它以树状的形式表现编程语⾔的语法结构,树上的每个节点都表示源代码中的⼀种结构。类似于虚拟 dom,只是虚拟 dom 是描述 dom 结构。

    image.png

    Eslint 以及 babel 都是基于抽象语法树来实现的,包括 JSX 语法和 Vue 中的 template 语法都离不开它。

    2. 抽象语法树⽤途

    1. 代码语法的检查、代码⻛格的检查、代码的格式化、代码的⾼亮、代码错误提示、代码⾃动补全等等
    2. 优化变更代码,改变代码结构使达到想要的结构。例如将 ES6 等高级语法转换成低级语法、JSX 通过 babel 最终转化成 React.createElement 这种形式

    3. JavaScript Parser

    JavaScript Parser 是把 JavaScript 源码转化为抽象语法树的解析器 ,常见的 Parser 有 esprima、traceur、acorn、shift 等。

    那么 AST 是如何生成的呢?

    JS 执行的第一步是读取 JS 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析生成 AST,最后生成机器码执行。

    1. 词法分析

    词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的 Token。Token 是一个不可分割的最小单元,例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等)。最终,整个代码将被分割进一个tokens 列表(或者说一维数组)。

    1. 语法分析

    语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。

    这里提提供一个在线工具: AST explorer,利用它可以看到不同的 parser 解析 JS 代码后得到的 AST。

    下面这个栗子是使用一个 esprima 这个 Parser 对 function a(){}生成的语法树。

    image.png

    可以看到这个 AST 对这句代码的描述非常详细。接下来我们不借助这个在线工具来实现它。

    4. 代码转换

    代码转换的大致流程为:

    1. 将代码转换成 AST 语法树
    2. 深度优先遍历,遍历 AST 抽象语法树
    3. 代码⽣成

    实现,我们需要按照三个第三方库:esprimaestraverseescodegen

    yarn add esprima estraverse escodegen
    
    • 1

    然后我们对它们进行导入。

    const esprima = require('esprima');
    const estraverse = require('estraverse');
    const escodegen = require('escodegen');
    
    • 1
    • 2
    • 3

    通过 esprima.parseScript 方法将我们的代码转换成 AST 语法树。

    let code = `function a(){}`;
    const ast = esprima.parseScript(code);
    console.log(ast);
    
    • 1
    • 2
    • 3

    可以看到输出结果与上面生成的 AST 类似。

    Script {
      type: 'Program',
      body: [
        FunctionDeclaration {
          type: 'FunctionDeclaration',
          id: [Identifier],
          params: [],
          body: [BlockStatement],
          generator: false,
          expression: false,
          async: false
        }
      ],
      sourceType: 'script'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    有了这么一个树结构之后,我们当然希望能够对它进去遍历,这个时候我们需要用到 estraverse.traverse

    estraverse.traverse(ast, {
      enter(node) { // Program ->  FunctionDeclaration -> Identifier
        console.log('enter:' + node.type)
        if (node.type === 'FunctionDeclaration') {
          node.id.name = 'ast'
        }
      },
      leave(node) {
        console.log('leave:' + node.type)
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    访问模式就是遍历节点的时候会有两个过程,一个是进入一个是离开,分别对应 enterleave 两个钩子函数,并且我们在其中修改函数的标识符。

    enter:Program
    enter:FunctionDeclaration
    enter:Identifier
    leave:Identifier
    enter:BlockStatement
    leave:BlockStatement
    leave:FunctionDeclaration
    leave:Program
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后我们再通过 escodegen.generate 方法来生成代码。

    console.log(escodegen.generate(ast));
    
    • 1

    输出结果如下。

    function ast() {
    }
    
    • 1
    • 2

    可以看到函数名已经被修改了,至此我们便实现了一个简单的代码转换。

    接下来我们来写个 babel 插件,来感受一下 babel 是如何进行转换的。

    5. babel 插件

    Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

    Babel 的三个主要处理步骤分别是:解析(parse)转换(transform)生成(generate)
    解析步骤前面已经讲过了,而转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程同时也是插件将要介入工作的部分。代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

    比较典型的就是将 ES6 语法转换为 ES5 语法,下面我们来实现两个简单的栗子:转换箭头函数、类编译为 Function

    5.1 转换箭头函数

    首先我们先介绍一下需要用到的第三方库。

    • @babel/core Babel 的编译器,核⼼ API 都在这⾥⾯,⽐如常⻅的 transformparse,并实现了插件功能 ,也就是说可以在其中放入对应的 babel 插件,在转换的时候会默认调用它们。
    • @babel/types ⽤于 AST 节点的 Lodash 式⼯具库, 它包含了构造、验证以及变换 AST 节点的⽅法,对编写处理 AST 逻辑⾮常有⽤
    • babel-plugin-transform-es2015-arrow-functions 转换箭头函数插件

    接下来我们来看看如何借助 babel-plugin-transform-es2015-arrow-functions 来实现转换箭头函数。

    const babel = require("@babel/core");
    const types = require("@babel/types");
    const transformFunction = require("babel-plugin-transform-es2015-arrow-functions");
    
    const code = `const sum = (a,b)=> a+b`;
    // 转化代码,通过 transformFunction 插件
    const result = babel.transform(code, {
      plugins: [transformFunction],
    });
    console.log(result.code);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    最终结果如下。

    const sum = function (a, b) {
      return a + b;
    };
    
    • 1
    • 2
    • 3

    可以看到我们已经成功将箭头函数转成 function 函数。其实这个 transformFunction 插件的原理跟上面的代码转换类似,下面我们先看看这两个结果的 AST。

    image.png

    通过仔细观察,相信你已经看出之间的不同。庆幸的是 @babel/core 为我们提供了很好的插件拓展方式,下面我们来看看如何实现。

    const transformFunction = {
      visitor: {
        ArrowFunctionExpression(path) { // path就是访问的路径   path -> node
          let {
            node
          } = path;
          node.type = 'FunctionExpression';
    
          let body = node.body; // 老节点中的 a+b;
    
          if (!types.isBlockStatement(body)) {
            node.body = types.blockStatement([types.returnStatement(body)])
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个**访问者模式(visitor)**的概念。访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。我们在对象里定义了 ArrowFunctionExpression 方法,遇到箭头函数表达式会命中此⽅法,如何利用 @babel/types 构造、变换 AST 节点。

    然而箭头函数还有一个问题,就是 this 的指向,我们需要单独对它进行处理。例如我们要将以下代码进行转换。

    const sum = ()=> console.log(this)
    
    • 1

    转换后的结果为:

    var _this = this;
    
    const sum = function () {
      return console.log(_this);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到,我们需要找到上层作用域里的 this ,将它赋值给 _this,从而将函数里的 this 改成了 _thiis 。下面我们先看看如何实现。

    const transformFunction = {
      visitor: {
        ArrowFunctionExpression(path) { // path就是访问的路径   path -> node
          let { node } = path;
          node.type = 'FunctionExpression';
    
          hoistFunctionEvn(path);
          let body = node.body; // 老节点中的 a+b;
    
          if (!types.isBlockStatement(body)) {
            node.body = types.blockStatement([types.returnStatement(body)])
          }
        }
      }
    }
    
    function getThisPath(path) {
      let arr = []
      path.traverse({
        ThisExpression(path) {
          arr.push(path);
        }
      })
      return arr;
    }
    
    function hoistFunctionEvn(path) {
      // 查找父作用域
      const thisEnv = path.findParent((parent) => (parent.isFunction() && !parent.isArrowFunctionExpression()) || parent.isProgram())
    
      const bingingThis = '_this'; // var _this = this;
    
      const thisPaths = getThisPath(path);
    
      // 修改当前路径中的this  变为_this
      thisPaths.forEach(path => {
        // this -> _this
        path.replaceWith(types.identifier(bingingThis))
      })
      thisEnv.scope.push({
        id: types.identifier(bingingThis),
        init: types.thisExpression()
      })
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    我们通过 path.findParent 方法从一个路径向上遍历语法树,直到满足相应的条件。对于每一个父路径调用 callback 并将其 NodePath 当作参数,当 callback 返回真值时,则将其 NodePath 返回。我们的判断条件是:该节点类型是 function 函数 or 根节点。接着我们便需要修改当前路径中的 this 变为 _this。当然这里用到了很多 API 这里便不再一一解释,读者可自行查阅:Babel 插件手册

    5.2 类编译为 Function

    同样的,我们要把以下 ES6 语法的类更改为 Function 的方式。

    class Person {
      constructor(name) {
        this.name = name;
      }
      getName() {
        return this.name;
      }
      setName(newName) {
        this.name = newName;
      }
    }
    
    // 转换后
    
    function Person(name) {
      this.name = name;
    }
    Person.prototype.getName = function () {
      return this.name;
    };
    Person.prototype.setName = function () {
      this.name = newName;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    下面我们先看看这两个结果的 AST,以及给出实现方式,这里就不再过多解释其中的 API,读者可自行查阅:Babel 插件手册

    image.png

    const transformFunction = {
      visitor: {
        ClassDeclaration(path) {
          const {
            node
          } = path;
          const id = node.id;
          const methods = node.body.body; // 获取类中的⽅法
          const nodes = [];
          methods.forEach((method) => {
            if (method.kind === "constructor") {
              let constructorFunction = types.functionDeclaration(
                id,
                method.params,
                method.body
              );
              nodes.push(constructorFunction);
            } else {
              // Person.prototype.getName
              const memberExpression = types.memberExpression(
                types.memberExpression(id, types.identifier("prototype")),
                method.key
              );
              // function(name){return name}
              const functionExpression = types.functionExpression(
                null,
                method.params,
                method.body
              );
              // 赋值
              const assignmentExpression = types.assignmentExpression(
                "=",
                memberExpression,
                functionExpression
              );
              nodes.push(assignmentExpression);
            }
          });
          // 替换节点
          if (node.length === 1) {
            path.replaceWith(nodes[0]);
          } else {
            path.replaceWithMultiple(nodes);
          }
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
  • 相关阅读:
    DevExpress VCL Subscription 23 crack
    计算机毕业设计Java校园资料分享平台(系统+源码+mysql数据库+lw文档)
    YOLOv8改进 | EIoU、SIoU、WIoU、DIoU、FocusIoU等二十余种损失函数
    「动态规划学习心得」正则表达式匹配
    Pytorch模型训练实用教程学习笔记:二、模型的构建
    ArcGIS按点提取栅格
    uniapp 点击事件-防重复点击
    堪称Python入门新华字典的《Python背记手册》高清无码版
    【02】Spring源码-手写篇-手写DI实现
    华为ENSP网络设备配置实战2(较为复杂的ospf)
  • 原文地址:https://blog.csdn.net/p1967914901/article/details/126036271