使用 RXJS 进行空闲/不活动跟踪

问题描述 投票:0回答:1

我需要在浏览器(Angular 应用程序)中实现一种跟踪空闲状态/不活动的方法,因此我尝试创建一个可观察对象,只要没有任何用户交互事件就会触发,因此客户端可以自动注销:

import {filter, from, fromEvent, mergeAll, Observable, of, repeat, timeout} from 'rxjs';

const timeoutDelay = 1000 * 60 * 5; // 5 minutes
const events = ['keypress', 'click', 'wheel', 'mousemove', 'ontouchstart'];    
const $onInactive = from(events.map(e => fromEvent(document, e)))
  .pipe(
    mergeAll(),
    timeout({each: timeoutDelay, with: () => of(undefined as any)}),
    filter(a => !a),
    repeat()
);

虽然我能够让它与上面的代码一起工作,但我不确定对于这个特定任务是否正确或有效地完成了,所以我只是想在这里仔细检查一下。

客户会像这样使用上面的代码:

$onInvactive.subscribe(() => {
    // no user activity for 5 minutes

    // if currently logged in, then log out
});

在 Web 客户端的整个生命周期中,只要发生不活动,通知就应该到达。

解决方案

根据接受的答案,下面是一个完整的 Angular v15 服务:

import {Injectable} from '@angular/core';
import {
  Observable,
  fromEvent, interval, merge,
  repeat, takeUntil, map,
} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class InactivityService {

  readonly timeoutDelay = 1000 * 60 * 5; // 5 minutes

  readonly $onIdle: Observable<void>;

  constructor() {
    const events = ['keypress', 'click', 'wheel', 'mousemove', 'ontouchstart'];
    const $signal = merge(...events.map(eventName => fromEvent(document, eventName)));
    this.$onIdle = interval(this.timeoutDelay).pipe(takeUntil($signal), map(() => undefined), repeat());
  }

}

因此任何组件或其他服务都可以轻松跟踪空闲状态:

constructor(private inactivity: InactivityService) {
    this.inactivity.$onIdle.subscribe(() => {
        // user is idle for 5 minutes;
        // it is emitted repeatedly
    });
}
angular rxjs
1个回答
4
投票

这实际上是一个很好的问题!
您跟踪某些活动事件的方法很聪明,但最终它实际上取决于您对“不活动”的定义。如果您对当前的定义感到满意,那么您可能可以跟踪更多的内容,例如滚动或调整大小。

另一个“不活动”定义可以是“只要选项卡处于活动状态,用户就处于活动状态”。在现代浏览器中,这可以通过 Page Visibility API 跟踪,也就是监听文档中的

visibilitychange
事件。在窗口对象上使用窗口焦点和模糊事件也应该作为一种解决方法。

现在,谈到实施本身,我将建议一种简化的方法。

const browserInactive$ = (graceTime: number): Observable<Event> => {
    const activityIndicatorEvents = ['click', 'mousemove', 'mousedown', 'scroll', 'keypress'];
    return merge(...activityIndicatorEvents.map(eventName => fromEvent(document, eventName))).pipe(debounceTime(graceTime));
};

我自己没有测试过,但是这个想法很简单。它遵循您的原始实现,但我们使用

debounceTime
作为等待 graceTime“过期”的机制,并使用函数作为配置行为的便捷方式。

编辑 1: 有一个想法可以让您的 Angular 组件或服务更容易地使用 Observable

export const BROWSER_INACTIVE$ = new InjectionToken<Observable<Event>>('Browser inactivity detector', {
    factory: () => {
        const activityIndicatorEvents = ['click', 'mousemove', 'mousedown', 'scroll', 'keypress'];
        const graceTime = 1000 * 60 * 5; // 5 minutes
        return merge(...activityIndicatorEvents.map(eventName => fromEvent(document, eventName))).pipe(debounceTime(graceTime), share());
    }
});

然后可以这样使用:

@Component({...})
export class SomeComponent {
  constructor(
    @Inject(BROWSER_INACTIVE$) private readonly browserInactive$: Observable<Event>
  ) {}
}

编辑 2:基于 OP 反馈。 在那种情况下,我们可以使用稍微不同的方法:

const browserInactive$ = (graceTime: number): Observable<any> => {
  const activityIndicatorEvents$ = merge(
      ...['click', 'mousemove', 'mousedown', 'scroll', 'keypress'].map(
          (eventName) => fromEvent(document, eventName)
      )
  );

  return interval(graceTime).pipe(
      takeUntil(activityIndicatorEvents$),
      repeat()
  );
};

现在的想法是我们有某种 timer(实际上是一个

interval
)它将发出每个
gracePeriod
除非检测到 activityIndicator 事件。问题是
takeUntil
将完成流,所以我们需要使用
repeat
来保持它的活力:)

© www.soinside.com 2019 - 2024. All rights reserved.