赞
踩
是源代码结构的一种抽象表示,以树状的形式表现编程语言的语法结构。树上的每个节点都表示源代码中的一种结构。
拆解一个简单的add函数
function add(a, b) {
return a + b
}
首先,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。
拆分成了三块:
一个id,就是它的名字,即add
两个params,就是它的参数,即[a, b]
一块body,也就是大括号内的一堆东西
我们发现,body其实是一个BlockStatement(块状域)对象,用来表示是{return a + b}
打开Blockstatement,里面藏着一个ReturnStatement(Return域)对象,用来表示return a + b
继续打开ReturnStatement,里面是一个BinaryExpression(二项式)对象,用来表示a + b
继续打开BinaryExpression,它成了三部分,left
,operator
,right
operator
即+
left
里面装的,是Identifier对象 a
right
里面装的,是Identifer对象 b
npm i recast
parse.js
// 给你一把"螺丝刀"——recast const recast = require("recast"); // 你的"机器"——一段代码 // 我们使用了很奇怪格式的代码,想测试是否能维持代码结构 const code = ` function add(a, b) { return a + // 有什么奇怪的东西混进来了 b } ` // 用螺丝刀解析机器 const ast = recast.parse(code); // ast可以处理很巨大的代码文件 // 但我们现在只需要代码块的第一个body,即add函数 const add = ast.program.body[0] console.log(add)
输入node parse.js
你可以查看到add函数的结构,与之前所述一致
你也可以继续使用console.log透视它的更内层,如:
console.log(add.params[0])
console.log(add.body.body[0].argument.left)
recast.types.builders里面提供了不少“模具”,可以轻松地拼接成新的代码。
最简单的例子,我们想把之前的function add(a, b){...}
声明,改成匿名函数式声明const add = function(a ,b){...}
如何改装?
第一步,我们创建一个VariableDeclaration变量声明对象,声明头为const, 内容为一个即将创建的VariableDeclarator对象。
第二步,创建一个VariableDeclarator,放置add.id在左边, 右边是将创建的FunctionDeclaration对象
第三步,我们创建一个FunctionDeclaration,如前所述的三个组件,id params body中,因为是匿名函数id设为空,params使用add.params,body使用add.body。
这样,就创建好了const add = function(){}
的AST对象。
在之前的parse.js代码之后,加入以下代码
// 引入变量声明,变量符号,函数声明三种“模具” const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders // 将准备好的组件置入模具,并组装回原来的ast对象。 ast.program.body[0] = variableDeclaration("const", [ variableDeclarator(add.id, functionExpression( null, // Anonymize the function expression. add.params, add.body )) ]); //将AST对象重新转回可以阅读的代码 const output = recast.print(ast).code; console.log(output)
可以看到,我们打印出了
const add = function(a, b) {
return a +
// 有什么奇怪的东西混进来了
b
};
最后一行
const output = recast.print(ast).code;
其实是recast.parse的逆向过程,具体公式为
recast.print(recast.parse(source)).code === source
打印出来还保留着“原装”的函数内容,连注释都没有变。
我们其实也可以打印出美化格式的代码段:
const output = recast.prettyPrint(ast, { tabWidth: 2 }).code
输出为
const add = function(a, b) {
return a + b;
};
除了parse/print/builder以外,Recast的三项主要功能:
创建一个用来示例文件,假设是demo.js
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
的文件,写入
read.js
recast.run( function(ast, printSource){
printSource(ast)
})
命令行输入
node read demo.js
可以查看js文件打印的内容。
我们可以知道,node read
可以读取demo.js
文件,并将demo.js内容转化为ast对象。
同时它还提供了一个printSource
函数,随时可以将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
}
});
});
recast.visit将AST对象内的节点进行逐个遍历。
#!/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)
}
})
});
调试时,如果你想输出AST对象,可以console.log(node)
如果你想输出AST对象对应的源码,可以printSource(node)
命令行输入 node read demo.js
进行测试。
TNT,即recast.types.namedTypes,它用来判断AST对象是否为指定的类型。
TNT.Node.assert(),当函数不能完好运行时(类型不匹配),就会报错退出
TNT.Node.check(),则可以判断类型是否一致,并输出False和True
上述Node可以替换成任意AST对象,例如TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()
read.js
#!/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); } }); });
read.js
#!/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);
}
});
});
命令行输入 node read demo.js
进行测试。
exportific.js
用AST实现将demo中的function能够全部导出的形式,例如
function add (a, b) {
return a + b
}
想改变为
exports.add = (a, b) => {
return a + b
}
exportific.js
推断出exports.add = ()=>{}
的过程,从而得到具体的AST结构体;
使用node exportific demo.js
运行可查看结果。
ExpressionStatement: 表达式语句节点,a = a + 1 或者 a++ 里边会有一个 expression 属性指向一个表达式节点对象(后边会提及表达式)。
memberExpression: 成员表达式节点,即表示引用对象成员的语句,object
是引用对象的表达式节点,property
是表示属性名称,computed
如果为 false
,是表示 .
来引用成员,property
应该为一个 Identifier
节点,如果 computed
属性为 true
,则是 []
来进行引用,即 property
是一个 Expression
节点,名称是表达式的结果值。
assignmentExpression: 赋值表达式节点,operator
属性表示一个赋值运算符,left
和 right
是赋值运算符左右的表达式
#!/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([]))))) });
接下来,只需要在获得的最终的表达式中,把id(‘add’)替换成遍历得到的函数名,把参数替换成遍历得到的函数参数,把blockStatement([])替换为遍历得到的函数块级作用域。
另外,我们需要注意,在commonDivision函数内,引用了sub函数,应改写成exports.sub
exportific.js
#!/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) })
可以通过简单的改写,就能通过recast制作成一个名为exportific的源码编辑工具。
以下代码添加作了两个小改动
exportific.js
#!/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
已经可以在当前目录下找到源码变更后的demo.export.js
文件了。
编辑一下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
命令。
之后,只要哪个js文件想导出来使用,就exportific XXX.js
一下。
这是在本地的玩法,想和大家一起分享这个前端小工具,只需要发布npm包就行了。
同时,一定要注意exportific.js文件头有
#!/usr/bin/env node
否则在使用时将报错。
浏览器最先就会把源码解析为抽象语法树,对浏览器而言AST的作用非常重要。
对开发者而言,AST的作用就是可以精准的定位到代码的任何地方,它就像是是你的手术刀,对代码进行一系列的操作。
常见的几种用途:代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
IDE的错误提示、格式化、高亮、自动补全等等
代码混淆压缩
UglifyJS2等
优化变更代码,改变代码结构使达到想要的结构
代码打包工具webpack、rollup等等
CommonJS、AMD、CMD、UMD等代码规范之间的转化
CoffeeScript、TypeScript、JSX等转化为原生Javascript
参考文档:
https://zhuanlan.zhihu.com/p/40102300
https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects
工具:http://esprima.org/demo/parse.html
fs:http://javascript.ruanyifeng.com/nodejs/fs.html#toc1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。