使用户激活在所有 API 之间保持一致

Mustaq Ahmed
Joe Medley
Joe Medley

为了防止恶意脚本滥用弹出式窗口等敏感 API, 全屏等,浏览器可通过用户 。“用户激活”是指浏览会话在 对用户操作的影响:“有效”状态通常意味着 当前正在与网页互动,或者在浏览网页后完成过互动 加载。对于相同的概念,用户手势是一个热门但具有误导性的术语。对于 例如,用户的滑动或翻动手势不会激活页面,因此 从脚本的角度来看,不算是用户激活。

如今,主流浏览器在用户激活机制方面表现出大相径庭的行为 用于控制受激活限制的 API。在 Chrome 中,实现方式基于 基于词元的模型,结果过于复杂,无法定义一致的 行为。例如,Chrome 一直 通过 postMessage()setTimeout() 次调用;并且用户激活 Promise 支持, XHR游戏手柄互动等。请注意,其中有些功能 是热门但长期存在的错误。

在版本 72 中,Chrome 提供了 User Activation v2, 已针对所有受激活限制的 API 提供激活可用性。这解决了 上述不一致之处(以及其他一些问题,例如 MessageChannels),我们认为它可以简化网站访问 关于用户激活的开发过程。此外,新的实现方法 所提议的 新规范 旨在从长远来看将所有浏览器整合在一起。

User Activation v2 的工作原理是什么?

新 API 会在每个 window 对象上保持两位用户激活状态 :一个用于记录历史用户激活状态的固定位(如果 帧曾见过用户激活),以及表示当前状态的瞬时位 (如果帧在大约 1 秒后看到激活)。固定式 在框架的生命周期内设置后,它绝不会重置。瞬时位 会在每次用户互动时设置,并且在过期后重置 (大约一秒)或通过调用消耗激活量的 API (例如 window.open())。

请注意,不同的激活控制 API 依赖于不同应用场景 方式;新 API 不会改变任何这些特定于 API 的行为。例如: 每次用户激活只允许展示一个弹出式窗口,因为 window.open() 消耗了 用户激活方式与往常一样,Navigator.prototype.vibrate() 将继续 如果某个帧(或其任何子帧)曾发生过用户操作, 依此类推。

有何变化?

  • 用户激活 v2 正式定义了用户激活可见性的概念 跨帧边界:现在,当用户与特定帧交互时 激活所有包含的帧(且仅激活这些帧),无论其 来源。(在 Chrome 72 中,我们提供一种临时的解决方法来扩展 所有同源帧的可见性。一旦发现问题, 可以 将用户激活操作显式传递给子框架。)
  • 当从已激活的帧调用受激活控制的 API 时, 事件处理脚本代码之外,那么只要用户激活了代码, 状态为“活动”(例如,既未过期,也未消耗)。在用户之前 激活 v2 将无条件失败。
  • 过期时间间隔内出现多次未使用的用户互动会融合 转化成与最终互动相对应的单次激活。

受激活控制的 API 中的一致性示例

下面是两个带有弹出式窗口(使用 window.open() 打开)的示例, 展示用户激活 v2 如何使受激活控制的 API 的行为 保持一致。

链式 setTimeout() 调用

此示例来自 我们的 setTimeout() 演示。 如果 click 处理程序尝试在一秒内打开弹出式窗口,则应该 无论代码以何种方式“编写”这些代码,延迟时间用户激活 v2 满足 因此以下每一个事件处理脚本都会在 click(延迟 100 毫秒):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

如果没有 User Activation v2,第二个事件处理脚本在我们所有的浏览器 测试。(即使是第一个 在某些情况下。)

跨网域 postMessage() 调用

下面是一个来自 我们的 postMessage() 演示。 假设跨源子框架中的 click 处理程序直接发送两条消息 添加到父框架中。父框架应该能够在 接收以下任一消息(但不是同时收到):

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

如果没有 User Activation v2,父框架在接收到弹出式窗口时就无法打开弹出式窗口 第二封邮件。即使第一条消息被“链接”也会失败到另一个 跨域帧(也就是说,如果第一个接收者转发了 转换为另一个)。

这适用于 User Activation v2,无论是原始形式还是 链接。