webpack-AST 目录
文章目录
前言
- 抽象语法树(
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 === 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
对象类型
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.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
}
- 除了使用
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.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
前端工具
- 添加说明书:
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.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