能力中的概念
用户激活与它门控的那些 API
发布于
用户激活是 HTML 标准用来把一类 API 限制为「only when the user is actively interacting with the web page or has interacted with the page at least once」的窗口状态,好让脚本无法在没有提示的情况下弹出窗口或让手机振动。标准定义了粘性激活、瞬时激活与 history-action 激活;瞬时激活会在用户代理定义的时长之后过期。
激活按 Window 跟踪,记录的是时间戳而不是一个布尔标志,这正是同一套机制既能表达「曾经交互过」又能表达「刚刚交互过」的原因。
HTML 标准里的数据模型
Section titled “HTML 标准里的数据模型”每个 Window 都带一个 last activation timestamp,它「either a DOMHighResTimeStamp, positive infinity (indicating that W has never been activated), or negative infinity (indicating that the activation has been consumed)」,初值为正无穷,另外还带一个 last history-action activation timestamp。用户代理还定义了 transient activation duration,即「a constant number indicating how long a user activation is available for certain user activation-gated APIs」,规范预期它最多只有几秒,「so that the user can possibly perceive the link between an interaction with the page and the page calling the activation-gated API」(HTML Standard: Tracking user activation,html.spec.whatwg.org)。
当前高精度时间大于或等于 last activation timestamp 时,粘性激活成立;规范把它描述为窗口的历史激活状态,该状态「starts false, then changes to true (and never changes back to false) when W gets the very first activation notification」。若该时间同时小于 last activation timestamp 加上 transient activation duration,则瞬时激活成立,而这一状态「considered expired if it becomes false because the transient activation duration time has elapsed since the last user activation」。它也可能在到期之前就因为一次激活消耗而变为 false。
哪些事件会授予激活
Section titled “哪些事件会授予激活”activation triggering input event 指 isTrusted 为 true 且类型为下列之一的事件:keydown,前提是该键既不是 Esc 键也不是用户代理保留的快捷键;mousedown;pointerdown,前提是事件的 pointerType 为 mouse;pointerup,前提是事件的 pointerType 不为 mouse;或 touchend。MDN 的清单补充说,被排除的按键「may vary by browser」,并点出 Caps Lock、Num Lock 与 Print Screen 在某些键盘上不产生用户激活(User activation,developer.mozilla.org)。
通知与消耗覆盖的浏览上下文集合并不相同,规范对此是有意为之:「an activation consumption changes (to false) the transient activation states for all browsing contexts in the page, but an activation notification changes (to true) the states for a subset of those browsing contexts」,因为这种穷尽式的消耗「prevents malicious sites from making multiple calls to an activation consuming API from a single user activation (possibly by exploiting a deep hierarchy of iframes)」。
四个门控级别
Section titled “四个门控级别”规范把依赖激活的 API 分成四个级别。粘性激活门控的 API 需要粘性激活,「so they are blocked until the very first user activation」。瞬时激活门控的 API 需要瞬时激活但不消耗它,「so multiple calls are allowed per user activation until the transient state expires」。瞬时激活消耗型 API 需要瞬时激活,并在每次调用时消耗它。history-action 激活消耗型 API 需要 history-action 激活状态并在每次调用时消耗它,而且不同于瞬时激活,「there is no time limit within which such APIs must be used」。
UserActivation 接口的浏览器支持
Section titled “UserActivation 接口的浏览器支持”BCD api.UserActivation 记录的支持起点为 Chrome 72、Firefox 120 与 Safari 16.4,并镜像到该记录列出的 Chromium 系浏览器(Edge、Opera、Samsung Internet 与 WebView),以及 Chrome Android、Firefox Android 与 iOS 上的 Safari(api/UserActivation.json,github.com)。hasBeenActive 与 isActive 两个成员适用同一组版本。
哪些 API 需要哪种状态
Section titled “哪些 API 需要哪种状态”MDN 为它的两份清单都标注了并不完备。它列为需要瞬时激活的 API 包括 Navigator.share()、Window.open()、PaymentRequest.show()、Element.requestFullscreen()、MediaDevices.getDisplayMedia()、Document.requestStorageAccess()、Clipboard.read()、Clipboard.write()、USB.requestDevice()、Serial.requestPort()、HID.requestDevice()、Window.showOpenFilePicker()、Window.showSaveFilePicker()、Window.showDirectoryPicker()、EyeDropper.open()、Clients.openWindow() 与 WindowClient.focus()。它列为需要粘性激活的 API 包括 beforeunload 事件、Navigator.vibrate()、VirtualKeyboard.show()、Media 与 Web Audio API 的自动播放(尤其是 AudioContext),以及 clipboardchange 事件,MDN 指出后者也可以通过用户授予 clipboard-read 权限来启用。
这一划分在实践中决定的是调用何时能够运行。粘性门控的调用在访客首次与页面交互之后的任何时刻都成立,所以卸载时的确认对话框没有问题。瞬时门控的调用可在瞬时激活仍有效时运行,直到其持续时间结束,或某个消耗型 API 为页面中的全部浏览上下文消耗它。
第一个示例读取粘性与瞬时状态;第二个在瞬时激活仍有效时调用受门控的 API。
读取两种激活状态
Section titled “读取两种激活状态”navigator.userActivation 暴露 hasBeenActive,它「indicates whether the window has sticky user activation」,以及 isActive,它「indicates whether the window has transient user activation」(UserActivation,developer.mozilla.org)。由于该接口在各引擎到场的版本不同,这次读取需要一个检测分支。
function activationState() { if (!('userActivation' in navigator)) { // 接口不存在:改为依据触发的那个事件分支,而不是窗口状态。 return { hasBeenActive: null, isActive: null, readable: false }; } const { hasBeenActive, isActive } = navigator.userActivation; return { hasBeenActive, isActive, readable: true };}readable: false 这个结果意味着不要再问窗口:在预期激活仍有效时调用被门控的 API,并处理激活不在时它给出的 rejection。注意 click 本身并不在上面那份清单里——activation triggering input event 是 keydown、mousedown、带条件的 pointerdown/pointerup 这一对,以及 touchend。受信任的 click 通常紧随其中之一发生,这也是把调用放进它的处理器里算合理的原因;但「紧随其后」不等于「就是其中之一」,所以无论如何都得留着那条 rejection 路径。
在激活仍有效时调用消耗型 API
Section titled “在激活仍有效时调用消耗型 API”Web Share 属于瞬时激活消耗型 API,所以调用 share() 时必须仍有瞬时激活。异步步骤只有在调用前使激活过期,或让另一个 API 先消耗激活时,才会造成问题。
shareButton.addEventListener('click', async () => { if (!('share' in navigator)) { showCopyLinkDialog(); return; } const payload = { title: document.title, url: location.href }; try { await navigator.share(payload); } catch (error) { // 激活缺失或已被消耗时是 NotAllowedError;用户关掉分享面板也会 reject。 // 两者都不是需要往外抛的缺陷。 if (error.name === 'NotAllowedError') showCopyLinkDialog(); }});用手头已有的值构造 payload,可以在点击之后迅速调用 share(),而那个 catch 就是用来兜住「激活终究还是没了」的那条 rejection 路径。一次 await 结束时激活仍可能有效,但瞬时持续时间也可能在等待期间结束;另一个消耗型 API 也可能先消耗激活。
- HTML Standard: Tracking user activation(html.spec.whatwg.org)
- User activation(developer.mozilla.org)
- UserActivation(developer.mozilla.org)
- Web Share API
- Async Clipboard API
- Document.requestStorageAccess()
规范
| 规范 | 状态 |
|---|---|
| 无。 | |