在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