webpack-AST剖析

webpack-AST 目录


前言

  • 抽象语法树(AST),是一个非常重要的知识
  • JavaScript的语言精髓
  • 可以制作Vue, React之类的大型框架
  • 借鉴于思否 - 刘宇冲

推荐阅读

  • 《编译原理》

拆解函数

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

拆成三块:

  1. id:名字,add
  2. params:参数,[a, b]
  3. body:括号内的数据
  • add没办法继续向下拆解,是最基础的Identifier对象,用来作为函数的唯一标志
{
    name: 'add',
    type: 'identifier'
}
  • param拆解
[
    {
        name: 'a',
        type: 'identifier'
    },
    {
        name: 'b',
        type: 'identifier'
    }
]
  • body拆解
  1. body是一个BlockStatement块状域对象,表示{return a+b}
  2. BlockStatement包含一个ReturnStatement(return域)对象,表示return a+b
  3. ReturnStatement里包含一个BinaryExpression对象,表示a+b
  4. BinaryExpression包含了三部分left, operator, right
    • lefta
    • operator+
    • rightb

在这里插入图片描述

  • 这就是一个抽象语法树

AST工具 - recast

  • npm i recast -S

例子:

const recast = require("recast");

const code =
    `
    function add(a, b) {
        return a + 
            b;
    }
    `;

const ast = recast.parse(code);
const add = ast.program.body[0]

console.log(add);
  • node xx.js就会看到add的结构

在这里插入图片描述

-console.log(add.param[0])

在这里插入图片描述

  • console.log(add.body.body[0].argument.left)

在这里插入图片描述

制作模具 - recast.types.builders

  • function add(a, b) {}改成匿名函数声明const add = function(a, b) {}

如何改装

  1. 创建一个VariableDeclaration变量声明对象,声明为const,内容为VariableDeclaration对象
  2. 创建VariableDeclaratoradd.id在左边,右边是FunctionDeclaration对象
  3. 创建FunctionDeclarationid, params, body中,由于匿名函数的id为空,params使用add.paramsbody使用add.body
  4. 完成
//组件置入模具,并且组装
ast.program.body[0] = variableDeclaration("const", [
    variableDeclarator(add.id, functionExpression(
        null,
        add.params,
        add.body
    ))
]);

const output = recast.print(ast).code;
console.log(output);

在这里插入图片描述

  • const output = recast.print(ast).code其实是recast.parse的逆向过程,具体公式为
recast.print(recast.parse(source)).code === source

console.log(recast.prettyPrint(ast, {tabWidth: 2}).code)
  • 可以通过AST树生成任何JS代码

实战 - 命令行修改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;
}

命令行文件读取 - read.js - recast.run

const recast = require('recast');

recast.run(function (ast, printSource) {
    printSource(ast);
});
  • 输入node read demo.js,读取后转化为ast对象

在这里插入图片描述

  • printSource函数,可以将ast内容转换为源码

recast.visit - AST节点遍历

  • read.js
#!/usr/bin/env node
const recast = require('recast');

recast.run(function (ast, printSource) {
    recast.visit(ast, {
        visitExpressionStatement: function ({node}) {
            console.log(node);
            return false;
        }
    });
});
  • 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)
      }
    })
});

TNT - 判断AST对象类型

  • TNTrecast.types.namedTypes,判断AST对象是否为指定的类型
  • TNT.Node.assert():类型不匹配时,报错退出
  • TNT.Node.check():判断类型是否一致,输出False, true
  • 等价替换:TNT.ExpressionStatement.check(), TNT.FunctionDeclaration.assert()
#!/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);
      }
    });
});

#!/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
}
  1. 除了使用fs.read读取文本、正则匹配替换文本、fs.write写入文件的方法
  2. AST

Builder实现一个箭头函数

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([])))))
});

在这里插入图片描述

  • node exportific demo.js运行可查看结果

  • id('add')换成遍历的函数名

  • blockStatement([])替换为函数块级作用域

#!/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)
})

exportific前端工具

  1. 添加说明书: helprewrite模式,可以直接覆盖文件或默认为导出*.export.js文件
  2. printSource(ast)替换为writeASTFile(ast, filename, rewriteMode)
#!/usr/bin/env node
const recast = require("recast");
const {
  identifier: id,
  expressionStatement,
  memberExpression,
  assignmentExpression,
  arrowFunctionExpression
} = recast.types.builders

const fs = require('fs')
const path = require('path')
// 截取参数
const options = process.argv.slice(2)

//如果没有参数,或提供了-h 或--help选项,则打印帮助
if(options.length===0 || options.includes('-h') || options.includes('--help')){
  console.log(`
    采用commonjs规则,将.js文件内所有函数修改为导出形式。

    选项: -r  或 --rewrite 可直接覆盖原有文件
    `)
  process.exit(0)
}

// 只要有-r 或--rewrite参数,则rewriteMode为true
let rewriteMode = options.includes('-r') || options.includes('--rewrite')

// 获取文件名
const clearFileArg = options.filter((item)=>{
  return !['-r','--rewrite','-h','--help'].includes(item)
})

// 只处理一个文件
let filename = clearFileArg[0]

const writeASTFile = function(ast, filename, rewriteMode){
  const newCode = recast.print(ast).code
  if(!rewriteMode){
    // 非覆盖模式下,将新文件写入*.export.js下
    filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.')
  }
  // 将新代码写入文件
  fs.writeFileSync(path.join(process.cwd(),filename),newCode)
}


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)
      const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName),
        arrowFunctionExpression(params, body)))
      path.replace(rep)
      return false
    }
  })


  recast.types.visit(ast, {
    visitCallExpression(path){
      const node = path.node;
      if (funcIds.includes(node.callee.name)) {
        node.callee = memberExpression(id('exports'), node.callee)
      }
      return false
    }
  })

  writeASTFile(ast,filename,rewriteMode)
})
  • node exportific demo.js
exports.add = (a, b) => {
    return a + b;
};

exports.sub = (a, b) => {
    return a - b;
};

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

使用NPM发包

  • 编辑一下package.json文件
{
  "name": "exportific",
  "version": "0.0.1",
  "description": "改写源码中的函数为可exports.XXX形式",
  "main": "exportific.js",
  "bin": {
    "exportific": "./exportific.js"
  },
  "keywords": [],
  "author": "wanthering",
  "license": "ISC",
  "dependencies": {
    "recast": "^0.15.3"
  }
}
  • bin:将全局命令exportific指向当前目录下的exportific.js
  • 输入npm link 就在本地生成了一个exportific命令
  • 想导出来使用,就exportific XXX.js

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值