笼统语法树(AST),是一个异常基本而主要的知识点,但国内的文档却险些一片空白。
本文将带人人从底层相识AST,而且经由历程宣布一个小型前端东西,来带人人相识AST的壮大功用
Javascript就像一台精巧运作的机器,我们可以用它来完成统统天马行空的构想。
我们对javascript生态管窥蠡测,却常无视javascript自身。这台机器,究竟是哪些零部件在支持着它运转?
AST在一样平常营业中或许很难涉及到,但当你不止于想做一个工程师,而想做工程师的工程师,写出vue、react之类的大型框架,或相似webpack、vue-cli前端自动化的东西,或许有批量修正源码的工程需求,那你必需晓得AST。AST的才能非常壮大,且能帮你真正吃透javascript的言语精华。
事实上,在javascript天下中,你可以以为笼统语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”范畴了。
人生第一次拆解Javascript
小时候,当我们拿到一个螺丝刀和一台机器,人生中最使人思念的梦境时候便最先了:
我们把机器,拆成一个一个小零件,一个个齿轮与螺钉,用奇妙的机器道理连接在一同…
当我们把它从新照差别的体式格局组装起来,这时候,机器从新又跑动了起来——天下在你眼中如获重生。
经由历程笼统语法树剖析,我们可以像童年时拆解玩具一样,透视Javascript这台机器的运转,而且从新按着你的志愿来组装。
现在,我们拆解一个简朴的add函数
function add(a, b) {
return a + b
}
起首,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象。
用力拆开,它成了三块:
- 一个id,就是它的名字,即add
- 两个params,就是它的参数,即[a, b]
- 一块body,也就是大括号内的一堆东西
add没办法继承拆下去了,它是一个最基本Identifier(标志)对象,用来作为函数的唯一标志,就像人的姓名一样。
{
name: 'add'
type: 'identifier'
...
}
params继承拆下去,实际上是两个Identifier构成的数组。以后也没办法拆下去了。
[
{
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
-
operator
即+
-
left
内里装的,是Identifier对象a
-
right
内里装的,是Identifer对象b
就如许,我们把一个简朴的add函数拆解终了,用图表示就是
看!笼统语法树(Abstract Syntax Tree),的确是一种规范的树构造。
那末,上面我们提到的Identifier、Blockstatement、ReturnStatement、BinaryExpression, 这一个个小部件的说明书去哪查?
请检察 AST对象文档
送给你的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: ...
}
你也可以继承运用console.log透视它的更内层,如:
console.log(add.params[0])
console.log(add.body.body[0].argument.left)
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, // 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;
};
现在,你是不是是已产生了“我可以经由历程AST树天生任何js代码”的幻觉?
我慎重通知你,这不是幻觉。
实战进阶:敕令行修正js文件
除了parse/print/builder之外,Recast的三项主要功用:
- run: 经由历程敕令行读取js文件,并转化成ast以供处置惩罚。
- tnt: 经由历程assert()和check(),可以考证ast对象的范例。
- visit: 遍历ast树,猎取有用的AST对象并举行变动。
我们经由历程一个系列小务来进修悉数的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
}
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);
}
});
});
敕令行输入`
node read demo.js`举行测试。
实战:用AST修正源码,导出悉数要领
exportific.js
现在,我们想让这个文件中的函数改写成可以悉数导出的情势,比方
function add (a, b) {
return a + b
}
想改变成
exports.add = (a, b) => {
return a + b
}
除了运用fs.read读取文件、正则婚配替代文本、fs.write写入文件这类愚笨的体式格局外,我们可以用AST文雅地解决问题。
查询AST对象文档
起首,我们先用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前端东西
上面讲了那末多,依然只体现在理论阶段。
但经由历程简朴的改写,就可以经由历程recast制造成一个名为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
一下。
这是在当地的弄法,想和人人一同分享这个前端小东西,只须要宣布npm包就好了。
同时,肯定要注重exportific.js文件头有
#!/usr/bin/env node
不然在运用时将报错。
接下来,正式宣布npm包!
假如你已有了npm 帐号,请运用npm login
登录
假如你还没有npm帐号 https://www.npmjs.com/signup 异常简朴就可以注册npm
然后,输入npm publish
没有任何烦琐步骤,涓滴考核都没有,你就宣布了一个有用的前端小东西exportific 。任何人都可以经由历程
npm i exportific -g
全局装置这一个插件。
提醒:==在实验教程时,请不要和我的包重名,修正一下发包称号。==
结语
我们对javascript再熟习不过,但透过AST的视角,最一般的js语句,却焕发出精心动魄的美感。你可以经由历程它批量构建任何javascript代码!
童年时,这个天下充满了新颖的玩具,再一般的东西在你眼中都犹如珍宝。现在,计算机言语就是你手中的大玩具,一段段AST对象的拆分组装,构建出我们所生涯的网络天下。
所以不得不说软件工程师是一个幸运的事情,你心中住的依然是谁人午后的少年,永久有无数新颖等你发明,永久有无数妄想等你构建。
github地点:https://github.com/wanthering…