如何实现无侵入式数据埋点

从业务埋点说起,埋点一般是在业务跑通之后加的,不相关的逻辑强行耦合在一起,导致对业务代码的侵入

加埋点的痛苦😖

  • 同步的代码可能相对来说更好处理,直接将埋点代码放到相应事件代码最前面或者最后面,进行显式的分离
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思想构造的高阶装饰函数还可以统计函数执行时间、动态修改参数等功能。

这种模式在日常开发中很有用,可以避免逻辑紧耦合,以无侵入的方式加入埋点,保证项目的主功能不受影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值