Created
June 1, 2025 13:54
-
-
Save ijse/b5a33cca5da766fe70297bb832d86c8b to your computer and use it in GitHub Desktop.
用户页面活跃状态跟踪
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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