本文主要是介绍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;
}
拆成三块:
id
:名字,add
params
:参数,[a, b]
body
:括号内的数据
add
没办法继续向下拆解,是最基础的Identifier
对象,用来作为函数的唯一标志
{name: 'add',type: 'identifier'
}
param
拆解
[{name: 'a',type: 'identifier'},{name: 'b',type: 'identifier'}
]
body
拆解
body
是一个BlockStatement
块状域对象,表示{return a+b}
BlockStatement
包含一个ReturnStatement
(return
域)对象,表示return a+b
ReturnStatement
里包含一个BinaryExpression
对象,表示a+b
BinaryExpression
包含了三部分left, operator, right
left
:a
operator
:+
right
:b
- 这就是一个抽象语法树
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) {}
如何改装
- 创建一个
VariableDeclaration
变量声明对象,声明为const
,内容为VariableDeclaration
对象 - 创建
VariableDeclarator
,add.id
在左边,右边是FunctionDeclaration
对象 - 创建
FunctionDeclaration
,id, params, body
中,由于匿名函数的id
为空,params
使用add.params
,body
使用add.body
- 完成
//组件置入模具,并且组装
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
对象类型
TNT
:recast.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
}
- 除了使用
fs.read
读取文本、正则匹配替换文本、fs.write
写入文件的方法 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
前端工具
- 添加说明书:
help
,rewrite
模式,可以直接覆盖文件或默认为导出*.export.js
文件 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剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!