詳解使用抽象語(yǔ)法樹(shù)AST實(shí)現(xiàn)一個(gè)AOP切面邏輯
開(kāi)篇
AST 功能很靈活,可以通過(guò)改變一些自定義結(jié)構(gòu)便可以輸入自定義的功能,下面簡(jiǎn)單的展示下如何利用抽象語(yǔ)法樹(shù)AST實(shí)現(xiàn)一個(gè)AOP切面邏輯
一、實(shí)現(xiàn)目的
將 js 文件下的全部方法添加一個(gè)開(kāi)始事件和一個(gè)結(jié)束事件并傳入方法名,以便監(jiān)聽(tīng)某個(gè)方法的調(diào)用。
1、work.js
function eat(){
let food = "apple"
let time = '2小時(shí)'
return '吃' + food + '用時(shí)' + time
}
function work(){
}
console.log(eat())
控制臺(tái)打印

可以看到,輸入了執(zhí)行 eat 方法的開(kāi)始事件及結(jié)束事件。其實(shí),這個(gè)過(guò)程就是 work.js 轉(zhuǎn)成語(yǔ)法樹(shù)后,對(duì)方法節(jié)點(diǎn)進(jìn)行了處理,添加了兩個(gè)切面方法:start 和 end 事件。
2、aop.js
這個(gè)文件用來(lái)聲明 start 和 end 事件。
function start(funcName){
console.log('開(kāi)始執(zhí)行:' + funcName + '方法')
}
function end(funcName){
console.log('執(zhí)行結(jié)束:' + funcName + '方法')
}
二、利用語(yǔ)法樹(shù)添加切面事件
//文件操作工具
const fs = require('fs');
//JS代碼轉(zhuǎn)語(yǔ)法樹(shù)工具
const parser = require('./babel-core/node_modules/babylon');
//語(yǔ)法樹(shù)遍歷工具
const traverse = require('./babel-core/node_modules/babel-traverse');
//語(yǔ)法樹(shù)轉(zhuǎn)JS代碼工具
const generator = require('./babel-core/node_modules/babel-generator');
//聲明語(yǔ)法樹(shù)類型工具
const t = require('./babel-core/node_modules/babel-types');
//獲取aop代碼
let aop = fs.readFileSync('./aop.js','utf-8')
//獲取需要添加切面的代碼
let content = fs.readFileSync('./work.js','utf-8')
//將需要添加切面的代碼轉(zhuǎn)換為語(yǔ)法樹(shù)
let ast = parser.parse(content,{
sourceType:'script'
})
//遍歷指定語(yǔ)法樹(shù)時(shí)操作項(xiàng)(這里遍歷的指定節(jié)點(diǎn)是FunctionDeclaration方法)
const visitor = {
FunctionDeclaration:({ node }) => {
//獲取每個(gè)方法體里面的節(jié)點(diǎn)
let funcBody = node.body.body
//創(chuàng)建一個(gè)名為start,參數(shù)為當(dāng)前方法名的執(zhí)行節(jié)點(diǎn),后面的參數(shù)為創(chuàng)建一個(gè)名為start方法的參數(shù)
let start = t.callExpression(t.identifier('start'), [t.identifier(`"${node.id.name}"`)])
//添加到方法的最前面
funcBody.unshift(start)
//判斷最后一個(gè)節(jié)點(diǎn)是不是return,方式結(jié)束事件調(diào)用節(jié)點(diǎn)在最后無(wú)法調(diào)用。
let lastNode = funcBody.slice(-1)
//創(chuàng)建一個(gè)名為end,參數(shù)為當(dāng)前方法名的執(zhí)行節(jié)點(diǎn),后面的參數(shù)為end方法的參數(shù)
let end = t.callExpression(t.identifier('end'), [t.identifier(`"${node.id.name}"`)])
//設(shè)定end節(jié)點(diǎn)添加的位置
let insertEndEventPosition = (funcBody.length)
if(lastNode[0].type == 'ReturnStatement'){
//放在return節(jié)點(diǎn)的前面
insertEndEventPosition -= 1
}
//添加end節(jié)點(diǎn)
funcBody.splice(insertEndEventPosition,0,end)
}
}
//開(kāi)始遍歷操作語(yǔ)法樹(shù)
traverse.default(ast,visitor)
//將處理完的語(yǔ)法樹(shù)再次轉(zhuǎn)換為JS代碼
let codeResult = generator.default(ast)
//這里需要添加aop里面的兩個(gè)切面事件到最終的JS代碼里。
let outFileCode = aop + '\n\n' + codeResult.code
// 寫(xiě)入文件操作
fs.mkdir('cache',(err)=>{
if(!err){
fs.writeFile('cache/main.js',outFileCode,(err)=>{
if(!err){
console.log('文件創(chuàng)建完成')
}
})
}
})
上面的代碼執(zhí)行完后,看下 main.js 生成的代碼內(nèi)容。
function start(funcName){
console.log('開(kāi)始執(zhí)行:' + funcName + '方法')
}
function end(funcName){
console.log('執(zhí)行結(jié)束:' + funcName + '方法')
}
function eat() {
start("eat")
let food = "apple";
let time = '2小時(shí)';
end("eat")
return '吃' + food + '用時(shí)' + time;
}
function work() {
start("work")
end("work")
}
console.log(eat());
相比較之前的 work.js 文件,添加了兩個(gè)切面方法的同時(shí),保證每個(gè)方法都添加了 start 及 end 事件。這樣再調(diào)用的時(shí)候便可直接在執(zhí)行前后操作其他任務(wù)了。
三、總結(jié)與思考
其實(shí)上面的操作人工也可以直接操作,但是,將一切重復(fù)單一的工作交給程序,這或許是代碼存在的真正意義。一個(gè)簡(jiǎn)單的例子或許能打開(kāi)一個(gè)新的世界,雖然,每一行代碼都是獨(dú)一無(wú)二的,但是如果追本溯源,最初并且真切的思想?yún)s是相通的
以上就是詳解使用抽象語(yǔ)法樹(shù)AST實(shí)現(xiàn)一個(gè)AOP切面邏輯的詳細(xì)內(nèi)容,更多關(guān)于AST抽象語(yǔ)法樹(shù)實(shí)現(xiàn)AOP的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
package-lock.json解決依賴的版本管理使用詳解
這篇文章主要為大家介紹了package-lock.json解決依賴的版本管理使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
如何使用Node寫(xiě)靜態(tài)文件服務(wù)器
這篇文章主要介紹了如何使用Node寫(xiě)靜態(tài)文件服務(wù)器,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-09-09
前端node Session和JWT鑒權(quán)登錄示例詳解
關(guān)于前端鑒權(quán)登錄是比較常見(jiàn)的需求了,本文將從服務(wù)端渲染和前后端分離的不同角度下演示鑒權(quán),為大家介紹前端node Session和JWT鑒權(quán)登錄示例詳解2022-07-07

