webpack-AST剖析

2024-09-05 11:48
文章标签 剖析 webpack ast

本文主要是介绍webpack-AST剖析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

webpack-AST 目录

文章目录

  • 前言
  • 推荐阅读
  • 拆解函数
  • `AST`工具 - `recast`
  • 制作模具 - `recast.types.builders`
    • 如何改装
  • 实战 - 命令行修改`js`文件
  • `recast.visit` - `AST`节点遍历
  • `TNT` - 判断`AST`对象类型
  • `AST`修改源码,导出全部方法
    • `Builder`实现一个箭头函数
  • `exportific`前端工具
  • 使用`NPM`发包


前言

  • 抽象语法树(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 === sourceconsole.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.nodeprintSource(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.namedTypesrecast.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.namedTypesrecast.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.buildersrecast.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.buildersrecast.run(function (ast, printSource) {// 用来保存遍历到的全部函数名let funcIds = []recast.types.visit(ast, {// 遍历所有的函数定义visitFunctionDeclaration(path) {//获取遍历到的函数名、参数、块级域const node = path.nodeconst funcName = node.idconst params = node.paramsconst 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.buildersconst 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).codeif(!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.nodeconst funcName = node.idconst params = node.paramsconst body = node.bodyfuncIds.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

这篇关于webpack-AST剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1138878

相关文章

org.hibernate.hql.ast.QuerySyntaxException:is not mapped 异常总结

org.hibernate.hql.ast.QuerySyntaxException: User is not mapped [select u from User u where u.userName=:userName and u.password=:password] 上面的异常的抛出主要有几个方面:1、最容易想到的,就是你的from是实体类而不是表名,这个应该大家都知道,注意

深度剖析AI情感陪伴类产品及典型应用 Character.ai

前段时间AI圈内C.AI的受够风波可谓是让大家都丈二摸不着头脑,连C.AI这种行业top应用都要找谋生方法了!投资人摸不着头脑,用户们更摸不着头脑。在这之前断断续续玩了一下这款产品,这次也是乘着这个风波,除了了解一下为什么这么厉害的创始人 Noam Shazeer 也要另寻他路,以及产品本身的发展阶段和情况! 什么是Character.ai? Character.ai官网:https://

最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)

文章目录 一、自动配置概念二、半自动配置(误~🙏🙏)三、源码分析1、验证DispatcherServlet的自动配置2、源码分析入口@SpringBootApplication3、@SpringBootConfiguration的@Configuration4、@EnableAutoConfiguration的@AutoConfigurationPackage和@Import5、Auto

C语言深度剖析--不定期更新的第四弹

哈哈哈哈哈哈,今天一天两更! void关键字 void关键字不能用来定义变量,原因是void本身就被编译器解释为空类型,编译器强制地不允许定义变量 定义变量的本质是:开辟空间 而void 作为空类型,理论上不应该开辟空间(针对编译器而言),即使开辟了空间,也只是作为一个占位符看待(针对Linux来说) 所以,既然无法开辟空间,也无法作为正常变量使用,既然无法使用,干脆编译器不让它编译变

Java CAS 原理剖析

在Java并发中,我们最初接触的应该就是synchronized关键字了,但是synchronized属于重量级锁,很多时候会引起性能问题,volatile也是个不错的选择,但是volatile不能保证原子性,只能在某些场合下使用。   像synchronized这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正

STL源码剖析之【二分查找】

ForwardIter lower_bound(ForwardIter first, ForwardIter last,const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置。      ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val

深入剖析 Redis 基础及其在 Java 应用中的实战演练

引言 在现代分布式系统和高并发应用中,缓存系统是不可或缺的一环,而 Redis 作为一种高性能的内存数据存储以其丰富的数据结构和快速的读写性能,成为了众多开发者的首选。本篇博客将详细介绍 Redis 的基础知识,并通过 Java 代码演示其在实际项目中的应用。 目录 什么是 Redis?Redis 数据结构详解Redis 与其他 NoSQL 数据库的对比在 Java 中使用 Redis 使用

我店平台商业模式深度剖析

“我店”平台采用了融合线上与线下资源,并以绿色积分为激励机制的创新商业模式。此模式旨在打造一个集购物、消费及积分兑换为一体的综合性服务平台。编辑v:qawsed2466。以下是对其商业模式的深入剖析: 平台定位与背景 “我店”由上海我店科技网络有限公司创立于2021年8月,作为一个本地生活服务平台,它致力于响应国家的环保政策,并运用绿色积分来促进经济活动,帮助实体店铺吸引客流。面对实体商业

剖析Cookie的工作原理及其安全风险

Cookie的工作原理主要涉及到HTTP协议中的状态管理。HTTP协议本身是无状态的,这意味着每次请求都是独立的,服务器不会保留之前的请求信息。为了在无状态的HTTP协议上实现有状态的会话,引入了Cookie机制。 1. Cookie定义 Cookie,也称为HTTP cookie、web cookie、互联网cookie或浏览器cookie,是一种用于在用户浏览网站时识别用户并为其准备

Yarn 源码 | 分布式资源调度引擎 Yarn 内核源码剖析

曾有人调侃:HBase 没有资源什么事情也做不了,Spark 占用了资源却没有事情可做?   那 YARN了解一下? 01 YARN! 伴随着Hadoop生态的发展,不断涌现了多种多样的技术组件 Hive、HBase、Spark……它们在丰富了大数据生态体系的同时,也引发了新的问题思考。   熟悉大数据底层平台的朋友,应该都了解这些为大数据场景设计的技术组件不仅个个都是消耗资源的大户,而且它们本