在javascript世界中,你能够认为抽象语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”领域了。javascript
如今,咱们拆解一个简单的add函数前端
function add(a, b) { return a + b }
首先,咱们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。java
用力拆开,它成了三块:node
- 一个id,就是它的名字,即add
- 两个params,就是它的参数,即[a, b]
- 一块body,也就是大括号内的一堆东西
add没办法继续拆下去了,它是一个最基础Identifier(标志)对象,用来做为函数的惟一标志。express
{ name: 'add' type: 'identifier' ... }
params继续拆下去,实际上是两个Identifier组成的数组。以后也没办法拆下去了。npm
[ { name: 'a' type: 'identifier' ... }, { name: 'b' type: 'identifier' ... } ]
接下来,咱们继续拆开body 咱们发现,body实际上是一个BlockStatement(块状域)对象,用来表示是{return a + b}
json
打开Blockstatement,里面藏着一个ReturnStatement(Return域)对象,用来表示return a + b
segmentfault
继续打开ReturnStatement,里面是一个BinaryExpression(二项式)对象,用来表示a + b
数组
继续打开BinaryExpression,它成了三部分,left
,operator
,right
ide
operator
即+
left
里面装的,是Identifier对象a
right
里面装的,是Identifer对象b
就这样,咱们把一个简单的add函数拆解完毕。
抽象语法树(Abstract Syntax Tree),的确是一种标准的树结构。
那么,上面咱们提到的Identifier、Blockstatement、ReturnStatement、BinaryExpression, 这一个个小部件的说明书去哪查?
请查看 AST对象文档
recast
输入命令:npm i recast -S
你便可得到一把操纵语法树的螺丝刀
接下来,你能够在任意js文件下操纵这把螺丝刀,咱们新建一个parse.js示意:
建立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函数的结构,与以前所述一致,经过AST对象文档可查到它的具体属性:
FunctionDeclaration{ type: 'FunctionDeclaration', id: ... params: ... body: ... }
recast.types.builders 制做模具
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, // 匿名化函数表达式. add.params, add.body )) ]); //将AST对象从新转回能够阅读的代码 //这一行实际上是recast.parse的逆向过程,具体公式为 //recast.print(recast.parse(source)).code === source const output = recast.print(ast).code; console.log(output)
打印出来还保留着“原装”的函数内容,连注释都没有变。
咱们其实也能够打印出美化格式的代码段:
const output = recast.prettyPrint(ast, { tabWidth: 2 }).code
//输出为 const add = function(a, b) { return a + b; };
实战进阶:命令行修改js文件
除了parse/print/builder之外,Recast的三项主要功能:
- run: 经过命令行读取js文件,并转化成ast以供处理。
- tnt: 经过assert()和check(),能够验证ast对象的类型。
- visit: 遍历ast树,获取有效的AST对象并进行更改。
经过一个系列小务来学习所有的recast工具库:
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 }
###recast.run 命令行文件读取
新建一个名为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的内容转换回源码,以方便调试。
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 } }); });
recast.visit将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) } }) });
调试时,若是你想输出AST对象,能够console.log(node)
若是你想输出AST对象对应的源码,能够printSource(node)
命令行输入 node read demo.js
进行测试。
#!/usr/bin/env node
在全部使用recast.run()
的文件顶部都须要加入这一行,它的意义咱们最后再讨论。
TNT 判断AST对象类型
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); } }); });
实战:用AST修改源码,导出所有方法
###exportific.js
如今,咱们想让这个文件中的函数改写成可以所有导出的形式,例如
function add (a, b) { return a + b }
想改变为
exports.add = (a, b) => { return a + b }
首先,咱们先用builders凭空实现一个键头函数
#####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([]))))) });
上面写了咱们一步一步推断出exports.add = ()=>{}
的过程,从而获得具体的AST结构体。
使用node exportific demo.js
运行可查看结果。
接下来,只须要在得到的最终的表达式中,把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) })
一步到位,发一个最简单的exportific前端工具
如下代码添加做了两个小改动
- 添加说明书--help,以及添加了--rewrite模式,能够直接覆盖文件或默认为导出*.export.js文件。
- 将以前代码最后的 printSource(ast)替换成 writeASTFile(ast,filename,rewriteMode)
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
文件了。
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
命令。
以后,只要哪一个js文件想导出来使用,就exportific XXX.js
一下。
必定要注意exportific.js文件头有
#!/usr/bin/env node
接下来,正式发布npm包!
若是你已经有了npm 账号,请使用npm login
登陆
若是你尚未npm账号 https://www.npmjs.com/signup 很是简单就能够注册npm
而后,输入 npm publish
没有任何繁琐步骤,丝毫审核都没有,你就发布了一个实用的前端小工具exportific 。任何人均可以经过
npm i exportific -g
全局安装这一个插件。
提示:在试验教程时,请不要和个人包重名,修改一下发包名称。
#!/usr/bin/env node
不一样用户或者不一样的脚本解释器有可能安装在不一样的目录下,系统如何知道要去哪里找你的解释程序呢? /usr/bin/env
就是告诉系统能够在PATH目录中查找。 因此配置#!/usr/bin/env node
, 就是解决了不一样的用户node路径不一样的问题,可让系统动态的去查找node来执行你的脚本文件。
若是出现No such file or directory
的错误?由于你的node安装路径没有添加到系统的PATH中。因此去进行node环境变量配置就能够了。
要是你只是想简单的测试一下,那么你能够经过which node
命令来找到你本地的node安装路径,将/usr/bin/env
改成你查找到的node路径便可。
参考文章:https://segmentfault.com/a/1190000016231512?utm_source=tag-newest#comment-area