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

vit*_*y-t 2 rxjs angular

我需要实现一种方法来跟踪浏览器(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()
);
Run Code Online (Sandbox Code Playgroud)

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

客户端将使用上面的代码,如下所示:

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

    // if currently logged in, then log out
});
Run Code Online (Sandbox Code Playgroud)

在 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());
  }

}
Run Code Online (Sandbox Code Playgroud)

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

constructor(private inactivity: InactivityService) {
    this.inactivity.$onIdle.subscribe(() => {
        // user is idle for 5 minutes;
        // it is emitted repeatedly
    });
}
Run Code Online (Sandbox Code Playgroud)

Jos*_*man 5

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

另一个“不活动”定义可以是“只要选项卡处于活动状态,用户就处于活动状态”。在现代浏览器中,可以通过页面可见性 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));
};
Run Code Online (Sandbox Code Playgroud)

我自己还没有测试过,但这个想法很简单。它遵循您的原始实现,但我们使用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());
    }
});
Run Code Online (Sandbox Code Playgroud)

然后可以像这样使用:

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

编辑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()
  );
};
Run Code Online (Sandbox Code Playgroud)

现在的想法是,我们有某种计时器interval实际上是计时器),除非检测到ActivityIndi​​catorgracePeriod事件,否则它将发出每个计时器。问题是这将完成流,所以我们需要使用它来保持它的活动:)takeUntilrepeat