用户激活API
作为一名网页开发者,你可能已经注意到某些API只有在最终用户点击或轻触HTML元素后才能工作。例如,如果你尝试在Safari的Web Inspector中运行以下代码,它将导致错误
await navigator.share({ text: "hi" });
NotAllowedError: The request is not allowed by the user agent or
the platform in the current context, possibly because the user denied permission.
当代码不是最终用户点击或轻触HTML元素(例如,<button>
)的直接结果时,就会发生此错误。
让代码作为最终用户操作的结果运行,这就是HTML规范所指的“用户激活”。Web上有大量API依赖于用户激活。常见的包括
window.open()
navigator.share()
navigator.wakelock.request()
PaymentRequest.prototype.show()
- 还有很多很多…
那么,“用户激活”包括什么?
HTML规范将以下事件定义为“激活触发用户事件”
keydown
,不包括Escape键以及浏览器或操作系统可能保留的一些键mousedown
pointerdown
,但pointerType
必须是“mouse”pointerup
,只要pointerType
不是“mouse”touchend
总而言之,这个列表有效地构成了“用户激活”。你会注意到上面的事件列表非常小。它被限制是为了让某些API调用只能作为这些非常独特的最终用户操作的结果发生。这可以防止最终用户意外地(或故意!)被弹出窗口或其他侵入性浏览器对话框骚扰。
既然我们了解了这些特殊事件,我们现在可以编写考虑用户激活的代码
button.addEventListener("click", async () => {
// This works fine...
await navigator.share({text: "hi"});
});
因此,即使我们没有专门监听 "mousedown"
事件,我们也知道激活触发事件已经发生,所以我们的代码可以运行而不会抛出任何错误。
最终用户保护
你现在可能会想,能否在一个或多个事件监听器中执行多个需要用户激活的命令?考虑以下代码示例
button.addEventListener("click", async () => {
// This works fine...
await navigator.share({text: "hi"});
// This will fail...
window.open("https://example.com");
});
button.addEventListener("click", async () => {
// This will now fail too...
window.open("https://example.com");
});
但是为什么 window.open()
的调用会失败呢?为了理解这一点,我们需要更深入地探讨浏览器如何在底层处理用户激活。
了解“瞬时”和“粘性”激活
当“激活触发用户事件”发生时,实际情况是浏览器启动一个专门与浏览器标签页绑定的内部计时器。这个计时器不直接暴露给网页,并且运行时间很短(几秒钟左右)。每个浏览器引擎都可以决定分配多少时间,并且它可能由于多种原因而改变(即,它故意不能被JavaScript观察到!)。它旨在为你的代码提供足够的时间来执行某些特定任务(例如,它可以处理一些图像数据,然后调用 navigator.share()
将图像分享给另一个应用程序)。
在HTML中,这个计时器被称为瞬时激活。当这个计时器运行时,该浏览器窗口“具有瞬时激活”。HTML还定义了一个称为粘性激活的概念。这仅仅意味着网页在过去某个时间点具有瞬时激活。
尽管罕见,一些API(例如Web Audio)使用粘性激活来执行某些操作。
现在,上述内容并未解释为什么 window.open()
会失败。为了理解原因,我们现在需要讨论HTML所谓的“激活消耗API”。
“消耗”用户激活的API
顾名思义,“激活消耗API”会消耗用户激活。也就是说,当调用这些API时,它们会有效地重置瞬时激活计时器,因此网页不再具有瞬时激活。
这种行为是 window.open()
失败的原因:调用 navigator.share()
消耗了用户激活,这意味着 window.open()
不再具有瞬时激活(因此它失败了)。
WebKit中消耗瞬时激活的常见API列表
- Web Notification 的
requestPermission()
方法。 - Payment Request:
show()
方法。 - 以及,我们已经讨论过的,Web Share 的
share()
方法。
此列表并非详尽无遗,并且不断有新的API添加到Web中,它们要么依赖于瞬时激活,要么消耗瞬时激活。
有趣的是:并非所有API都消耗用户激活。有些API只需要瞬时激活但不会消耗它。这允许依赖用户激活的多个异步操作发生。否则,这将要求用户一遍又一遍地点击或按下按钮来完成任务,这对他们来说会非常烦人。
瞬时激活的范围
关于瞬时激活一个非常有用的知识是,它的作用域是整个窗口(或浏览器标签页)!这意味着,只要页面上的所有 iframe
都同源,它们都具有瞬时激活。然而,出于安全原因,跨源 iframe
将不具有瞬时激活。
要使第三方 iframe
具有瞬时激活,用户必须在第三方 iframe
内部明确激活一个HTML元素。但是,一旦他们激活了一个元素,瞬时激活就会传播到父级以及与激活发生的 iframe
同源的任何 iframe
安全提示:你可以(并且应该!)通过根据需要设置 allow=
和/或 sandbox=
属性来限制第三方iframe的访问权限。
UserActivation
API
为了帮助开发者处理用户激活,HTML标准引入了一个简单的API来检查页面是否具有瞬时和/或粘性激活。
navigator.userActivation.isActive
:
当窗口具有瞬时激活时返回true。navigator.userActivation.hasBeenActive
:
如果窗口在过去具有瞬时激活(即“粘性激活”),则返回true。
所以例如,你可以这样做
if (navigator.userActivation.isActive) {
await navigator.share({text: "hi"})
}
限制和正在进行中的标准工作
当前用户激活模型存在两个显著限制,标准制定者仍在努力解决。
首先,考虑以下情况,当文件下载时间过长且瞬时激活计时器耗尽时
button.onclick = () => {
// Slow network + really big file
const image = await fetch("really-big-file");
// Oh no!!! transient activation expired! 😢
navigator.share({files: [image]});
}
WHATWG和W3C正在就如何解决上述问题进行讨论。不幸的是,我们还没有解决方案,但自然我们需要一些方法来延长瞬时激活,这样上面的代码就不会失败。
其次,在第一方文档中启用第三方 iframe 中的瞬时激活存在合法的用例(例如,允许第三方处理支付请求)。正在进行讨论,以查看是否存在某种方法可以在特殊情况下安全地启用第三方 iframe
也具有瞬时激活。
自动化和测试
为了帮助开发者处理瞬时激活意外过期可能导致的棘手边缘情况,WebKit一直与其他浏览器厂商合作,允许通过Web Driver消耗用户激活。
结论
Web API受用户激活控制有助于保护用户免受烦人的干扰,例如多个弹出窗口或通知垃圾邮件,同时允许开发者根据用户交互做正确的事情。UserActivation
API可以帮助你确定是否可以调用依赖用户激活的函数。
你可以在Safari 技术预览版 160 或更高版本中试用用户激活 API。