ECMA:JavaScript中AOP的一種實現
相比Javascript,果然還是Java好用的多。
隨著JavaScript的發展,目前的JS已經支援Class等多種新的關鍵字,當然,也就多了很多新的坑。
今天發現某個專案中,突然出現了很多JWT異常,觀察了一下,JWT不知道為啥變成了null,不知道是那個前輩搞的,如果在發出請求之前,可以先對token進行判斷,就不會由於這種問題導致後端大量報錯甚至狗帶了。
但是由於一些原因,傳送http請求的方法很多,比如說getXXXX,然後postXXXX這樣的,一個一個寫顯然很麻煩,而且會有大量程式碼重複,我就想到了Spring對這種情況的處理方式,就是AOP,通過CGLib可以對Java的Class進行代理,並且攔截相關方法,織入橫切邏輯。
但是在js中,找了找沒有類似的類庫,這就比較麻煩了。
文章目錄
AOP是什麼
AOP,指面向切面程式設計,是另外一種程式設計思路,對面向物件是一種補充,在面向物件程式設計中,有些時候會遇到這種問題:既,一些功能相同的邏輯會包圍另外一些邏輯。
或者這樣說,我希望在某些方法被呼叫的開始或者執行的結束,或者是這個方法的丟擲了異常的時候進行一些額外的動作,那麼有幾種做法呢?
首先就是直接將程式碼放到方法中,這樣的話,這些本來不屬於這個方法的功能的程式碼就會擾亂方法本身的結構,讓整體不方便後續的閱讀,而且如果在很多的方法都需要這樣的處理,也會導致程式碼的重複。當然,也可以採用繼承的手法,不過如果這些方法是屬於多種Class,繼承也就不是很合適了。
AOP會在方法開始或者結束或者跑出異常等特定的場景下被觸發和執行,這樣原本包圍在程式碼前後的無關邏輯就會被抽出,從而實現複用和簡化。
那麼這是怎麼做到的呢?
代理。代理是用另外一個代理物件
代替本來的物件,在Java中,一般會在原始類的基礎上生成一個新的類,這個新的類就包含了需要的,原本被抽取的邏輯部分,這一部分被抽取的邏輯也稱為橫切邏輯
像是這種運用代理方式對程式碼進行橫向簡化和複用的思路,就是AOP。
JavaScript實現AOP的基本思路
首先,要得到一個Object的所有Method(連線點,可以被其他邏輯包圍的程式碼部分,一般是方法),然後根據某種規則進行匹配(切點,我們要替換掉,使用橫切邏輯的程式碼部分),然後生成另一個Function(切面,包含了橫切邏輯),讓這個Function代替原有Function(織入,把橫切程式碼和原本的方法合併的過程),並且在這裡面執行橫切邏輯。
為了達到這個目的,就要知道怎麼得到一個JavaScript中Object的所有Method,最開始我是這樣想的:
class Http {
getNews(type){
// weex原生的ajax
}
getUser(id){
// weex原生的ajax
}
// 省略部分方法
}
let instance = new Http();
// 遍歷這個instance
for(let item in instance){
console.log(item);
}
我認為他應該就能夠打印出所有的Method的名字,然而,啥也沒有,這就比較鬱悶了。
語法 | 適用範圍 | 備註 |
---|---|---|
for(let elem in obj) | 可迭代物件,例如陣列,可以得到key(鍵) | ES6的Class的例項是得不到裡面的方法的 |
for(let elem of obj) | 陣列之類的,可以得到內容(value) |
後來發現ES6的物件,自身有一個欄位叫做__proto__
這個欄位是不可以迭代的,在這裡面就有Class的各種method,可以通過Object.getOwnPropertyNames
這個方法得到方法名稱的陣列。
既然是方法名稱,想來拿他匹配切點應該沒問題,因此我使用正則表示式作為切點的條件,只要方法名字可以被正則匹配到,那麼就對他進行代理。
那麼剩下的問題就是該怎麼使用這個被攔截到的方法了。
Function的呼叫方法
方法 | 引數 | 含義 | 備註 |
---|---|---|---|
call | object,argument… | this代指的物件,(多個)函式引數,需要幾個直接往後面加就行,他是可變的 | 引數是多個的,需要多少都可以寫進去,第一個是函式內部的this |
apply | object,arguments | this代指的物件,函式引數陣列 | 引數是一個數組,第一個也是函式內部的this |
Function的呼叫方法有apply和call,就是在javascript中實現AOP的關鍵。
apply方法接收兩個引數,第一個是this,在Function內的this指的就是這個引數的物件,另一個是arguments,這是一個數組,包含了這個Function接收到的全部引數。
call方法接收不限制個數的引數,第一個也是Function內的this,剩下的就是function的引數,需要什麼直接填就行。
前置增強和後置增強
export class Aspect {
/**
* 對某個物件新增前置增強
* @param object 代理目標
* @param regexp 切入點,使用正則表示式,按照方法名進行切入
* @param func 待織入的增強
*/
static beforeAdvice(object,regexp,func){
let names = [];
let isES6 = .isES6Instance(object);
// 獲取方法列表
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
for (let item of names){
// 查詢切入點,使用正則表示式匹配名稱
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
let target =isES6?object.__proto__[item]:object[item];
// 生成代理方法,織入切面
let proxyed = function () {
try {
// 呼叫前置增強,傳入當期方法名稱以及引數
func.call(object,item,arguments);
// 呼叫原始方法並返回
return target.apply(object,arguments);
}catch (e) {
}
};
// 替換原有方法,完成橫切
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
/**
* 為目標物件新增後置增強
* @param object 代理目標
* @param regexp 切入點
* @param func 待織入增強
*/
static afterAdvice(object,regexp,func){
// 獲取方法名稱列表
let names = [];
let isES6 = .isES6Instance(object);
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
for (let item of names){
// 查詢切入點,使用正則表示式匹配方法名
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
let target =isES6?object.__proto__[item]:object[item];
// 生成代理方法,織入切面
let proxyed = function () {
try {
// 呼叫原始方法,保留返回值
let result = target.apply(object,arguments);
// 呼叫後置增強
func.call(object,item,arguments);
// 返回原方法的結果
return result;
}catch (e) {
}
};
// 替換原始方法,完成橫切
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
static isES6Instance(obj){
if (typeof obj.__proto__ !== 'undefined' && obj.__proto__ != null){
return true
}else{
return false;
}
}
}
其實應該還有其他幾個AOP增強,不過其實原理都差不多。
環繞增強
不同於之前的幾個增強,環繞增強需要傳遞一個執行物件給切面,讓切面決定什麼時候執行原始方法,因此需要一個新的class對這個進行描述。
/**
* 切入點
* 用於封裝原始方法,提供一個process方法用於執行原始方法。
* 原始方法的返回值既process的返回值
*/
class ExcPoint {
constructor(target,scope,param){
this.target = target;
this.scope = scope;
this.param = param;
}
process(){
return this.target.apply(this.scope,this.param);
}
}
然後,在Aspect的class中增加環繞增強的編織方法:
static asyncAroundAdvice(object,regexp,func){
// 獲取方法名稱列表
let names = [];
let isES6 = Aspect.isES6Instance(object);
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
for (let item of names){
// 尋找切入點,通過正則表示式對方法名稱進行匹配
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
let target = isES6 ? object.__proto__[item] : object[item];
// 生成代理方法,編織切面
let proxyed = function () {
try {
// 封裝原始方法
let point = new ExcPoint(target,object,arguments);
// 返回切面的處理結果
return func.call(object,item,point);
}catch (e) {
}
};
// 替換原有方法,完成橫切
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
其實到了這裡可以發現除了返回不太一樣,執行的時機不太一樣,其他部分是完全可以進行整合的,這一部分整合之後程式碼將會更加簡潔。
完整的AOP工具類
export class Aspect {
/**
* 對某個物件新增前置增強
* @param object 代理目標
* @param regexp 切入點,使用正則表示式,按照方法名進行切入
* @param func 待織入的增強
*/
static beforeAdvice(object,regexp,func){
Aspect.aspectAware(object,regexp,func,0)
}
/**
* 為目標物件新增後置增強
* @param object 代理目標
* @param regexp 切入點
* @param func 待織入增強
*/
static afterAdvice(object,regexp,func){
Aspect.aspectAware(object,regexp,func,1)
}
/**
* 為目標物件新增環繞增強
* @param object 代理目標
* @param regexp 切入點
* @param func 待織入的增強
*/
static aroundAdvice(object,regexp,func){
Aspect.aspectAware(object,regexp,func,2)
}
/**
* 為目標新增異常時增強
* @param object 代理目標
* @param refexp 切入點
* @param func 待織入增強
*/
static afterThrowingAdvice(object,refexp,func){
Aspect.aspectAware(object,refexp,func,3)
}
/**
* 為目標物件新增增強
* @param object 目標物件
* @param regexp 切入點
* @param func 待織入增強
* @param type 0:前置 1:後置 2:環繞 3:異常
*/
static aspectAware(object,regexp,func,type){
let names = [];
let isES6 = Aspect.isES6Instance(object);
// 獲取方法列表
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
// 尋找切入點
for (let item of names){
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
// 被代理的目標方法
let target = isES6 ? object.__proto__[item] : object[item];
let proxyed = null;
// 根據選擇型別的不同生成不同的代理方法
if(type === 0){
// 前置增強
proxyed = function () {
func.call(object,item,arguments);
return target.apply(object,arguments);
};
}else if(type === 1){
// 後置增強
proxyed = function () {
let result = target.apply(object,arguments);
func.call(object,item,arguments);
return result;
};
}else if(type === 2){
// 環繞增強
proxyed = function () {
let point = new ExcPoint(target,object,arguments);
return func.call(object,item,point);
};
}else if(type === 3){
// 異常時增強
proxyed = function () {
try {
return target.apply(object,arguments);
}catch (e) {
return func.call(object,item,arguments);
}
}
}
// 替換原有方法,完整橫切織入
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
/**
* 判別物件是否為ES6下的例項
* @param obj
* @returns {boolean}
*/
static isES6Instance(obj){
if (typeof obj.__proto__ !== 'undefined' && obj.__proto__ != null){
return true
}else{
return false;
}
}
}
class ExcPoint {
constructor(target,scope,param){
this.target = target;
this.scope = scope;
this.param = param;
}
process(){
return this.target.apply(this.scope,this.param);
}
}