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的调用方法

方法参数含义备注
callobject,argument…this代指的对象,(多个)函数参数,需要几个直接往后面加就行,他是可变的参数是多个的,需要多少都可以写进去,第一个是函数内部的this
applyobject,argumentsthis代指的对象,函数参数数组参数是一个数组,第一个也是函数内部的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);
    }

}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值