是什么?
AOP(Aspect-Oriented Programming)面向切片编程,是一种软件开发方法,旨在提高代码的模块化性和可维护性。它通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,并以独立的方式处理这些关注点。
为什么?
在项目中代码不仅要实现业务需求还要完成各种与业务无关的的其他功能,比如日志埋点、异常上报。通过 AOP,开发者可以将这些关注点定义为"切面",然后在需要的地方将这些切面织入到应用程序的特定位置,而不必直接修改主要业务逻辑。
怎么做?
例子1:计算一个事件的执行时间
页面中有一个插入DOM 的事件。
js
function appendEmpty() {
for (let index = 0; index < 100; index++) {
let div = document.createElement('div')
document.body.appendChild(div)
}
}
为了计算事件执行的时间,需要在方法内部插入计算时间的代码。
js
function appendEmpty() {
let start = new Date().getTime()
for (let index = 0; index < 100; index++) {
let div = document.createElement('div')
document.body.appendChild(div)
}
let end = new Date().getTime()
console.log(end - start)
}
代码问题
计算时间的逻辑全部插入到了源码中,不仅入侵源码,还改变了 appenEmpty 的业务职能, 如果再有其他的需求(埋点)会使函数越来越难维护。
解决方案
在全局的 Function
上挂载 before
after
方法,用于在函数执行前和执行后执行。
js
Function.prototype.before = function (beforeFn) {
let self = this
return function () {
beforeFn.apply(this, arguments)
return self.apply(this, arguments)
}
}
Function.prototype.after = function (afterFn) {
let self = this
return function () {
let ret = self.apply(this, arguments)
afterFn.apply(this, arguments)
return ret
}
}
声明函数 fnTime
方法,内部调用 after
函数计算传入函数参数的执行时间。
js
function appendEmpty() {
for (let index = 0; index < 100; index++) {
let div = document.createElement('div')
document.body.appendChild(div)
}
}
const fnTime = function (fn) {
let start = new Date().getTime()
return fn.after(function () {
console.log(new Date().getTime() - start)
})
}
fnTime(appendEmpty)()
例子 2: 表单提交
在表单提交时,通常会校验表单数据后进行表单提交
js
function validate (value) {
if (!value.length) {
return false
}
if (value.length > 10) {
return false
}
return true
}
function submit (value) {
if (!validate(value)) return
form.submit(value)
}
submit(formValue)
如果除了表单验证,还有操作日志上传、其他业务逻辑插入,就会导致 submit
的动作变的不那么 单纯
, 可以使用 AOP 模式把提交无关的过程交给切片处理。
js
let submit = function (value) {
form.submit(value)
}
// 添加before 切面函数
submit.before(validate)(value)
实现切片方法
和上面的切片方法相比, 下面的切片方法多了
if(!ret) return false
的写法,这样可以在出错时阻断切片执行。
js
// before 切片
Function.prototype.before = function (beforeFn) {
let self = this
return function () {
let ret = beforeFn.apply(this, arguments)
if(!ret) return false
return self.apply(this, arguments)
}
}
Function.prototype.after = function (afterFn) {
let self = this
return function () {
let ret = self.apply(this, arguments)
if(!ret) return false
return afterFn.apply(this, arguments)
}
}
例子 3:操作埋点
在点击 btnClick
后打开弹出框,并上传埋点日志。
js
methods: {
track () {
// 添加埋点
this.$tracker({ elementCode: 104340 })
},
showDialog () {
// 业务逻辑。。。
this.showDialog = true
},
btnClick () {
// 业务逻辑。。。。。
this.showDialog.after(this.track)()
}
}
使用装饰器设计 AOP
除了使用传入函数参数的方式,装饰器用来实现切片写法上会优雅些许
js
function log(target, name, descriptor) {
var oldValue = descriptor.value;
descriptor.value = function () {
console.log(`Calling "${name}" with`, arguments);
return oldValue.apply(null, arguments);
}
return descriptor;
}
// 日志应用
class Maths {
@log
add(a, b) {
return a + b;
}
}
const math = new Maths();
// passed parameters should get logged now
math.add(2, 4);
总结
AOP 的业务应用模式不难理解,相比于 IoC 有更多的业务应用场景。 在 NestJs
框架中,中间件,拦截器,守卫,异常过滤器,管道也都是基于 AOP 设计。
AOP 应用架构