作为 Web 开发人员,您可能已经注意到某些 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 元素(例如,a <button>
)的直接结果导致代码未运行时,会发生此错误。
拥有作为最终用户操作结果运行的代码就是 HTML 规范所指的“用户激活”。网络上有大量依赖于用户激活的 API。常见的包括:
window.open()
navigator.share()
navigator.wakelock.request()
PaymentRequest.prototype.show()
- 还有很多很多……
那么什么是“用户激活”呢?
HTML 规范将以下事件定义为“激活触发用户事件”:
keydown
, 不包括 Escape 键和可能由浏览器或操作系统保留的一些键mousedown
pointerdown
,但pointerType
必须是“鼠标”pointerup
, 只要pointerType
不是“鼠标”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 通知的
requestPermission()
方法。 - 付款请求:
show()
方法。 - 而且,正如我们已经讨论过的,Web Share 的
share()
方法。
此列表并不详尽,并且一直在向 Web 添加新的 API,这些 API 依赖或消耗瞬时激活。
有趣的是:并非所有 API 都会消耗用户激活。有些只需要短暂激活但不会消耗它。这允许发生依赖于用户激活的多个异步操作。否则,将需要用户一遍又一遍地单击或按下按钮才能完成任务,这对他们来说会很烦人。
瞬态激活范围
了解瞬态激活的一个真正有用的事情是它的范围是整个窗口(或浏览器选项卡)!这意味着,只要iframe
页面上的所有 s 都是同源的,它们都具有瞬态激活。但是出于安全考虑,跨域iframe
不会有瞬态激活。
对于 third-partyiframe
具有瞬态激活,用户必须显式激活 third-party 中的 HTML 元素iframe
。然而,一旦它们激活了一个元素,瞬态激活就会传播到父元素和任何与激活发生的地方iframe
同源的元素:iframe
安全注意事项:您可以(并且应该!)根据需要通过设置allow=
和/或属性来限制第三方 iframe 可以访问的容量。sandbox=
UserActivation
应用程序接口
为了帮助开发人员处理用户激活,HTML 标准引入了一个简单的 API 来检查页面是否具有瞬态和/或粘性激活。
navigator.userActivation.isActive
:
当窗口有瞬时激活时返回真。navigator.userActivation.hasBeenActive
:
如果窗口在过去有过瞬态激活(即“粘性激活”),则返回真。
因此,例如,您可以执行以下操作:
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 在用户激活时受到限制有助于保护用户免受恼人的入侵,例如多个弹出窗口或通知垃圾邮件,同时允许开发人员做正确的事情来响应用户交互。APIUserActivation
可以帮助您确定是否可以调用依赖于用户激活的函数。
您可以在Safari Technology Preview 版本 160或更高版本中试用用户激活 API 。