我需要在浏览器(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
});
}
这实际上是一个很好的问题!
您跟踪某些活动事件的方法很聪明,但最终它实际上取决于您对“不活动”的定义。如果您对当前的定义感到满意,那么您可能可以跟踪更多的内容,例如滚动或调整大小。
另一个“不活动”定义可以是“只要选项卡处于活动状态,用户就处于活动状态”。在现代浏览器中,这可以通过 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
来保持它的活力:)