AOP切面编程的概念
AOP这个概念来源于Java
的Spring
框架,是Spring
为了解决OOP(面向对象编程模式)面对一些业务场景的限制而开发来的,下面就让我用JavaScript
代替Java
来解释一下AOP
比如我现在使用OOP
写法新建一个读取数据的业务组件,分别有读取data
,更新data
,删除data
三个方法:
javascript
class DataService {
constructor(ctx) {
this.ctx = ctx;
}
createData() {
ctx.createData();
}
updateData() {
ctx.updateData();
}
deleteData() {
ctx.deleteData();
}
}
对于每个接口,业务可能会有一些相同的操作,如日志记录、数据检验、安全验证等,那么代码就会像下面这样
javascript
class DataService {
constructor(ctx) {
this.ctx = ctx;
}
createData() {
ctx.dataCheck();
ctx.createData();
}
updateData() {
ctx.dataCheck();
ctx.updateData();
}
deleteData() {
ctx.dataCheck();
ctx.deleteData();
}
}
一个相同的功能在很多不同的方法中以相同的方式出现,这样显然不符合编码的简洁性和易读性。 有一种解决方法是使用Proxy
模式,为了保持我们的代码中一个类只负责一件事的原则,新建一个新的类继承于DataService
,在这个类中为每个方法都加上dataCheck
javascript
class CheckDataService extends DataService {
constructor(ctx) {
super(ctx)
}
createData() {
ctx.dataCheck();
ctx.createData();
}
updateData() {
ctx.dataCheck();
ctx.updateData();
}
deleteData() {
ctx.dataCheck();
ctx.deleteData();
}
}
这样的做法缺点是比较麻烦,每个Proxy
中都要重复执行父类的方法。
那么这时就有了AOP
,其基本原理是在Spring
中某个类的运行期的前期或者后期插入某些逻辑,在Spring
中可以通过注解的形式,让Spring
容器启动时实现自动注入,如下面代码片段
java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect // 声明为切面
@Component // 让Spring能够扫描并创建切面实例
public class LoggingAspect {
// 在UserService的每个public方法前执行logBefore
@Before("execution(public * com.example.UserService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Logging before the method executes");
}
// 在UserService的每个public方法前执行logAfter
@After("execution(public * com.example.UserService.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("Logging after the method executes");
}
}
// UserService
import org.springframework.stereotype.Component;
@Component // 声明为一个Spring管理的组件
public class UserService {
public void login() {
System.out.println("Doing some important work");
}
}
但是JS
没有在底层实现这些东西,所以我们只能自己改造。
在JavaScript中实现AOP
思路是这样的,我们将某个需要切入的方法进行重写,在重写后的函数中切入相关逻辑就行了
javascript
/**
* 重写对象上面的某个属性
* @param targetObject 需要被重写的对象
* @param propertyName 需要被重写对象的 key
* @param newImplementation 以原有的函数作为参数,执行并重写原有函数
*/
function overrideProperty(
targetObject,
propertyName,
newImplementation,
) {
if (targetObject === undefined) return // 若当前对象不存在
if (propertyName in targetObject) { // 若当前对象存在当前属性
const originalFunction = targetObject[propertyName]
const modifiedFunction = newImplementation(originalFunction) // 把原本的函数传入
if (typeof modifiedFunction == 'function') {
targetObject[propertyName] = modifiedFunction
}
}
}
先写一个公共方法,去重写对象上的某个属性,这样我们可以调用overrideProperty
去重写任意对象上的任何方法。 现在用overrideProperty
重写一下window
上的fetch
方法
javascript
overrideProperty(window, 'fetch', originalFetch => {
return function (...args) {
// 在fetch发起前做些什么
return originalFetch.apply(this, args).then((res) => {
// 在fetch完成后做些什么
return res
})
}
})
可以看到我们第三个参数传入一个函数并返回一个函数,这个返回的函数就是重写完成的fetch
方法,只要在项目初始化时调用overrideProperty
,那么以后调用fetch
时都会执行。 是不是感觉这样写也挺麻烦的,我们换一种写法:
javascript
function overrideProperty(
targetObject,
propertyName,
context,
) {
if (targetObject === undefined) return
if (propertyName in targetObject) {
const originalFunction = targetObject[propertyName]
function reactorFn(...args) {
this.before && this.before();
originalFunction.apply(context, args);
this.after && this.after();
}
targetObject[propertyName] = reactorFn;
reactorFn.before = (fn) => {
this.before = fn;
return reactorFn
};
reactorFn.after = (fn) => {
this.after = fn;
return reactorFn;
};
return reactorFn;
}
}
overrideProperty(window, 'alert', window).before(() => {
console.log('before')
}).after(() => {
console.log('after')
})
alert('test')
这样子就可以通过链式调用的方式来定义before
和after
的回调函数了,但是这只适用于同步执行的方法,对于fetch
这种异步的返回Promise
的方法,为了在Promise.then
中执行after
,又得专门写一个重写函数,大家就根据自己的项目情况来选择不同就写法吧。
监听HTTP请求
浏览器中主要的HTTP请求通过XMLHttpRequest
和fetch
发出,在上面我们已经监听了fetch
,接下来我们监听一下XMLHttpRequest
。 一般使用XMLHttpRequest
发送HTTP
请求会调用open
方法,最后调用send
方法,所以我们监听开始时的open
和最后的send
所以我们重写这两个方法
javascript
// 重写open
overrideProperty(XMLHttpRequest.prototype, 'open', (originalOpen) => {
return function (...args) {
// do something
originalOpen.apply(this, args)
}
})
// 重写send
overrideProperty(XMLHttpRequest.prototype, 'send', (originalSend) => {
return function (...args) {
// do something
originalSend.apply(this, args)
}
})
当send
后,xhr对象上的readyState会经历四个状态,分别是: 0 (UNSENT): XMLHttpRequest 对象已经创建,但 open() 方法还没有被调用。 1 (OPENED): open() 方法已经被调用。在这个状态下,你可以通过设置请求头和请求方法来配置请求。 2 (HEADERS_RECEIVED): send() 方法已经被调用,并且头部和状态已经可获得。 3 (LOADING): 下载中;responseText 属性已经包含部分数据。 4 (DONE): 请求操作已经完成。
我们直接监听readyState为4(DONE)的完成状态即可
javascript
// 重写send
overrideProperty(XMLHttpRequest.prototype, 'send', (originalSend) => {
return function (...args) {
// do something
originalSend.apply(this, args)
// 监听 readystatechange 事件
this.addEventListener("readystatechange", function () {
// 检查 readyState 的状态
if (this.readyState === XMLHttpRequest.DONE) {
// 请求已完成,检查状态码
if (this.status === 200) {
// 请求成功,处理响应数据
console.log("请求成功:", this.responseText);
} else {
// 请求失败,处理错误
console.log("请求失败:", this.status);
}
}
});
}
})
这样当我们当前页面有fetch
请求和xhr
请求时,都可以被捕获到。
总结
本文从Spring
出发,介绍了AOP
面向切面编程的由来,又用JavaScript
演示了AOP
编程的优势,最后使用AOP
编程实现了HTTP
请求的监听,这大家的平时的开发中也可以灵活运用。
欢迎点赞、关注、收藏~