前端行为监控系统的实战搭建与核心功能实现

Mr-素平 前端 阅读 2,505
赞 10 收藏
二维码
手机扫码查看
反馈

核心代码就这几行

行为监控这块儿,其实核心就是监听用户的各种操作然后上报数据。我之前做过几个项目,现在回头看,最简单的实现方式就是把常用的事件都监听一遍,然后统一处理。

前端行为监控系统的实战搭建与核心功能实现

class BehaviorMonitor {
  constructor(options = {}) {
    this.apiEndpoint = options.apiEndpoint || 'https://jztheme.com/api/behavior';
    this.userId = options.userId;
    this.pageId = this.generatePageId();
    this.init();
  }

  init() {
    // 监听点击事件
    document.addEventListener('click', (e) => {
      this.trackEvent('click', e);
    });

    // 监听页面滚动
    let scrollTimer;
    document.addEventListener('scroll', () => {
      clearTimeout(scrollTimer);
      scrollTimer = setTimeout(() => {
        this.trackEvent('scroll', { 
          scrollTop: window.pageYOffset,
          scrollLeft: window.pageXOffset 
        });
      }, 100);
    });

    // 监听输入框变化
    document.addEventListener('input', (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
        this.trackEvent('input', e);
      }
    });

    // 监听页面加载完成
    window.addEventListener('load', () => {
      this.trackEvent('page_load');
    });

    // 监听页面卸载
    window.addEventListener('beforeunload', () => {
      this.trackEvent('page_unload');
    });
  }

  trackEvent(type, event) {
    const eventData = {
      type,
      userId: this.userId,
      pageId: this.pageId,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      ...this.extractEventData(type, event)
    };

    // 发送上报数据
    this.sendData(eventData);
  }

  extractEventData(type, event) {
    switch (type) {
      case 'click':
        return {
          element: event.target.tagName + (event.target.id ? '#' + event.target.id : '') + (event.target.className ? '.' + event.target.className.split(' ')[0] : ''),
          x: event.clientX,
          y: event.clientY
        };
      case 'input':
        return {
          element: event.target.name || event.target.id || event.target.tagName,
          value: event.target.value,
          maxLength: event.target.maxLength
        };
      default:
        return {};
    }
  }

  sendData(data) {
    // 使用 navigator.sendBeacon 确保数据发送
    if (navigator.sendBeacon) {
      navigator.sendBeacon(this.apiEndpoint, JSON.stringify(data));
    } else {
      // fallback 方案
      fetch(this.apiEndpoint, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' }
      }).catch(err => console.error('Behavior tracking failed:', err));
    }
  }

  generatePageId() {
    return Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
  }
}

上面这段代码基本覆盖了大部分用户行为的监听。初始化的时候传入一个用户ID就行:

// 初始化行为监控
const monitor = new BehaviorMonitor({
  userId: 'user_12345',
  apiEndpoint: 'https://jztheme.com/api/behavior'
});

踩坑提醒:这三点一定注意

我踩过的坑主要有这么几个,记录一下避免后来人重复踩坑。

  • 上报频率控制:刚开始没做频率限制,结果滚动事件疯狂触发,每秒几十条上报,服务器直接崩了。后来加了个节流,在scroll事件里用了防抖处理
  • sendBeacon兼容性:这个API在老版本浏览器里不支持,必须做降级处理。我用fetch作为fallback,虽然不能保证页面关闭前数据能发出去,但至少在大部分情况下可用
  • 敏感信息过滤:最开始没过滤密码输入框的内容,差点造成数据泄露事故。现在会在input事件里判断inputType,如果是password就只记录字段名不记录值

关于敏感信息过滤,这里有个具体的处理函数:

isSensitiveElement(element) {
  const sensitiveTypes = ['password', 'card-number', 'cvv', 'pin'];
  const sensitiveClasses = ['password', 'secret', 'sensitive'];
  
  // 检查input类型
  if (element.type && sensitiveTypes.includes(element.type.toLowerCase())) {
    return true;
  }
  
  // 检查className
  if (element.className && sensitiveClasses.some(cls => 
    element.className.toLowerCase().includes(cls))) {
    return true;
  }
  
  // 检查name属性
  if (element.name && sensitiveTypes.includes(element.name.toLowerCase())) {
    return true;
  }
  
  return false;
}

extractEventData(type, event) {
  if (type === 'input') {
    if (this.isSensitiveElement(event.target)) {
      return {
        element: event.target.name || event.target.id || event.target.tagName,
        isSensitive: true
      };
    }
    // 其他input处理...
  }
  // 其他事件处理...
}

高级用法:页面热力图实现

有了这些数据,其实还能做很多有趣的事情。比如基于点击数据生成页面热力图。原理很简单,就是在后端收集所有点击坐标,然后前端用canvas绘制点密度图。

// 点击分布可视化
class ClickHeatmap {
  constructor(container) {
    this.container = container;
    this.clicks = [];
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.setupCanvas();
  }

  setupCanvas() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    this.canvas.style.position = 'fixed';
    this.canvas.style.top = '0';
    this.canvas.style.left = '0';
    this.canvas.style.pointerEvents = 'none';
    this.canvas.style.zIndex = '9999';
    document.body.appendChild(this.canvas);
  }

  addClick(x, y) {
    this.clicks.push({x, y});
    this.render();
  }

  render() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    // 绘制点击点
    this.clicks.forEach(click => {
      this.ctx.beginPath();
      this.ctx.arc(click.x, click.y, 20, 0, Math.PI * 2);
      this.ctx.fillStyle = 'rgba(255, 0, 0, 0.1)';
      this.ctx.fill();
    });
  }
}

不过这种方式只是前端模拟,真实的数据分析还是得在服务端进行。我之前有个项目就是根据这些数据优化页面布局,发现用户在某些区域点击特别频繁,后来就把重要的按钮移到那些位置,转化率提升了不少。

性能优化和内存泄漏防范

行为监控最大的问题就是可能影响用户体验,特别是事件监听器太多的时候。这里有几个优化点:

首先是事件委托,不要给每个元素都单独绑定事件,而是利用事件冒泡机制统一处理:

// 事件委托方式
document.addEventListener('click', (e) => {
  let target = e.target;
  while (target && !target.hasAttribute('data-track')) {
    target = target.parentElement;
  }
  
  if (target) {
    const trackConfig = target.getAttribute('data-track');
    if (trackConfig) {
      this.trackEvent('custom_click', { 
        config: JSON.parse(trackConfig), 
        element: target 
      });
    }
  }
});

其次是合理销毁监听器,特别是在SPA应用中切换页面时:

destroy() {
  // 移除所有事件监听器
  document.removeEventListener('click', this.clickHandler);
  document.removeEventListener('input', this.inputHandler);
  window.removeEventListener('scroll', this.scrollHandler);
  // 清理定时器等资源
}

数据上报的可靠性保障

最重要的一个问题:如何确保用户离开页面时数据还能正常上报?这个问题困扰了我很久。最终用了一个组合方案:

// 多重保障的数据发送
sendData(data) {
  const payload = JSON.stringify(data);
  
  // 1. 优先使用 sendBeacon
  if (navigator.sendBeacon) {
    try {
      navigator.sendBeacon(this.apiEndpoint, payload);
      return;
    } catch (e) {
      console.warn('sendBeacon failed:', e);
    }
  }
  
  // 2. 尝试同步XMLHttpRequest (页面即将关闭时同步请求会阻塞)
  if (this.isPageClosing()) {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', this.apiEndpoint, false); // 同步请求
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(payload);
    return;
  }
  
  // 3. 普通异步请求作为fallback
  fetch(this.apiEndpoint, {
    method: 'POST',
    body: payload,
    headers: { 'Content-Type': 'application/json' }
  }).catch(err => {
    // 4. 本地缓存失败数据,下次页面加载时重试
    this.cacheFailedData(data);
  });
}

isPageClosing() {
  return document.readyState === 'unloading' || document.readyState === 'loading';
}

cacheFailedData(data) {
  const failedData = JSON.parse(localStorage.getItem('behavior_failed_data') || '[]');
  failedData.push(data);
  localStorage.setItem('behavior_failed_data', JSON.stringify(failedData));
}

然后在页面加载时检查是否有缓存的失败数据需要重试:

// 页面加载时重试失败数据
window.addEventListener('load', () => {
  const failedData = JSON.parse(localStorage.getItem('behavior_failed_data') || '[]');
  if (failedData.length > 0) {
    failedData.forEach(data => {
      this.sendData(data);
    });
    localStorage.removeItem('behavior_failed_data');
  }
});

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如结合A/B测试、用户路径分析等等,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论