深入理解空闲定时器:原理、实现与测试指南

本文详细介绍了空闲定时器的概念、应用场景及实现原理,通过React-idle-timer库的实战示例展示如何集成和测试空闲检测功能,并对比了多种空闲检测解决方案的优缺点。

理解空闲定时器

我将通过一个实际可用的示例和适当的单元测试示例来解释空闲定时器,并比较不同的空闲定时器解决方案。

什么是空闲定时器?

空闲定时器是一个软件组件,用于跟踪应用程序或网站内的用户不活动或空闲状态。它测量用户保持不活动的时间长度——没有鼠标移动、键盘输入或触摸事件——并在预定义的空闲时间后触发特定操作,例如注销用户、显示警告或保存工作。

空闲定时器解决什么问题?

空闲定时器在许多场景中至关重要,包括:

  • 安全性:在不活动后自动注销用户以防止未经授权的访问
  • 资源管理:当用户离开应用程序空闲时释放资源或断开会话
  • 用户体验:当用户处于不活动状态时提供及时的警告或提示
  • 合规性:满足有关会话超时的监管要求

没有空闲定时器,应用程序可能会无限期保持会话开放,这可能导致安全漏洞或资源浪费。

空闲定时器如何工作?

在其核心,空闲定时器利用硬件或软件计数器,该计数器以来自时钟源的已知频率递增。通过设置阈值,它可以在计数器达到特定计数时触发事件,从而有效地测量经过的时间或生成周期性信号。

例如,在微控制器环境中:

  • 定时器的计数器开始计数时钟脉冲
  • 当计数器与预设值匹配时,生成中断
  • 中断处理程序执行用户定义的代码,例如读取传感器或更新显示

在纯软件环境中,空闲定时器可能使用系统时钟结合高效的轮询或事件驱动机制来模拟定时器功能。

实践示例:集成和单元测试空闲定时器

为了演示如何有效地将空闲定时器集成到您的项目中并通过单元测试确保其可靠性,我创建了一个示例仓库,其中包含一个可工作的示例和全面的测试覆盖。

集成概述

该示例仓库展示了如何:

  • 使用可配置的间隔初始化空闲定时器
  • 注册回调函数来处理定时器事件
  • 在应用程序流中根据需要启动和停止定时器

以下是集成代码的简化片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import {Dialog, Button} from '../components';
import { useAuthenticationStore } from '../auth/store';
import React, { useState } from 'react';
import { useIdleTimer } from 'react-idle-timer';

export const PROMT_TIME = 1000 * 15 * 60;
export const TIMEOUT = 1000 * 30 * 60; 

export function SessionTimer() {
  const { user, actions } = useAuthenticationStore();
  const [open, setOpen] = useState<boolean>(false);
  
  const onIdle = () => {
    setOpen(false);
    actions.logout()
  };

  const onActive = () => {
    setOpen(false);
  };

  const onPrompt = () => {
    setOpen(true);
  };

  const { activate } = useIdleTimer({
    onIdle,
    onActive,
    onPrompt,
    timeout: TIMEOUT,
    promtBeforeIdle: PROMT_TIME,
    throttle: 500,
    disabled: !user,
  });

  const handleStillHere = () => {
    activate();
  };

  return (
    <Dialog open={open} onOpenChange={handleStillHere}>
      <p>Are you still here?</p>
      <Button onClick={handleStillHere}>
        Continue
      </Button>
      <Button onClick={onIdle}>
        End session
      </Button>
    </Dialog>
  );
}

这种模式确保您的应用程序能够清晰高效地响应定时器事件。

单元测试空闲定时器

测试定时器可能具有挑战性,因为它们具有异步性质并且依赖于实时。对于测试设置,我们使用 jest.useFakeTimers。此外,我们应该确保使用正确的 jest 时间函数,因为 react-idle-timer 底层不仅仅使用 setInterval 或 setTimeout。当使用系统时间时,我们需要推进时间。因此我们应该使用 jest.setSystemTime 而不是 jest.advanceTimersByTime

1
2
3
4
5
6
// ❌
jest.advanceTimersByTime(4 * 60 * 1000); // 模拟4分钟不活动

// ✅
jest.setSystemTime(start + 4 * 60 * 1000); // 模拟4分钟不活动
fireEvent.focus(document); // 触发任何事件以检查超时

基本测试示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import '@testing-library/jest-dom';
import {
  act,
  cleanup,
  fireEvent,
  render,
  screen,
} from '@testing-library/react';

import { SessionTimer, PROMT_TIME } from './session-timer';

describe('SessionTimer', () => {
  jest.useFakeTimers();

  beforeEach(() => {
    jest.clearAllTimers();
  });

  afterAll(() => {
    cleanup();
    jest.clearAllTimers();
    jest.useRealTimers();
  });

  it('calls onPrompt and opens the dialog when user is prompted', () => {
    render(<SessionTimer />);
    const start = Date.now();
    jest.setSystemTime(start);

    act(() => {
      jest.setSystemTime(start + PROMT_TIME);
      fireEvent.focus(document);
    });

    expect(screen.getByText('Are you still there?')).toBeInTheDocument();
  })
})

在仓库中,我实现了单元测试,这些测试:

  • 行为类似于模拟定时器,无需实时等待即可模拟滴答
  • 验证回调是否在预期间隔被调用
  • 确保定时器启动、停止和重新配置功能按预期工作

为什么单元测试在这里很重要

  • 可靠性:确认定时器在各种场景下表现一致
  • 回归预防:在修改定时器逻辑时防止错误
  • 文档:为未来的开发人员提供可执行的示例

比较和替代方案

React-idle-timer

概述

  • 描述:最流行的 React 库,通过跟踪鼠标移动、键盘输入、滚动和触摸等事件来检测用户不活动
  • 特性
    • 支持可自定义的超时持续时间
    • 提供钩子和组件 API(useIdleTimer 钩子和 <IdleTimer> 组件)
    • 检测空闲、活动和超时状态
    • 支持跨标签页同步(可选)
    • 允许暂停、恢复、重置和手动控制

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import { useIdleTimer } from 'react-idle-timer';

function App() {
  const handleOnIdle = () => {
    console.log('User is idle');
    // 例如,注销用户或显示警告
  };

  const { getRemainingTime, reset } = useIdleTimer({
    timeout: 1000 * 60 * 5, // 5分钟
    onIdle: handleOnIdle,
    debounce: 500,
  });

  return (
    <div>
      <h1>React Idle Timer Example</h1>
      <button onClick={reset}>Reset Timer</button>
      <p>Remaining Time: {getRemainingTime()} ms</p>
    </div>
  );
}

优点

  • 维护良好且广泛使用
  • 灵活的 API,包含钩子和组件
  • 跨标签页空闲检测支持
  • 良好的 TypeScript 支持
  • 活跃的社区和文档

缺点

  • 与最小替代方案相比,包大小稍大
  • 对于非常简单的用例可能过于复杂

React-idle

概述

  • 用于空闲检测的简单 React 组件
  • 当用户变为空闲时触发回调
  • 与 react-idle-timer 相比功能较少

优点

  • 轻量级且易于使用
  • 用于基本空闲检测的简单 API

缺点

  • 功能有限(无钩子,无跨标签页支持)
  • 开发活跃度较低

UseIdle(React 钩子)

概述

  • 用于空闲检测的最小自定义 React 钩子
  • 通过事件监听器检测用户不活动
  • 通常作为小型实用程序而不是完整库实现

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState, useEffect } from 'react';

function useIdle(timeout = 300000) {
  const [isIdle, setIsIdle] = useState(false);

  useEffect(() => {
    let timer = setTimeout(() => setIsIdle(true), timeout);
    const resetTimer = () => {
      clearTimeout(timer);
      setIsIdle(false);
      timer = setTimeout(() => setIsIdle(true), timeout);
    };

    window.addEventListener('mousemove', resetTimer);
    window.addEventListener('keydown', resetTimer);
    window.addEventListener('scroll', resetTimer);

    return () => {
      clearTimeout(timer);
      window.removeEventListener('mousemove', resetTimer);
      window.removeEventListener('keydown', resetTimer);
      window.removeEventListener('scroll', resetTimer);
    };
  }, [timeout]);

  return isIdle;
}

优点

  • 最小依赖和小占用空间
  • 完全可定制且易于扩展

缺点

  • 需要手动设置跨标签页同步等功能
  • 没有内置的暂停/恢复或重置 API

IdleTimer.js(原生 JS 库)

概述

  • 轻量级原生 JavaScript 空闲定时器库
  • 可以集成到 React 应用程序中
  • 支持超时、事件和回调机制

优点

  • 框架无关,可与任何 JavaScript 框架一起使用
  • 小巧且性能良好

缺点

  • 需要手动 React 集成
  • 缺少 React 特定功能,如钩子或组件

比较表

特性 react-idle-timer react-idle useIdle 钩子 IdleTimer.js
React 特定 API 是(钩子和组件) 是(组件) 自定义钩子 否(原生 JS)
跨标签页支持
暂停/恢复 API
TypeScript 支持 取决于用户
包大小 中等 非常小 非常小
社区和维护 活跃 不太活跃 自定义解决方案 中等
复杂性 功能丰富 基本 最小 基本

何时使用哪个?

  • 使用 react-idle-timer:如果您需要功能丰富的强大解决方案,具有跨标签页支持以及钩子和组件 API。适用于大多数需要全面空闲检测的生产 React 应用程序。
  • 使用 react-idle:如果您想要一个非常简单的组件进行基本空闲检测,并且不需要高级功能。
  • 使用自定义 useIdle 钩子:如果您更喜欢最小依赖并希望完全控制实现,特别是对于小型或高度自定义的项目。
  • 使用 IdleTimer.js:如果您想要一个与框架无关的解决方案或计划与非 React 部分集成您的应用程序。

探索仓库

您可以在此处探索完整示例和测试:

  • GitHub 仓库:https://github.com/h-labushkina/idle-timer
  • 在线演示:https://hannas-idl-timer.netlify.app/

欢迎克隆它,进行实验,并将模式适应到您自己的项目中。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计