从业务埋点说起,埋点一般是在业务跑通之后加的,不相关的逻辑强行耦合在一起,导致对业务代码的侵入
加埋点的痛苦😖
- 同步的代码可能相对来说更好处理,直接将埋点代码放到相应事件代码最前面或者最后面,进行显式的分离
function Button(){
const handleClick= () => {
// 上报数据的逻辑
log();
// 业务逻辑
}
return <button onClick={handleClick}></button>;
}
- 如果遇到异步的情况(埋点的参数涉及到接口的返回值)等,会更糟糕,不得不在Promise返回的结果中完全与业务代码混合在一起。
function Button() {
const handleClick = () => {
// 业务代码
service
.getResult(data)
.then((result) => {
// 发送埋点
log();
// 业务代码
})
.catch((err) => {
// 业务代码
});
};
return <button onClick={handleClick}></button>;
}
有什么问题:紧耦合🔗
本来基本功能写好了,需要深入到每个功能内部安插埋点,导致原本组织好的业务代码被侵入的支离破碎
无论是从逻辑分离、代码简洁或后期维护的角度,这都不是让人满意的方案
如何分离埋点和业务代码😈
分析埋点特点:
- 与某段业务逻辑强绑定,埋点代码总是与该段业务代码之前或之后调用
- 埋点代码是自成一块的,所以很容易抽象成一块切片
用AOP实现分离🌟
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。
把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块
1 借助装饰器模式,仅适用于class组件
// 定义切面函数,用于在需要埋点的函数执行前执行上报数据的逻辑
function log(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function() {
// 上报数据的逻辑
console.log(`埋点数据:${key}`);
return original.apply(this, arguments);
};
return descriptor;
}
// 定义一个需要埋点的函数
class Button {
@log
handleClick() {
// 点击事件处理逻辑
}
// return 逻辑
}
2 对于函数式组件,无法使用类的方法装饰器,怎么办
- 可以通过在Function的原型上添加before/after方法,用于在调用该函数之前执行一个回调函数
// 定义一个函数类型CallbackFunc,该函数可以接收任意数量、任意类型的参数,且没有返回值
type CallbackFunc = (...args: any[]) => void;
// 声明全局的接口Function,该接口添加一个before方法,该方法接收一个函数类型的参数,返回一个Function类型的函数
declare global {
interface Function {
before(callback: CallbackFunc): Function;
}
}
// 业务主模块前执行beforeFn
Function.prototype.before = function (beforeFn: CallbackFunc): Function {
const self = this;
return (...args: any[]) => {
beforeFn(args);
return self(args);
};
};
使用Function.prototype.before分离业务和埋点
function Button(){
const handleClick= () => {
// 业务代码
}
const logFunction = function () {
// 埋点代码
};
const clickWithLog = handleClick.before(logFunction);
return <button onClick={clickWithLog}></button>;
}
这种方法的优点是非常简单,快捷且灵活。它不需要修改原始代码或创建代理对象,因此不会对应用程序的功能或性能产生任何影响。
但是,这种方法只能对单个函数进行跟踪,可能需要对每个需要跟踪的函数进行相应地修改,适用于埋点这种每个埋点处理逻辑都不同的情况
- 如果是异步请求,写一个Function.prototype.afterPromise实现对Promise返回值的切面读取
// 定义一个函数类型CallbackFunc,该函数可以接收任意数量、任意类型的参数,且没有返回值
type CallbackFunc = (...args: any[]) => void;
// 声明全局的接口Function,该接口添加一个before方法,该方法接收一个函数类型的参数,返回一个Function类型的函数
declare global {
interface Function {
afterPromise(callback: CallbackFunc): Function;
}
}
// 异步函数返回后执行afterFn
Function.prototype.afterPromise = function (afterFn: CallbackFunc): Function {
const self = this;
return (...args: any[]) => {
const result = self(args).then((res) => {
afterFn(res);
return res;
});
return result;
};
};
使用afterPromise实现queryWithLog
function Button() {
const queryWithLog = service.getResult.afterPromise((data) => {
console.log(`埋点数据:${data}`);
});
const handleClick = () => {
// 业务代码
queryWithLog(data)
.then((result) => {
// 业务代码
})
.catch((err) => {
// 错误处理
});
};
return <button onClick={handleClick}></button>;
}
AOP其他用法
- 插件式表单校验
需求:先对字段进行校验,再提交
// 检验数据
const test = (...args) => {
console.log('test', [...args]);
return true;
}
// 提交表单
const submitForm = (...args) => {
// 数据校验
if(!test(...args)) throw new Error('参数校验不通过');
console.log('submit', [...args]);
}
缺点:提交表单的逻辑和数据检测的逻辑耦合
使用AOP改造:
const submitWithValidation = submitForm.before(function(...args) {
if(!test(...args)) throw new Error('参数校验不通过');
});
// 使用新的函数提交表单数据
submitWithValidation();
在上面的代码中,submitWithValidation函数会先执行参数校验函数,如果校验通过则会继续执行提交表单数据函数,否则会抛出一个错误
这样可以将参数校验和提交表单两个步骤分开处理,避免逻辑的耦合,提高代码的可读性和维护性
总结
除了实现数据上报、表单验证外,使用AOP思想构造的高阶装饰函数还可以统计函数执行时间、动态修改参数等功能。
这种模式在日常开发中很有用,可以避免逻辑紧耦合,以无侵入的方式加入埋点,保证项目的主功能不受影响。