大多数情况下,您并不真正关心用户是否积极参与或在您的应用程序上暂时处于非活动状态。非活动状态,意味着,也许他们起身去喝水,或者更有可能,切换标签去做其他事情。但是,在某些情况下,跟踪用户活动和检测非活动状态可能很方便。
让我们思考一下您可能需要该功能的几个示例
- 跟踪文章阅读时间
- 自动保存表单或文档
- 自动暂停游戏
- 隐藏视频播放器控件
- 出于安全原因自动注销用户
我最近遇到了一个涉及最后一个示例的功能,出于安全原因自动注销非活动用户。
为什么我们要关心自动注销?
许多应用程序允许用户访问一定数量的个人数据。根据应用程序的目的,数据的数量和价值可能有所不同。它可能只是用户的姓名,但也可能是更敏感的数据,例如医疗记录、财务记录等。
一些用户可能会忘记注销并保持会话打开。这种情况发生在你身上多少次了?也许你的手机突然响了,或者你需要立即离开,让浏览器保持开启状态。保持用户会话打开状态很危险,因为其他人可能会使用该会话提取敏感数据。
解决此问题的一种方法是跟踪用户是否在特定时间段内与应用程序交互,如果超过该时间则触发注销。您可能希望显示一个弹出窗口,或者一个警告用户即将注销的计时器。或者,您可以在检测到非活动用户时立即注销。
更进一步,我们要做的是计算从用户上次交互开始经过的时间。如果该时间段长于我们的阈值,我们希望触发我们的非活动处理程序。如果用户在阈值被突破之前执行操作,我们重置计数器并重新开始计数。
本文将展示我们如何基于 此示例 实现此类活动跟踪逻辑。
步骤 1:实现跟踪逻辑
让我们实现两个函数。第一个负责在用户每次与应用程序交互时重置我们的计时器,第二个负责处理用户变得非活动的情况
resetUserActivityTimeout
– 这将是我们的方法,负责在用户每次与应用程序交互时清除现有的超时并启动一个新的超时。inactiveUserAction
– 这将是在用户活动超时运行时触发的我们的方法。
let userActivityTimeout = null;
function resetUserActivityTimeout() {
clearTimeout(userActivityTimeout);
userActivityTimeout = setTimeout(() => {
inactiveUserAction();
}, INACTIVE_USER_TIME_THRESHOLD);
}
function inactiveUserAction() {
// logout logic
}
好的,所以我们有负责跟踪活动的方法,但我们还没有在任何地方使用它们。
步骤 2:跟踪激活
现在我们需要实现负责激活跟踪的方法。在这些方法中,我们 添加事件监听器,当检测到事件时,这些监听器将调用我们的 resetUserActivityTimeout
方法。您可以监听任意数量的事件,但为简单起见,我们将该列表限制为一些最常见的事件。
function activateActivityTracker() {
window.addEventListener("mousemove", resetUserActivityTimeout);
window.addEventListener("scroll", resetUserActivityTimeout);
window.addEventListener("keydown", resetUserActivityTimeout);
window.addEventListener("resize", resetUserActivityTimeout);
}
就是这样。我们的用户跟踪已准备就绪。我们唯一需要做的是在页面加载时调用 activateActivityTracker
。
我们可以这样保留它,但如果你仔细观察,我们刚刚提交的代码存在一个严重的性能问题。用户每次与应用程序交互时,整个逻辑都会运行。这很好,但再仔细看看。某些类型的事件在用户与页面交互时会触发大量次,即使这对我们的跟踪来说不是必需的。让我们看看 mousemove
事件。即使您只稍微移动一下鼠标,mousemove
事件也会触发数十次。这是一个真正的性能杀手。我们可以通过引入一个节流器来解决这个问题,该节流器将只允许用户活动逻辑在指定的时间段内触发一次。
让我们现在就来做。
步骤 3:提高性能
首先,我们需要添加一个变量,该变量将保留对我们的节流器超时的引用。
let userActivityThrottlerTimeout = null
然后,我们创建一个方法来创建我们的节流器。在该方法中,我们检查节流器超时是否已存在,如果不存在,我们创建一个超时,该超时将在特定时间段后触发 resetUserActivityTimeout
。这是所有用户活动在该时间段内不会再次触发跟踪逻辑的时间段。在此时间之后,节流器超时将被清除,允许下一次交互重置活动跟踪器。
userActivityThrottler() {
if (!userActivityThrottlerTimeout) {
userActivityThrottlerTimeout = setTimeout(() => {
resetUserActivityTimeout();
clearTimeout(userActivityThrottlerTimeout);
userActivityThrottlerTimeout = null;
}, USER_ACTIVITY_THROTTLER_TIME);
}
}
我们刚刚创建了一个应该在用户交互时触发的新方法,因此我们需要记住在我们的激活逻辑中将事件处理程序从 resetUserActivityTimeout
更改为 userActivityThrottler
。
activateActivityTracker() {
window.addEventListener("mousemove", userActivityThrottler);
// ...
}
奖励:让我们用 Vue 重写它!
现在我们已经实现了我们的活动跟踪逻辑,让我们看看如何将该逻辑移动到使用 Vue 构建的应用程序中。我们将根据 此示例 进行解释。
首先,我们需要将所有变量移动到组件的 data
中,这是所有反应式属性所在的位置。
export default {
data() {
return {
isInactive: false,
userActivityThrottlerTimeout: null,
userActivityTimeout: null
};
},
// ...
然后我们将所有函数移动到 methods
中
// ...
methods: {
activateActivityTracker() {...},
resetUserActivityTimeout() {...},
userActivityThrottler() {...},
inactiveUserAction() {...}
},
// ...
由于我们正在使用 Vue 及其反应式系统,我们可以放弃所有直接的 DOM 操作(即 document.getElementById("app").innerHTML
)并依赖我们的 isInactive
数据属性。我们可以在组件的模板中直接访问数据属性,如下所示。
<template>
<div id="app">
<p>User is inactive = {{ isInactive }}</p>
</div>
</template>
我们需要做的最后一件事是找到一个合适的位置来激活跟踪逻辑。Vue 带有组件 生命周期钩子,这正是我们需要的——特别是 beforeMount
钩子。所以让我们把它放在那里。
// ...
beforeMount() {
this.activateActivityTracker();
},
// ...
我们还可以做一件事。由于我们正在使用超时并在窗口上注册事件监听器,因此始终建议在完成后进行一些清理。我们可以在另一个生命周期钩子 beforeDestroy
中执行此操作。当组件的生命周期结束时,让我们删除我们注册的所有监听器并清除所有超时。
// ...
beforeDestroy() {
window.removeEventListener("mousemove", this.userActivityThrottler);
window.removeEventListener("scroll", this.userActivityThrottler);
window.removeEventListener("keydown", this.userActivityThrottler);
window.removeEventListener("resize", this.userActivityThrottler);
clearTimeout(this.userActivityTimeout);
clearTimeout(this.userActivityThrottlerTimeout);
}
// ...
总结!
此示例完全专注于检测用户与应用程序的交互,对其做出反应并在特定时间段内未检测到交互时触发方法。我希望此示例尽可能通用,因此这就是为什么我将检测到非活动用户时应发生的事情的实现留给您。
希望您能在您的项目中发现此解决方案有用!
你好,Mateusz Rybczonek!
你能告诉我上面给出的代码是用于实时网站还是像普通 js 编码一样的实验性 js?
对于上面任何英语表达不佳的地方,我表示歉意。
你好,Mahesh,
如上所述,此代码只是整个自动注销功能的一小部分,完整的功能必须包含前端和后端部分。
我想象了一些需要更复杂解决方案的非活动检测的粗略案例。如果用户的鼠标坏了怎么办。它不断进行微小移动。或者用户设置了一个软件来故意这样做。或者,用户禁用了负责非活动检测的脚本部分。
我更倾向于使用服务器端解决方案来补充你的方案,该方案会检查用户上次请求资源或操作的时间,并将该时间回传给应用(或者不回传,直接将其注销)。当然,大多数服务器都有内置的会话过期机制,但在某些情况下你可能不希望使用它。
嗨,Artur,
我同意,这只是一个开始,它只展示了一种检测非活动用户的方法。
正如你提到的,通常后端会有一个会话,在一段时间后过期。每次用户调用 API 时,该会话都会被延长。
此解决方案展示了如何检测用户是否在特定时间段内处于非活动状态,以及如何对这种非活动状态做出反应。例如,你的会话在 10 分钟后过期,在 8 分钟的非活动时间后,你可能希望向用户显示会话即将过期,如果未采取任何操作,他将被注销。
感谢你的评论。
这是一个好的开始,但根据我的个人经验,它缺少一些重要的部分
1. 如果安全是目标,客户端自动注销是不够的。服务器必须在一定时间段的非活动后终止会话。
2. 用户可能会关闭浏览器或断开连接,同样出于安全考虑,这仍然应该导致用户注销。
3. 由于 1 和 2,你几乎被迫显示一个“你还在吗?”对话框,如果点击“是”,则向服务器发送 ping 以保持会话活动。当用户处于活动状态时,你可能希望定期向服务器发送 ping 以与客户端保持同步。
4. 客户端可能打开了多个选项卡,你需要在这些选项卡之间同步身份验证超时。LocalStorage 可用于此目的。
5. 作为后备方案,如果服务器和客户端不同步,特别是由于客户端不可靠,你需要捕获 400 错误并处理它们。
在我看来,活动超时是那些看似简单但实际上很难正确实现的事情之一。
是的,使用 LocalStorage 在浏览器选项卡之间同步似乎是一个好主意。
嗨,Aaron,
我完全同意你的评论,这只是一个开始,它只展示了一种检测非活动用户的方法。
感谢你的评论。
解决方案:只需限制一个去抖动的注销方法即可。搞定。
现在仍然有代码在每次事件发生时运行。取消注册事件监听器怎么样?
嗨,Jakob,
你可以在这两个沙箱中看到区别。
https://codesandbox.io/s/activity-tracker-vanilla-js-events-count-without-throttler-t89gx
https://codesandbox.io/s/activity-tracker-vanilla-js-events-count-with-throttler-4d3hy
感谢你关于取消注册事件监听器的建议,已添加到示例中。
非常感谢,Mateusz!
这真的很有帮助 :)