在javascript世界中,你可以認為抽象語法樹(AST)是最底層, 再往下,就是關于轉換和編譯的“黑魔法”領域了,
現在,我們拆解一個簡單的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物件aright里面裝的,是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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/168253.html
標籤:JavaScript
