本文主要是介绍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:名字,addparams:参数,[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+bReturnStatement里包含一个BinaryExpression对象,表示a+bBinaryExpression包含了三部分left, operator, rightleft:aoperator:+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剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!