webpack4学习笔记3-AST入门

AST(抽象语法树),是一个非常基础而重要,缺又被大多数人忽略的知识点。webpack和lint等很多工具和库的核心都是通过AST来实现对代码的检查、分析等操作的。了解抽象语法树对于深入理解JavaScript有很大的帮助。
以下内容参考了 AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解

抽象语法树的用途

  1. 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
  • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
  • IDE的错误提示、格式化、高亮、自动补全等等
  1. 代码混淆压缩
  • UglifyJS2等
  1. 优化变更代码,改变代码结构使达到想要的结构
  • 代码打包工具webpack、rollup等等
  • CommonJS、AMD、CMD、UMD等代码规范之间的转化
  • CoffeeScript、TypeScript、JSX等转化为原生Javascript


    AST.png

我们来拆解一个简单的add函数

function add(a, b) {
    return a + b
}
addFunction.png

首先,我们拿到的语法块是一个FunctionDeclaration(函数定义)对象。
拆开之后,变成三块:

  • 一个id,就是他的名字,即add
  • 两个params,就是它的参数,即[a,b]
    一个body,即函数的主体部分

add没办法继续拆下去了,它是一个最基础Identifier(标志)对象,用来作为函数的唯一标志,就像人的姓名一样。


add.png

params继续拆下去,其实是两个Identifier组成的数组。之后也没办法拆下去了。

params.png

接着我们拆body
body其实是一个BlockStatement(块状域)对象,用来表示是{return a + b}


body.png

打开ReturnStatement,里面是一个BinaryExpression(二项式)对象,用来表示a + b

打开BinaryExpression,它成了三部分,left,operator,right

BinaryExpression.png

拆解之后是这个样子:


ASTtree.png

可以看出AST其实就是一棵标准的语法树。

AST螺丝刀:recast

npm i recast -S

ASTdemo.js

// 给你一把"螺丝刀"——recast
const recast = require("recast");

// 你的"机器"——一段代码
// 我们使用了很奇怪格式的代码,想测试是否能维持代码结构
const code =
  `
  function add(a, b) {
    return a +
      // 有什么奇怪的东西混进来了
      b
  }
  `
// 用螺丝刀解析机器
const ast = recast.parse(code);

// ast可以处理很巨大的代码文件
// 但我们现在只需要代码块的第一个body,即add函数
const add  = ast.program.body[0]

console.log(add)
// console.log(add.body.body[0].argument.left)
ASTdemo.png

可以看出add函数的结构,与前面的结构一致。

recast.types.builders 制作模具

只能查看不能修改肯定是远远不够的,那么我们如何来修改这个函数呢?

  • 第一步,我们创建一个VariableDeclaration变量声明对象,声明头为const, 内容为一个即将创建的VariableDeclarator对象。
  • 第二步,创建一个VariableDeclarator,放置add.id在左边, 右边是将创建的FunctionDeclaration对象
  • 第三步,我们创建一个FunctionDeclaration,如前所述的三个组件,id params body中,因为是匿名函数id设为空,params使用add.params,body使用add.body。

以上遍创建好了 const add = function(){}的AST对象

在ASTdemo.js中加入代码

// 引入变量声明,变量符号,函数声明三种“模具”
const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders

// 将准备好的组件置入模具,并组装回原来的ast对象。
ast.program.body[0] = variableDeclaration("const", [
  variableDeclarator(add.id, functionExpression(
    null, // Anonymize the function expression.
    add.params,
    add.body
  ))
]);

//将AST对象重新转回可以阅读的代码
const output = recast.print(ast).code;

console.log(output)

打印结果:

const add = function(a, b) {
  return a +
    // 有什么奇怪的东西混进来了
    b
};

命令行修改js文件

除了parse/print/builder以外,Recast的三项主要功能:

  • run: 通过命令行读取js文件,并转化成ast以供处理。
  • tnt: 通过assert()和check(),可以验证ast对象的类型。
  • visit: 遍历ast树,获取有效的AST对象并进行更改。
    新建demo.js
function add(a, b) {
  return a + b
}

function sub(a, b) {
  return a - b
}

function commonDivision(a, b) {
  while (b !== 0) {
    if (a > b) {
      a = sub(a, b)
    } else {
      b = sub(b, a)
    }
  }
  return a
}

新建一个名为run.js的文件
读取demo.js文件,并转化为AST对象

#!/usr/bin/env node
const recast  = require('recast')
recast.run( function(ast, printSource){
  printSource(ast)
})

recast.visit —— AST节点遍历
注意:

  • 你想操作函数声明,就使用visitFunctionDelaration遍历,想操作赋值表达式,就使用visitExpressionStatement。 只要在 AST对象文档中定义的对象,在前面加visit,即可遍历
  • 通过node可以取到AST对象
  • 每个遍历函数后必须加上return false,或者选择以下写法,否则报错:
#!/usr/bin/env node
const recast  = require('recast')

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.node
        printSource(node)
        this.traverse(path)
      }
    })
});

!/usr/bin/env node 在所有使用recast.run()的文件顶部都需要加入这一行,否则执行命令时将报错

TNT —— 判断AST对象类型

TNT,即recast.types.namedTypes,就像它的名字一样火爆,它用来判断AST对象是否为指定的类型。

TNT.Node.assert(),就像在机器里埋好的炸药,当机器不能完好运转时(类型不匹配),就炸毁机器(报错退出)

TNT.Node.check(),则可以判断类型是否一致,并输出False和True

上述Node可以替换成任意AST对象,例如TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()
read.js

#!/usr/bin/env node
const recast = require("recast");
const TNT = recast.types.namedTypes

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.value
        // 判断是否为ExpressionStatement,正确则输出一行字。
        if(TNT.ExpressionStatement.check(node)){
          console.log('这是一个ExpressionStatement')
        }
        this.traverse(path);
      }
    });
});

read.js

#!/usr/bin/env node
const recast = require("recast");
const TNT = recast.types.namedTypes

recast.run(function(ast, printSource) {
  recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.node
        // 判断是否为ExpressionStatement,正确不输出,错误则全局报错
        TNT.ExpressionStatement.assert(node)
        this.traverse(path);
      }
    });
});

用AST修改源码,导出全部方法

我们想让这个文件中的函数改写成能够全部导出的形式,例如

function add (a, b) {
    return a + b
}

变成下面这样

exports.add = (a, b) => {
  return a + b
}

新建exportific.js文件

#!/usr/bin/env node
const recast = require("recast");
const {
  identifier:id,
  expressionStatement,
  memberExpression,
  assignmentExpression,
  arrowFunctionExpression,
  blockStatement
} = recast.types.builders

recast.run(function(ast, printSource) {
  // 一个块级域 {}
  console.log('\n\nstep1:')
  printSource(blockStatement([]))

  // 一个键头函数 ()=>{}
  console.log('\n\nstep2:')
  printSource(arrowFunctionExpression([],blockStatement([])))

  // add赋值为键头函数  add = ()=>{}
  console.log('\n\nstep3:')
  printSource(assignmentExpression('=',id('add'),arrowFunctionExpression([],blockStatement([]))))

  // exports.add赋值为键头函数  exports.add = ()=>{}
  console.log('\n\nstep4:')
  printSource(expressionStatement(assignmentExpression('=',memberExpression(id('exports'),id('add')),
    arrowFunctionExpression([],blockStatement([])))))
});

上面写了我们一步一步推断出exports.add = ()=>{}的过程,从而得到具体的AST结构体。
接下来,只需要在获得的最终的表达式中,把id('add')替换成遍历得到的函数名,把参数替换成遍历得到的函数参数,把blockStatement([])替换为遍历得到的函数块级作用域,就成功地改写了所有函数!

另外,我们需要注意,在commonDivision函数内,引用了sub函数,应改写成exports.sub
exportific.js

#!/usr/bin/env node
const recast = require("recast");
const {
  identifier: id,
  expressionStatement,
  memberExpression,
  assignmentExpression,
  arrowFunctionExpression
} = recast.types.builders

recast.run(function (ast, printSource) {
  // 用来保存遍历到的全部函数名
  let funcIds = []
  recast.types.visit(ast, {
    // 遍历所有的函数定义
    visitFunctionDeclaration(path) {
      //获取遍历到的函数名、参数、块级域
      const node = path.node
      const funcName = node.id
      const params = node.params
      const body = node.body

      // 保存函数名
      funcIds.push(funcName.name)
      // 这是上一步推导出来的ast结构体
      const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
        arrowFunctionExpression(params, body)))
      // 将原来函数的ast结构体,替换成推导ast结构体
      path.replace(rep)
      // 停止遍历
      return false
    }
  })


  recast.types.visit(ast, {
    // 遍历所有的函数调用
    visitCallExpression(path){
      const node = path.node;
      // 如果函数调用出现在函数定义中,则修改ast结构
      if (funcIds.includes(node.callee.name)) {
        node.callee = memberExpression(id('exports'), node.callee)
      }
      // 停止遍历
      return false
    }
  })
  // 打印修改后的ast源码
  printSource(ast)
})

你可能感兴趣的:(webpack4学习笔记3-AST入门)