前言
本意不想用太长的篇幅,来阐述这个话题。但是有些概念和设计初衷有必要讲清楚,以便于搞清楚其深层次的内在逻辑。这是我一直遵从的“知其然,知其所以然”的原则。首先,本文将简单的阐述一下概念;进而,举例一起探讨一下前端AOP实现方式,以及随着前端语言ES5、ES6、Typescript的发展,实现方式的演变;最后,一起了解一下实际项目中AOP的应用场景、相关框架以及开发模式。
什么是AOP?
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。主要意图是将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
简而言之,就是“优雅”的把“辅助功能逻辑”从“业务逻辑”中分离解耦。
简单说一下AOP中的一些概念
确切说是其实是AspectJ中的相关概念。AspectJ是一个扩展了Java语言的面向切面的框架。其优秀的设计思想值得我们参考学习。
AspectJ的主要相关概念有下面这些:
-
连接点(Joinpoint):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等
-
切入点(Pointcut):表示一组Joinpoint,这些Joinpoint或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了将要放置增强Advice的地方。比如"所有类的submit和reset方法调用执行”
-
切面(Aspect):由切点和增强组成,既包含了增强逻辑的具体实现和连接点的定义
-
通知/增强(Advice):具体的增强逻辑实现。通常依据放置的地方不同,可分为前置通知(Before)、后置返回通知(AfterReturning)、后置异常通知(AfterThrowing)、后置通知(After)与围绕通知(Around)5种
-
织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程
为了便于理解,我们可以把一个业务流程比作一个面包,那么面包切片就是一个业务模块或者功能。类似于“某个切面的前面”,“某个切片的后面”这种描述叫做连接点(Joinpoint);把花生酱、面包、生菜打包成一个独立模块进行逻辑处理,就是一个切面(Aspect);切面可以定义通知(Advice)、切入点(Pointcut)也即放入通知的连接点;把切面跟面包结合,从而制成三明治这个过程叫做织入(Weaving)。
早期前端AOP的简单实现
多数的前端的编程思想、设计模式、框架以及工具,都是参考服务端已经应用成熟的设计思路实现的,AOP也不例外。我们前面已经讲过一些AspectJ的相关概念。很明显,下面我们打算参照其用法,逐步实现前端AOP编程。
通过前面对AspectJ的概念的分析,可以得出一个简略的总结:AOP编程就是在不破坏原有代码的基础上,在原执行动作之前或者之后,加入一段自定义的动作。这里的“原有动作”是函数,“增强的动作”也是函数,把他们打包成一个动作也是函数。显然,基于javascript语言的特性,函数式编程是实现前端AOP编程的最佳方式。
看下面的简单实现:
/**
* @param {Function} oldFn 原函数
* @param {Function} before 前置函数
* @param {Function} after 后置函数
* @return {Function} 新函数
*/
var adviceAction = function(oldFn, before, after){
return function(){
var args = [].slice.call(arguments)
try{
// 增强
before.apply(null, args)
}catch(e){}
// 原函数
var rst = oldFn.apply(this, args)
try{
// 增强
after.apply(null, args)
}catch(e){}
return rst
}
}
调用执行:
function run(){
console.log('run')
}
function before(jp){
console.log('before action')
}
function after(jp){
console.log('after action')
}
var newFn = adviceAction(run, before, after)
newFn()
执行结果
这里面通过传参匹配到的"oldFn的调用执行"对应切入点(PointCut);before和after对应通知/增强(Advice);创造newFn的过程可以对应织入(Weaving)动作。另外,还可以参考AspectJ的用法,简单模拟一下每个增强对应的连接点(JoinPoint)信息。
var adviceAction = function(oldFn, before, after){
return function(){
var args = [].slice.call(arguments)
var joinPoint = {
target:null,
key: oldFn.name,
method: oldFn.bind(this),
context: this,
args: args
}
try{
// 增强
before.apply(null, [joinPoint])
}catch(e){}
// 原函数
var rst = oldFn.apply(this, args)
try{
// 增强
after.apply(null, [joinPoint])
}catch(e){}
return rst
}
}
测试调用:
// ......省略
function before(jp){
console.log('before action:' +jp.key)
}
function after(jp){
console.log('after action:' + jp.key)
}
// .......省略
执行结果
至此,已经可以初见前端AOP编程的雏形。前面例子已经简单实现过前置通知(Before)和后置终于通知(After)。还有后置返回通知(AfterReturning)、后置异常通知(AfterThrowing)、与围绕通知(Around)的具体实现没有讨论。接下来,我们将简单的模拟实现一个简单完整的AspectJS。
-
Before标识的方法为前置方法,在目标方法的执行之前执行,即在连接点之前进行执行。
-
Around环绕通知方法可以包含上面四种通知方法,环绕通知的功能最全面。且环绕通知必须有返回值, 返回值即为目标方法的返回值。对应JoinPoint需要附带可执行原执行动作的方法(invoke)
比如我们需要计算原函数的执行时间
function around(joinPoint){
var startTime = Date.now()
var rst = joinPoint.invoke()
var howLong = Dte.now - startTime
return rst
}
- AfterThrowing异常通知方法只在连接点方法出现异常后才会执行,否则不执行。因此对应JoinPoint需要附带异常参数(exception)
try{
var rst = oldFn.apply(this, args)
// returning
return rst
}catch(err){
joinPoint.exception = err
throwing.apply(null, [joinPoint])
}finally{
//after
}
- AfterReturning当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。返回通知方法在目标方法执行成功后才会执行,返回通知方法可以拿到目标方法(连接点方法)执行后的结果。因此对应的JoinPoint需要附带执行结果(result)
try{
var rst = oldFn.apply(this, args)
joinPoint.result = rst
returning.apply(null,[joinPoint])
return rst
}catch(err){
// throwing
}finally{
//after
}
- After后置通知方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法
// brefore
try{
// around or oldFn
// returning
}catch(err){
// throwing
}finally{
after.apply(null, [joinPoint])
}
下面完整的实现一下这个简略版的AOP工具
/**
* @param {Function} target 对象或者类
* @param {String} methodKey 方法名称
* @param {Object} advices 增强 {before?:Function, afeter?: Function, throwing?: Fuction, around?: Function, returning?: Function}
*/
var adviceAction = function (target, methodKey, advices) {
var context = target
target = typeof target === 'function' ? target.prototype : target
var oldFn = target[methodKey]
Object.defineProperty(target, methodKey, {
value: _decorator,
writeable:true
})
function _decorator() {
var args = [].slice.call(arguments)
var that = this,
rst
var joinPoint = {
target: target,
key: methodKey,
method: oldFn.bind(this),
context: this,
args: args
}
if (advices) {
var before = advices.before,
after = advices.after,
throwing = advices.throwing,
around = advices.around,
returning = advices.returning
}
if (typeof before === 'function') {
try {
// 前置增强
before.apply(null, [joinPoint])
} catch (e) { }
}
try {
if (typeof around === 'function') {
var invoke = function () {
var _args = [].slice.call(arguments)
return oldFn.apply(that, args.concat(_args))
}
rst = around.apply(null, [Object.assign({}, joinPoint, {
invoke: invoke
})])
} else {
rst = oldFn.apply(that, args)
}
if (typeof returning === 'function') {
joinPoint.result = rst
// 后置增强
returning.apply(null, [joinPoint])
}
return rst
} catch (err) {
if (typeof throwing === 'function') {
joinPoint.exception = err
throwing.apply(null, [joinPoint])
}
} finally {
if (typeof after === 'function') {
after.apply(null, [joinPoint])
}
}
}
}
下面通过一个简单的实例简单说明。同样的,我们打算在Person类的run方法的执行前后织入增强。按照前面的实现思路:
function Person(name, age){
this.name = name
this.age = age
}
Person.prototype.run = function(){
console.log(this.name + ': runing!')
}
// 织入增强的方法
adviceAction(Person, 'run', {before:before, after:after})
var p = new Persion('老刀', 28)
p.run()
这是针对单独类方法AOP的实现。显然这里面存在以下几个问题:
1、现实中不总是针对一个应用类的一个方法做AOP处理 2、应该如何如何定义切面Aspect,也就是上面定advices 3、一个应用类不总是对应一个切面Aspect 4、一个切面Aspect也不总是对应一个应用类
综上所述,更合理的应用类织入切面Aspect应该是这样的:
var waveing = function(TargetClass, [Aspect1, Aspect2,...]){
// ....
}
这里引发出另外一个问题,切面类Aspect应该是什么样子?应该如何定义?
前面讲过,切面包括切点(joinPoint)和通知(advice),方法名称对应切点joinPoint,相应的增强函数对应通知advice。那么一个简单的Aspect应该是这样子:
var Aspect = {
joinPoint1:{
before?:function{...},
after?:function{...},
around?:function{...},
throwing?:function{...},
returning?:function{...}
},
joinPoint2:{
before?:function{...},
after?:function{...},
around?:function{...},
throwing?:function{...},
returning?:function{...}
}
...
}
相应的我们创建一个织入waveing函数
var waveing = function(TargetClass, Aspects){
Aspects.forEach(function(aspect){
for(var key in aspect){
if (aspect.hasOwnProperty(key)){
var advices = aspect[key]
adviceAction(TargetClass, key, advices)
}
}
})
// ....
}
看一下完整的实例:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.run = function () {
console.log(this.name + ': runing!')
return 'running'
}
Person.prototype.work = function () {
console.log(this.name + ': working!')
return 'working'
}
function before(jp) {
console.log('before action:' + jp.key)
}
function after(jp) {
console.log('after action:' + jp.key)
}
function throwing(jp) {
console.log('throw action:' + jp.exception)
}
function around(jp) {
console.log('around action:' + jp.key)
return jp.invoke()
}
function returning(jp) {
console.log('returning action:' + jp.result)
}
var Aspect = {
run: {
before: before,
after: after
},
work: {
throwing: throwing,
around: around,
returning: returning
}
}
waveing(Person, [Aspect])
var p = new Person('老刀', 28)
p.run()
p.work()
至此,一个简略版的前端AOP工具已经完成了。但是这里依然存在一些问题。
比如:
1,上面讨论过的应用类和Aspect多对多关系 2,Aspect类的实现 3,ES6和Typescript的出现与发展对前端AOP开发带来哪些影响? 4,AOP的应用以及遇到的问题及其解决办法
由于篇幅所限我们将在下一篇文章继续讨论,继续完善我们上面实现的AOP工具。
这仅仅是一个开始,我们需要讨论的还有很多......