Skip to content

Instantly share code, notes, and snippets.

@ijse
Created June 1, 2025 13:54
Show Gist options
  • Save ijse/b5a33cca5da766fe70297bb832d86c8b to your computer and use it in GitHub Desktop.
Save ijse/b5a33cca5da766fe70297bb832d86c8b to your computer and use it in GitHub Desktop.
用户页面活跃状态跟踪
/* eslint-disable no-console */
interface ActiveTrackerConfigs {
/**
* 活跃事件名称列表
*/
watchEvents: string[];
/**
* 不活跃阈值(ms)
*
* @description 一段时间(INACTIVE_THRESHOLD)用户无操作,则判定为不活跃状态
*/
inactiveThreshold: number;
/**
* 活跃状态检查间隔(ms)
*
* @description 一段时间(CHECK_INTERVAL)用户无操作,则判定为不活跃状态
*/
checkInterval: number;
}
/**
* 用户活跃状态跟踪
*
* @description 一段时间(INACTIVE_THRESHOLD)用户无操作,则判定为不活跃状态
*/
export class ActiveTracker extends EventTarget {
private _watchEvents = ['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart'];
private _inactiveThreshold: number = 60_000;
private _checkInterval: number = 5_000;
private _activeStartAt: Date | null;
private _activeEndAt: Date;
private _interceptTimer: number;
private _lastActiveDuration: number = 0;
private readonly boundUpdateLastActive = this.updateLastActive.bind(this);
constructor(configs: Partial<ActiveTrackerConfigs> = {}) {
super();
this._watchEvents = configs.watchEvents || this._watchEvents;
this._inactiveThreshold = configs.inactiveThreshold || this._inactiveThreshold;
this._checkInterval = configs.checkInterval || this._checkInterval;
this.startTrack();
}
private startTrack() {
this.updateLastActive();
this._watchEvents.forEach(event => window.addEventListener(event, this.boundUpdateLastActive));
return this;
}
destroy() {
this._watchEvents.forEach(event => window.removeEventListener(event, this.boundUpdateLastActive));
window.clearTimeout(this._interceptTimer);
}
/**
* 记录活跃时间点
*/
private updateLastActive() {
if (!this._activeStartAt) {
this._activeStartAt = new Date();
// 不活跃 -> 活跃
this.emitStatusChange(true);
}
this._activeEndAt = new Date();
this.interceptActive();
console.debug(`[ActiveTracker] 更新活跃:`, {
isActive: this.isActive,
activeStartAt: this._activeStartAt.toLocaleString(),
activeEndAt: this._activeEndAt.toLocaleString(),
activeDuration: this.lastActiveDuration
});
}
/**
* 中断活跃状态
*
* @description 最近一次活跃操作后一段时间内无其它操作, 则中断连续的活跃记录
*/
private interceptActive() {
if (this._activeStartAt) {
this._lastActiveDuration = this._activeEndAt.getTime() - this._activeStartAt.getTime();
}
// 活跃事件持续触发情况下,只保留最后一次中断判定执行
window.clearTimeout(this._interceptTimer);
this._interceptTimer = window.setTimeout(() => {
console.debug(`[ActiveTracker] 中断活跃: `, {
isActive: this.isActive,
lastActiveDuration: this.lastActiveDuration,
inactiveDuration: Date.now() - this._activeEndAt.getTime()
});
if (!this.isActive) {
this._activeStartAt = null;
// 活跃 -> 不活跃
this.emitStatusChange(false);
} else {
this.interceptActive();
}
}, this._checkInterval);
}
/**
* 触发活跃状态变更事件
*
* @param nextStatus 活跃状态
*/
private emitStatusChange(nextStatus: boolean) {
console.debug(`[ActiveTracker] 状态切换: isActive=${nextStatus}`);
this.dispatchEvent(
new CustomEvent<ActiveStatusChangeData>('active-status-change', {
detail: {
isActive: nextStatus,
lastActiveDuration: this.lastActiveDuration
}
})
);
}
/**
* 当前是否活跃
*
* @description 最近一次活跃操作时间小于阈值
*/
get isActive() {
return this._activeEndAt?.getTime() + this._inactiveThreshold > Date.now();
}
/**
* 最近一次连续活跃时间(ms)
*/
get lastActiveDuration() {
return this._lastActiveDuration;
}
}
export interface ActiveStatusChangeData {
/**
* 是否活跃
*/
isActive: boolean;
/**
* 最近一次连续活跃时间(ms)
*/
lastActiveDuration: number;
}
export type ActiveStatusChangeEvent = CustomEvent<ActiveStatusChangeData>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment