LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

前端监控用户停留时长

freeflydom
2025年6月4日 11:58 本文热度 264

在前端监控用户在当前界面的停留时长(也称为“页面停留时间”或“Dwell Time”)是用户行为分析中非常重要的指标。它可以帮助我们了解用户对某个页面的兴趣程度、内容质量以及用户体验。

停留时长监控的挑战

监控停留时长并非简单地计算进入和离开的时间差,因为它需要考虑多种复杂情况:

  1. 用户切换标签页或最小化浏览器: 页面可能仍在后台运行,但用户并未真正“停留”在该界面。
  2. 浏览器关闭或崩溃: 页面没有正常卸载,可能无法触发 unload 事件。
  3. 网络问题: 数据上报可能失败。
  4. 单页应用 (SPA) : 在 SPA 中,页面切换不会触发传统的页面加载和卸载事件,需要监听路由变化。
  5. 长时间停留: 如果用户停留时间很长,一次性上报可能导致数据丢失(例如,浏览器或电脑崩溃)。

实现监测的思路和方法

我们将结合多种 Web API 来实现一个健壮的停留时长监控方案。

1. 基础方案:页面加载与卸载 (适用于传统多页应用)

这是最基本的方案,通过记录页面加载时间和卸载时间来计算停留时长。

// page-duration-tracker.js
let startTime = 0; // 页面加载时间
let pageId = '';   // 页面唯一标识符,例如 URL 路径
/**
 * 上报页面停留时长数据
 * @param {string} id 页面唯一标识
 * @param {number} duration 停留时长 (毫秒)
 * @param {boolean} isUnload 是否是页面卸载时上报
 */
function sendPageDuration(id, duration, isUnload = false) {
    const data = {
        pageId: id,
        duration: duration,
        timestamp: Date.now(),
        eventType: isUnload ? 'page_unload' : 'page_hide',
        // 可以添加更多上下文信息,如用户ID、会话ID、浏览器信息等
        userAgent: navigator.userAgent,
        screenWidth: window.screen.width,
        screenHeight: window.screen.height
    };
    console.log('上报页面停留时长:', data);
    // 使用 navigator.sendBeacon 确保在页面卸载时也能发送数据
    // sendBeacon 适合发送少量数据,且不会阻塞页面卸载
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/api/page-duration', JSON.stringify(data));
    } else {
        // Fallback for older browsers (可能会阻塞页面卸载或失败)
        fetch('/api/page-duration', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data),
            keepalive: true // 尝试在页面卸载时保持连接
        }).catch(e => console.error('发送停留时长失败:', e));
    }
}
// 页面加载时记录开始时间
window.addEventListener('load', () => {
    startTime = Date.now();
    pageId = window.location.pathname; // 使用路径作为页面ID
    console.log(`页面 ${pageId} 加载,开始计时: ${startTime}`);
});
// 页面卸载时计算并上报时长
// 'beforeunload' 可能会被浏览器阻止,'pagehide' 更可靠,尤其是在移动端
window.addEventListener('pagehide', () => {
    if (startTime > 0) {
        const duration = Date.now() - startTime;
        sendPageDuration(pageId, duration, true);
        startTime = 0; // 重置,避免重复上报
    }
});
// 对于某些浏览器或场景,'beforeunload' 仍然有用,作为补充
window.addEventListener('beforeunload', () => {
    if (startTime > 0) {
        const duration = Date.now() - startTime;
        sendPageDuration(pageId, duration, true);
        startTime = 0;
    }
});
// 在你的 HTML 中引入此脚本
// <script src="page-duration-tracker.js"></script>

代码讲解:

  • startTime: 记录页面加载时的 Unix 时间戳。

  • pageId: 标识当前页面,这里简单地使用了 window.location.pathname。在实际应用中,你可能需要更复杂的 ID 策略(如路由名称、页面 ID 等)。

  • sendPageDuration(id, duration, isUnload): 负责将页面 ID 和停留时长发送到后端。

    • navigator.sendBeacon(): 推荐用于在页面卸载时发送数据。它不会阻塞页面卸载,且即使页面正在关闭,也能保证数据发送。
    • fetch({ keepalive: true })keepalive: true 选项允许 fetch 请求在页面卸载后继续发送,作为 sendBeacon 的备用方案。
  • window.addEventListener('load', ...): 在页面完全加载后开始计时。

  • window.addEventListener('pagehide', ...): 当用户离开页面(切换标签页、关闭浏览器、导航到其他页面)时触发。这是一个更可靠的事件,尤其是在移动端,因为它在页面进入“后台”状态时触发。

  • window.addEventListener('beforeunload', ...): 在页面即将卸载时触发。它比 pagehide 触发得更早,但可能会被浏览器阻止(例如,如果页面有未保存的更改)。作为补充使用。

2. 考虑用户活跃状态:Visibility API

当用户切换标签页或最小化浏览器时,页面可能仍在运行,但用户并未真正“停留”。document.visibilityState 和 visibilitychange 事件可以帮助我们识别这种状态。

// page-duration-tracker-with-visibility.js
let startTime = 0;
let totalActiveTime = 0; // 累计活跃时间
let lastActiveTime = 0;  // 上次活跃时间戳
let pageId = '';
function sendPageDuration(id, duration, eventType) {
    const data = {
        pageId: id,
        duration: duration,
        timestamp: Date.now(),
        eventType: eventType,
        // ... 其他上下文信息
    };
    console.log('上报页面停留时长:', data);
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/api/page-duration', JSON.stringify(data));
    } else {
        fetch('/api/page-duration', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), keepalive: true }).catch(e => console.error('发送停留时长失败:', e));
    }
}
function startTracking() {
    startTime = Date.now();
    lastActiveTime = startTime;
    totalActiveTime = 0;
    pageId = window.location.pathname;
    console.log(`页面 ${pageId} 加载,开始计时 (总时长): ${startTime}`);
}
function stopTrackingAndReport(eventType) {
    if (startTime > 0) {
        // 如果页面当前是可见的,需要将从上次活跃到现在的这段时间也计入活跃时间
        if (document.visibilityState === 'visible') {
            totalActiveTime += (Date.now() - lastActiveTime);
        }
        sendPageDuration(pageId, totalActiveTime, eventType);
        startTime = 0; // 重置
        totalActiveTime = 0;
        lastActiveTime = 0;
    }
}
// 页面加载时开始追踪
window.addEventListener('load', startTracking);
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
        // 页面变为不可见,暂停计时,并将当前活跃时间累加
        totalActiveTime += (Date.now() - lastActiveTime);
        console.log(`页面 ${pageId} 变为不可见,累加活跃时间: ${totalActiveTime}`);
    } else {
        // 页面变为可见,恢复计时
        lastActiveTime = Date.now();
        console.log(`页面 ${pageId} 变为可见,恢复计时: ${lastActiveTime}`);
    }
});
// 页面卸载时上报最终时长
window.addEventListener('pagehide', () => stopTrackingAndReport('page_hide'));
window.addEventListener('beforeunload', () => stopTrackingAndReport('page_unload'));
// 考虑长时间停留:定时上报心跳或分段数据
// 例如,每隔 30 秒上报一次当前活跃时间,并重置计数器
let heartbeatInterval;
window.addEventListener('load', () => {
    startTracking(); // 确保在 load 事件中启动计时
    heartbeatInterval = setInterval(() => {
        if (document.visibilityState === 'visible' && startTime > 0) {
            const currentActiveTime = Date.now() - lastActiveTime;
            totalActiveTime += currentActiveTime;
            lastActiveTime = Date.now(); // 重置上次活跃时间
            console.log(`心跳上报 ${pageId} 活跃时间: ${currentActiveTime}ms, 累计: ${totalActiveTime}ms`);
            // 可以选择上报当前心跳的活跃时间,或者累计活跃时间
            sendPageDuration(pageId, currentActiveTime, 'heartbeat'); // 上报当前心跳时间
        }
    }, 30 * 1000); // 每 30 秒
});
// 页面卸载时清除心跳定时器
window.addEventListener('pagehide', () => {
    clearInterval(heartbeatInterval);
    stopTrackingAndReport('page_hide');
});
window.addEventListener('beforeunload', () => {
    clearInterval(heartbeatInterval);
    stopTrackingAndReport('page_unload');
});

代码讲解:

  • totalActiveTime: 存储用户在页面可见状态下的累计停留时间。

  • lastActiveTime: 记录页面上次变为可见的时间戳。

  • document.addEventListener('visibilitychange', ...): 监听页面可见性变化。

    • 当页面变为 hidden 时,将从 lastActiveTime 到当前的时间差累加到 totalActiveTime
    • 当页面变为 visible 时,更新 lastActiveTime 为当前时间,表示重新开始计算活跃时间。
  • 心跳上报setInterval 每隔一段时间(例如 30 秒)检查页面是否可见,如果是,则计算并上报当前时间段的活跃时间。这有助于在用户长时间停留但未触发 pagehide 或 beforeunload 的情况下(例如浏览器崩溃、电脑关机),也能获取到部分停留数据。

3. 针对单页应用 (SPA) 的解决方案

SPA 的页面切换不会触发传统的 load 或 unload 事件。我们需要监听路由变化来模拟页面的“加载”和“卸载”。

// page-duration-tracker-spa.js
let startTime = 0;
let totalActiveTime = 0;
let lastActiveTime = 0;
let currentPageId = '';
function sendPageDuration(id, duration, eventType) {
    const data = {
        pageId: id,
        duration: duration,
        timestamp: Date.now(),
        eventType: eventType,
        // ... 其他上下文信息
    };
    console.log('上报 SPA 页面停留时长:', data);
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/api/page-duration', JSON.stringify(data));
    } else {
        fetch('/api/page-duration', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), keepalive: true }).catch(e => console.error('发送停留时长失败:', e));
    }
}
function startTrackingNewPage(newPageId) {
    // 结束旧页面的追踪并上报
    if (currentPageId && startTime > 0) {
        if (document.visibilityState === 'visible') {
            totalActiveTime += (Date.now() - lastActiveTime);
        }
        sendPageDuration(currentPageId, totalActiveTime, 'route_change');
    }
    // 开始新页面的追踪
    startTime = Date.now();
    lastActiveTime = startTime;
    totalActiveTime = 0;
    currentPageId = newPageId;
    console.log(`SPA 页面 ${currentPageId} 加载,开始计时: ${startTime}`);
}
// 监听页面可见性变化 (与多页应用相同)
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
        totalActiveTime += (Date.now() - lastActiveTime);
        console.log(`SPA 页面 ${currentPageId} 变为不可见,累加活跃时间: ${totalActiveTime}`);
    } else {
        lastActiveTime = Date.now();
        console.log(`SPA 页面 ${currentPageId} 变为可见,恢复计时: ${lastActiveTime}`);
    }
});
// 监听路由变化
// 对于 React Router, Vue Router 等,你需要监听它们提供的路由事件
// 这里以原生的 History API 监听为例
window.addEventListener('popstate', () => {
    startTrackingNewPage(window.location.pathname);
});
const originalPushState = history.pushState;
history.pushState = function() {
    originalPushState.apply(history, arguments);
    startTrackingNewPage(window.location.pathname);
};
const originalReplaceState = history.replaceState;
history.replaceState = function() {
    originalReplaceState.apply(history, arguments);
    // replaceState 通常不视为页面切换,但如果需要,也可以在这里触发
    // startTrackingNewPage(window.location.pathname);
};
// 首次加载时启动追踪
window.addEventListener('load', () => {
    startTrackingNewPage(window.location.pathname);
});
// 页面最终卸载时上报(用户关闭浏览器或离开整个 SPA)
window.addEventListener('pagehide', () => {
    if (currentPageId && startTime > 0) {
        if (document.visibilityState === 'visible') {
            totalActiveTime += (Date.now() - lastActiveTime);
        }
        sendPageDuration(currentPageId, totalActiveTime, 'app_unload');
        currentPageId = ''; // 重置
        startTime = 0;
        totalActiveTime = 0;
        lastActiveTime = 0;
    }
});
window.addEventListener('beforeunload', () => {
    if (currentPageId && startTime > 0) {
        if (document.visibilityState === 'visible') {
            totalActiveTime += (Date.now() - lastActiveTime);
        }
        sendPageDuration(currentPageId, totalActiveTime, 'app_unload');
        currentPageId = '';
        startTime = 0;
        totalActiveTime = 0;
        lastActiveTime = 0;
    }
});
// 心跳上报 (与多页应用相同,确保在 load 事件中启动)
let heartbeatInterval;
window.addEventListener('load', () => {
    heartbeatInterval = setInterval(() => {
        if (document.visibilityState === 'visible' && currentPageId) {
            const currentActiveTime = Date.now() - lastActiveTime;
            totalActiveTime += currentActiveTime;
            lastActiveTime = Date.now();
            console.log(`SPA 心跳上报 ${currentPageId} 活跃时间: ${currentActiveTime}ms, 累计: ${totalActiveTime}ms`);
            sendPageDuration(currentPageId, currentActiveTime, 'heartbeat');
        }
    }, 30 * 1000); // 每 30 秒
});
window.addEventListener('pagehide', () => clearInterval(heartbeatInterval));
window.addEventListener('beforeunload', () => clearInterval(heartbeatInterval));

代码讲解:

  • startTrackingNewPage(newPageId): 这是 SPA 方案的核心函数。

    • 每次路由变化时调用它。
    • 它会先计算并上报前一个页面的停留时长。
    • 然后重置计时器,开始计算新页面的停留时长。
  • 路由监听:

    • window.addEventListener('popstate', ...): 监听浏览器前进/后退按钮导致的 URL 变化。

    • history.pushState 和 history.replaceState 的劫持: SPA 框架通常通过这些方法来改变 URL 而不触发页面刷新。通过劫持它们,我们可以在路由发生变化时触发 startTrackingNewPage

    • 注意: 如果你使用 React Router, Vue Router 等,它们通常提供了更方便的路由守卫或事件钩子来监听路由变化,你应该优先使用框架提供的 API。例如:

      • React Router: 在 useEffect 中监听 location.pathname 变化。
      • Vue Router: 使用 router.beforeEach 或 router.afterEach 导航守卫。

总结与最佳实践

  1. 区分多页应用和单页应用: 根据你的应用类型选择合适的监听策略。
  2. 结合 Visibility API: 确保只计算用户真正“活跃”在页面上的时间。
  3. 使用 navigator.sendBeacon: 确保在页面卸载时数据能够可靠上报。
  4. 心跳上报: 对于长时间停留的页面,定期上报数据,防止数据丢失。
  5. 唯一页面标识: 确保每个页面都有一个唯一的 ID,以便后端能够正确聚合数据。
  6. 上下文信息: 上报数据时,包含用户 ID、会话 ID、设备信息、浏览器信息等,以便更深入地分析用户行为。
  7. 后端处理: 后端需要接收这些数据,并进行存储、聚合和分析。例如,可以计算每个页面的平均停留时间、总停留时间、不同用户群体的停留时间等。
  8. 数据准确性: 即使有了这些方案,停留时长仍然是一个近似值,因为总有一些极端情况(如断网、浏览器崩溃)可能导致数据丢失。目标是尽可能提高数据的准确性和覆盖率。

转自https://juejin.cn/post/7510803578505134119


该文章在 2025/6/4 11:59:09 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved