跳转到内容

iOS 主屏 App 通知实测笔记

发布于

iOS 主屏 Web App 的通知流程涉及用户授权、Push 订阅入口、Service Worker 和通知事件。需要复现 iOS 18.6.2 的行为、排查权限或点击路径,或与后续系统 build 对照时,可以按这些步骤留下可核验记录;单台设备结果不能替代通用兼容性结论。

原始实验在 2026-10-20 使用一台 iPhone 16 Pro,系统为 iOS 18.6.2(22G100)。准备一个 HTTPS App:manifest 设为 display: standalone,注册 Service Worker,提供权限按钮、IndexedDB 事件日志和创建本地通知的入口。开始前先把 App 添加到主屏幕。WebKit 记载,iOS 与 iPadOS 16.4 起为主屏 Web App 提供 Web Push,并要求由直接用户互动触发通知权限请求(Web Push for Web Apps on iOS and iPadOS,webkit.org)。这次实验没有发送真实的远程 Push。测量范围是权限、订阅对象、本地显示的通知、通知点按和 Worker 状态。真机研究与公开数据包保存了完整记录。

WebKit 记载 iOS 与 iPadOS 16.4 起为主屏 Web App 提供 Web Push。下文行为只对应一台 iOS 18.6.2 设备上安装后的主屏 App,不能用于判断 Safari 标签页、其他系统版本或远程 Push 投递;这次实验没有测试远程投递。

从主屏图标打开 App,检查流程会用到的接口;如果处在 Safari 标签页或接口缺失,则进入明确的回退分支:

function notificationSurface() {
const installed = matchMedia('(display-mode: standalone)').matches
|| navigator.standalone === true;
const supported = 'Notification' in window
&& 'serviceWorker' in navigator
&& 'PushManager' in window;
if (!installed || !supported) {
return { enabled: false, action: 'Open the Home Screen app' };
}
return { enabled: true, action: 'Show permission control' };
}

回退分支属于功能流程。原始 build 的 Safari 标签页没有 Notification 和 PushManager 构造器,安装后的 App 则有。iOS 18.4 还加入 Declarative Web Push 的 window.pushManager;实验在两个上下文都读到这个属性,所以只检查它不能判断页面是否处于主屏 App。

使用从未请求过权限的 App。把 Notification.requestPermission() 直接放进按钮处理器,调用前记录 performance.now(),Promise 结束后记录结果。不要在页面加载时触发。WebKit 文档明确要求直接用户互动。

permissionButton.addEventListener('click', async () => {
const started = performance.now();
permissionButton.disabled = true;
const permission = await Notification.requestPermission();
const elapsed = Math.round(performance.now() - started);
eventLog.textContent = JSON.stringify({ permission, elapsed });
if (permission !== 'granted') {
permissionButton.disabled = false;
fallbackPanel.hidden = false;
return;
}
subscribeButton.hidden = false;
});

归档运行中,操作者点按钮后出现系统提示,Promise 一直等待用户作答。其中一次授权记录在 2068 ms 后结束。这个数值只描述当次互动,不能用作应用超时值(通知权限提示)。

结果为 denied 或 default 时,保留 App 内回退界面,不要立刻再次请求。如果测试中 Promise 一直不结束,先保存录屏、请求前权限状态和系统 build,再改代码。

iOS 与 iPadOS 18.4 起,WebKit 通过 window.pushManager 暴露 Declarative Web Push。WebKit 说明,作用域位于站点根路径的 Service Worker 会与 window 对象共享订阅;18.6.2 实验的 Worker 位于子路径,因此不符合共享条件(Meet Declarative Web Push,webkit.org)。

权限获准后,只读取两个入口,不要新建第二份订阅:

const registration = await navigator.serviceWorker.ready;
const workerSubscription = await registration.pushManager.getSubscription();
const windowSubscription = window.pushManager
? await window.pushManager.getSubscription()
: null;
console.table({
worker: workerSubscription?.endpoint ? 'present' : 'missing',
window: windowSubscription?.endpoint ? 'present' : 'missing',
permission: Notification.permission,
});

原实验主屏 App 的 Service Worker 订阅端点主机为 web.push.apple.com,window.pushManager.getSubscription() 返回 null。一次授权令两个 Manager 的权限状态检查都返回 granted。测试 Worker 使用演示应用或设备实验室的非根作用域,不是 /,因此结果符合 WebKit 的根作用域条件(见订阅作用域记录)。实验没有测试根作用域 Worker,也没有投递远程 Push。

用 addEventListener 注册事件,同时给 self.onnotificationclick 赋值,把它们作为两个独立探针。聚焦客户端前,先把每次调用写入 IndexedDB,方便页面再次启动后显示结果。

sw.js
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(recordClick('listener').then(() => focusClient()));
});
self.onnotificationclick = (event) => {
event.notification.close();
event.waitUntil(recordClick('property').then(() => focusClient()));
};
async function focusClient() {
const clients = await self.clients.matchAll({ type: 'window' });
return clients[0]?.focus();
}

分别点横幅和通知中心中的通知,然后在重新启动 App、从多任务界面划走 App 后重复。研究记录的六次点按中,两个处理器每次都运行;clients.matchAll() 找到已打开窗口,focus() 成功。clients.openWindow() 分支没有执行(主屏 App 的 notificationclick)。

真机结果与 BCD 描述的上下文不同。BCD 把 Safari on iOS 的 api.ServiceWorkerGlobalScope.notificationclick_event 记为不支持。Issue #22959 说明采集器会在 Service Worker 中检查处理器属性;18.6.2 实验发现,同一个 Worker 由 Safari 标签页控制时,'onnotificationclick' in self 为 false,由主屏 App 控制时为 true(Worker 处理器暴露记录;BCD issue #22959,github.com)。

若点按没有进入处理器,先确认通知属于已安装 App、包含处理器的 Worker 已激活,并验证页面重新启动后仍能读取事件日志。还要记下当时是否已有 App 窗口。WebKit 的错误报告包含“没有页面存活时不触发”的情况;本次运行找到了现有客户端,没有覆盖 openWindow()。

日志缺失也可能来自写入失败,不能直接证明事件没有触发。原研究运行期间曾发现观察器错误,并以新笔记保留修正过程。失败记录应包括通知来源、处理器注册方式、App 生命周期状态、Worker 脚本 URL、系统 build 与 IndexedDB 原始日志。