1. 程式人生 > >ECMA:JavaScript中AOP的一種實現

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的物件,其實使用的就是這種重新生成的,含有橫切邏輯的代理類的物件。

像是這種運用代理方式對程式碼進行橫向簡化和複用的思路,就是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);
    }

}