面向切面
基本概念
awe-axios还实现了面向切面编程(AOP)的功能,通过@Before、@After等装饰器,可以对请求前、请求后、请求错误等阶段进行拦截,并对请求进行处理。
切面类
使用@Aspect可以定义切面类。切面类中的方法可以在目标方法的不同执行阶段产生影响。定义切面类方式如下:
// 定义切面类
@Aspect()
class Logger {
@Before('getUser*')
log(ctx: AspectContext) {
console.log('before getUser*');
}
}切点表达式
aop的核心在于明确在什么方法、方法什么执行阶段进行切入。这个方法就是切入点(或者说叫做切入位置),切入点需要使用切入点表达式来表示。
切入点表达式其实就是就是指定切入位置的字符串,通过字符串的方式指定切入位置。其语法为:[模块名].[类名].(方法名),并且这些字符串都支持用*作为通配符表示任意字符。比如:
getUser*:表示所有以getUser开头的方法UserApi.getUser*:表示UserApi类中所有以getUser开头的方法UserApi.getUserById:表示UserApi类中getUserById方法UserApi.*:表示UserApi类中所有的方法user.UserApi.getUserById:表示user模块中UserApi类中getUserById方法*:表示所有的方法
缓存优化
awe-axios对使用过的切点表达式进行函数记忆缓存,避免重复执行,提高性能。
切入时机
切入时机指的是在什么阶段进行切入,比如:
@Before
@Before装饰器用于在方法调用前进行拦截
@Aspect(1)
class Logger {
@Before('getUser*')
log(ctx: AspectContext) {
// 调用方法前打印before getUser*
console.log('before getUser*');
}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);@After
@After装饰器用于在方法调用后进行拦截
@Aspect(1)
class Logger {
@After('getUser*')
logAfter(ctx: AspectContext) {
console.log('after getUser*');
}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);@Around
@Around装饰器用于在方法调用前后进行拦截
@Aspect(1)
class Logger {
@Around('getUser*')
logAround(ctx: AspectContext, adviceChain: AdviceChain) {
console.log('around before getUser*');
const result = adviceChain.proceed(ctx);
console.log('arount after getUser*');
return result;
}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);注意
@Around装饰器必须要有返回值,否则会报错。- 你必须要调用
adviceChain.proceed(ctx)方法来手动推进执行链的执行,否则不会执行目标方法。
@AfterReturning
@AfterReturning装饰器用于在方法调用成功后进行拦截,它可以获取到方法的返回值,并进行处理。
@Aspect(1)
class Logger {
@AfterReturning('getUser*')
logAfterReturning(ctx: AspectContext, result: any) {
console.log(result);
console.log('afterReturning getUser*');
}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);@AfterThrowing
@AfterThrowing装饰器用于在方法调用失败后进行拦截,它可以获取到方法的错误信息,并进行处理。
@Aspect(1)
class Logger {
@AfterThrowing('getUser*')
logAfterThrowing(ctx: AspectContext, error: any) {
console.log('出错了');
console.log('afterThrowing getUser*');
}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);切点上下对象
切点上下对象(AspectContext)是awe-axios中用于保存切点信息的对象,也就是在上面的方法中第一个参数ctx。它包含了如下信息:
export class AspectContext {
/**
* 原方法
*/
method: Function;
/**
* 原方法this
*/
target: any;
/**
* 原方法参数
*/
args: any[];
/**
* axios配置
*/
axiosConfig?: HttpRequestConfig;
}所以,你可以通过ctx.method、ctx.target、ctx.args获取到原方法、原方法 this、原方法参以及axios配置数等。所以你可以利用这些信息更加精确的实现拦截功能。
可复用的切入表达式
某些切入点表达式你可能会经常用到,所以awe-axios支持提供可复用的切入表达式。可复用的切入点表达式是一个函数,只需要返回切点表达式即可,如下所示:
function reusableExp() {
return 'getUser*';
}
@Aspect(1)
class Logger {
@Before(reusableExp)
log(ctx: AspectContext) {
console.log('before getUser*');
}
@After(reusableExp)
logAfter(ctx: AspectContext) {
console.log('after getUser*');
console.log(ctx.axiosConfig);
}
}
@Component()
@HttpApi('http://localhost:3000/api/users')
class UserApi {
@Post({
url: '/pages',
headers: {
'Content-Type': 'application/json',
},
mock: async ({ request }) => {
const data = await request.json();
const { page, size } = data as { page: number; size: number };
return HttpResponse.json({
message: 'ok',
data: { id: 1, name: '张三' },
});
},
})
getUserPages(@BodyParam() data: { page: number; size: number }): any {}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages({ page: 1, size: 10 })();
console.log(data);执行顺序
切入时机执行顺序
我们可能会很疑惑,这些切入时机的执行顺序,我们先来看看这个案例:
@Component()
@HttpApi('http://localhost:3000/api/users')
class UserApi {
@Get({
url: '/pages',
mock: () => {
return HttpResponse.json({
data: 'hello world',
});
},
})
getUserPages(): any {}
getUsers(): any {}
}
@Aspect(1)
class Logger {
@Before('getUser*')
log(ctx: AspectContext) {
console.log('before getUser*');
}
@After('getUser*')
logAfter(ctx: AspectContext) {
console.log('after getUser*');
}
@Around('getUser*')
logAround(ctx: AspectContext, adviceChain: AdviceChain) {
console.log('around before getUser*');
const result = adviceChain.proceed(ctx);
console.log('arount after getUser*');
return result;
}
@AfterReturning('getUser*')
logAfterReturning(ctx: AspectContext, result: any) {
console.log('result');
console.log('afterReturning getUser*');
}
@AfterThrowing('getUser*')
logAfterThrowing(ctx: AspectContext, error: any) {
console.log('afterThrowing getUser*');
}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages()();
console.log(data);这个案例执行结果为:
around before getUser*
before getUser*
after getUser*
[AsyncFunction (anonymous)]
afterReturning getUser*
arount after getUser*所以切入时机的执行顺序为:环绕前置通知->前置通知->后置通知->目标函数执行->返回通知->异常通知->环绕后置通知。
切面类的执行顺序
当有多个切面类时awe-axios支持为切面类添加优先级序号,其默认值为 5,值越小优先级越高则越先执行。如果优先级相同则顺序是随机的,这个我们不做讨论。如果优先级不同但是有多个切面类,那么执行顺序又是怎么样呢?我们来看下下面的代码:
function reusableExp() {
return 'getUser*';
}
@Aspect(1)
class Logger {
@Before(reusableExp)
log(ctx: AspectContext) {
console.log('before getUser*');
}
@After(reusableExp)
logAfter(ctx: AspectContext) {
console.log('after getUser*');
}
@Around('getUser*')
logAround(ctx: AspectContext, adviceChain: AdviceChain) {
console.log('around before getUser*');
const result = adviceChain.proceed(ctx);
console.log('arount after getUser*');
return result;
}
@AfterReturning('getUser*')
logAfterReturning(ctx: AspectContext, result: any) {
console.log('result');
console.log('afterReturning getUser*');
}
@AfterThrowing('getUser*')
logAfterThrowing(ctx: AspectContext, error: any) {
console.log('afterThrowing getUser*');
}
}
@Aspect(2)
class Logger2 {
@Before(reusableExp)
log(ctx: AspectContext) {
console.log('2before getUser*');
}
@After(reusableExp)
logAfter(ctx: AspectContext) {
console.log('2after getUser*');
}
@Around('getUser*')
logAround(ctx: AspectContext, adviceChain: AdviceChain) {
console.log('2around before getUser*');
const result = adviceChain.proceed(ctx);
console.log('2arount after getUser*');
return result;
}
@AfterReturning('getUser*')
logAfterReturning(ctx: AspectContext, result: any) {
console.log('result');
console.log('2afterReturning getUser*');
}
@AfterThrowing('getUser*')
logAfterThrowing(ctx: AspectContext, error: any) {
console.log('2afterThrowing getUser*');
}
}
@Component()
@HttpApi('http://localhost:3000/api/users')
class UserApi {
@Post({
url: '/pages',
headers: {
'Content-Type': 'application/json',
},
mock: async ({ request }) => {
const data = await request.json();
const { page, size } = data as { page: number; size: number };
return HttpResponse.json({
message: 'ok',
data: { id: 1, name: '张三' },
});
},
})
getUserPages(@BodyParam() data: { page: number; size: number }): any {}
}
const userApi = new UserApi();
const { data } = await userApi.getUserPages({ page: 1, size: 10 })();
console.log(data);所以当有多个切面类时,执行顺序为:
around before getUser*
2around before getUser*
before getUser*
2before getUser*
after getUser*
2after getUser*
result
afterReturning getUser*
result
2afterReturning getUser*
2arount after getUser*
arount after getUser*