# iOS 滑走后的页面存活与存储

> 复现 iOS 18.6.2 滑走主屏幕 Web App 后的页面存活检查，并区分 Safari 与已安装 App 的独立存储。

从 App 切换器滑走 iOS 主屏幕 Web App，不能单独证明文档已经结束，也不能说明 App 重新使用 Safari 的存储。iPhone 16 Pro 的一次 iOS 18.6.2 测试中，滑走后约 6 秒点击通知，原文档仍然存活；另一组测试发现 localStorage 和 IndexedDB 各自隔离，而 cookie 会在添加 App 时复制一次。

通知点击意外聚焦旧页面，或新安装的 App 看不到 Safari 写入的数据时，可以按下面的步骤检查。准备已安装的 HTTPS Web App、Web Push 权限、带有 `notificationclick` 处理器的 Service Worker 和 Mac Safari Web Inspector。时间和值来自 2026-10-20 的 iPhone 16 Pro、iOS 18.6.2 (22G100) 归档测试（[研究数据包](https://openpwa.net/research/2026-10-ios18-home-screen-web-apps/data/)，openpwa.net）。

## 支持范围（Support）

实测的生命周期和存储结果适用于 iPhone 16 Pro 上的 Safari 和 iOS 18.6.2 主屏幕 Web App。Service Worker 规范把 `clients.matchAll()` 定义为返回现有 client，把 `clients.openWindow()` 定义为打开新的顶层浏览上下文（[Service Workers，`matchAll()`](https://w3c.github.io/ServiceWorker/#clients-matchall)，w3.org；[Service Workers，`openWindow()`](https://w3c.github.io/ServiceWorker/#clients-openwindow)，w3.org）。规范没有规定 iOS 主屏幕 App 被滑走后能存活多久。

WebKit 说明主屏幕 Web App 的网站数据与 Safari 隔离。Safari 17.2 的发布说明补充了复制规则：用户添加 Web App 时复制 cookie，不复制其他本地存储，添加完成后不再共享网站数据（[Tracking Prevention in WebKit](https://webkit.org/tracking-prevention/)，webkit.org；[WebKit Features in Safari 17.2](https://webkit.org/blog/14787/webkit-features-in-safari-17-2/)，webkit.org）。18.6.2 设备测试用一个第一方 cookie、localStorage 和 IndexedDB 值复现了这个模型。

生命周期证据只有一次滑走和约 6 秒后的通知点击，没有覆盖更长等待、内存压力、低电量模式、重启、iPadOS 或其他 iPhone。存储测试没有覆盖 CacheStorage、OPFS、`HttpOnly` cookie、会话 cookie 和分区 cookie。

## 示例

分别记录文档身份、通知处理和存储结果，不要让其中一项代替另一项证据。

### 检测通知测试路径

安排推送前，先检查已安装显示环境和通知测试需要的 API。如果页面位于 Safari，或缺少任一 API，就跳过推送路径，改用后面的文档身份与存储检查。

```js
const standalone = matchMedia('(display-mode: standalone)').matches
  || navigator.standalone === true;
const notificationPath = 'serviceWorker' in navigator
  && 'Notification' in window
  && 'PushManager' in window;

if (!standalone || !notificationPath) {
  console.warn('Push path unavailable; run the identity and storage checks instead.');
} else {
  console.log('Installed context and notification APIs detected.');
}
```

回退流程仍可在手动重启前后比较文档身份，并确认当前使用的存储环境，但它不能复现归档测试中的通知点击路径。

### 为每个文档记录身份

离开 App 前保存 `performance.timeOrigin`。它是当前文档性能时间坐标的起点；通知把 App 带回前台后比较两次读数，但要结合导航类型和 history length 判断，不要要求精确相等。归档的 N38 和 N40 记录包含这三项信号，没有采集内存 token。

```js
const identity = {
  timeOrigin: performance.timeOrigin,
  navigationType: performance.getEntriesByType('navigation')[0]?.type,
  historyLength: history.length
};

localStorage.setItem('document-identity', JSON.stringify(identity));
console.log(identity);
```

N38 重新启动记录的 `timeOrigin` 是 `08:28:29.978Z`，导航类型为 `navigate`，`history.length` 为 1。后来 N40 滑走 App 并点击通知，读数是 `08:28:30.036Z`；实验记录说明两次相差 58 ms，属于多次读取 `performance.timeOrigin` 时的舍入差异，而且两次 `history.length` 都是 1，因此可判断为同一文档（[iOS 18 滑走后的页面生命周期](/zh/lab/behaviour/page-survives-swipe-away/)）。

再用内存值补充判断，因为新文档也能读到旧文档留下的 localStorage。下面的回退分支会在内存 token 不存在时把它当作新文档，并创建 token。

```js
if (globalThis.__documentToken) {
  console.log({ outcome: 'same-js-realm', token: globalThis.__documentToken });
} else {
  globalThis.__documentToken = crypto.randomUUID();
  console.log({ outcome: 'new-js-realm', token: globalThis.__documentToken });
}
```

滑走前后各运行一次。相同 token 支持 JavaScript realm 延续；新 token 表示这项信号没有保留。归档判断应保留两次 time-origin 读数作为辅助证据，并允许归档测试记录的 58 ms 舍入差异。不要依赖进程 ID，因为测试中的同一 Web App 进程承载了多个页面。

### 处理通知点击时保留两个分支

先查找 window client。找到同源 client 就聚焦；没有匹配项时才调用 `openWindow()`。两个分支都写日志，测试结果才能区分它们。

```js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  event.waitUntil((async () => {
    const windows = await clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    });

    const existing = windows.find((client) =>
      new URL(client.url).origin === self.location.origin
    );

    if (existing) {
      try {
        await existing.focus();
        console.log({ windowsFound: windows.length, outcome: 'focused existing window' });
      } catch (error) {
        console.error({ windowsFound: windows.length, outcome: 'focus failed', error });
        throw error;
      }
    } else {
      try {
        const opened = await clients.openWindow('/');
        if (opened) {
          console.log({ windowsFound: 0, outcome: 'opened new window' });
        } else {
          console.error({ windowsFound: 0, outcome: 'openWindow returned null' });
        }
      } catch (error) {
        console.error({ windowsFound: 0, outcome: 'openWindow failed', error });
        throw error;
      }
    }
  })());
});
```

归档测试在 08:43:03.022 UTC 创建通知，并在 08:43:09 点击。处理器于 08:43:09.236Z 记录 `{ windowsFound: 1, outcome: "focused existing window" }`，页面属性处理器又在 08:43:09.245Z 记录点击。这个时间间隔内，滑走卡片没有进入无 client 分支。

:::caution
不要用快速“滑走再点通知”的步骤证明无页面时 `clients.openWindow()` 可以工作。iOS 18.6.2 测试在约 6 秒后仍找到一个 window client，所以无法执行该分支。WebKit bug 268797 报告称，在 iOS 16.4–17.3 的主屏幕 Web App 中，点击通知时没有观察到 `notificationclick` 处理器运行。报告没有说明或证实点击通知时 App 页面已经不存活（[Bug 268797](https://bugs.webkit.org/show_bug.cgi?id=268797)，bugs.webkit.org；[滑走后的页面生命周期](/zh/lab/behaviour/page-survives-swipe-away/)）。
:::

### 给每个存储环境写标记

添加 App 之前，先在 Safari 写入标记。localStorage、IndexedDB 和 cookie 使用各自的键；首次启动 App 后，就能看出哪些值进入了新环境。下面的控制台示例覆盖 localStorage 和 cookie，IndexedDB 记录可用实验 harness 复现。

```js
const context = matchMedia('(display-mode: standalone)').matches ? 'APP' : 'TAB';
const marker = `${context}-${new Date().toISOString()}`;

localStorage.setItem('storage-marker', marker);
document.cookie = `storage_marker=${encodeURIComponent(marker)}; Path=/; Max-Age=31536000; SameSite=Lax; Secure`;

console.log({
  localStorage: localStorage.getItem('storage-marker'),
  cookie: document.cookie
});
```

归档流程先让 Safari 写入 `TAB-2026-10-20T07:13:46Z`，随后添加 App。PWA Lab 首次读取时，localStorage 和 IndexedDB 都是 `null`，cookie 则含有 Safari 的标记。App 写入 `APP-2026-10-20T07:37:40Z` 后，Safari 仍返回先前的 `TAB-...` 值。同一站点的第二个 App 也使用独立存储（[iOS 主屏幕 App 中的 Safari 存储与 cookie](/zh/lab/behaviour/storage-isolation-and-cookie-copy/)）。

解释缺失标记之前，先检查显示环境。`matchMedia('(display-mode: standalone)').matches` 为 false，且 `navigator.standalone` 不为 true 时，当前读取来自 Safari，不是已安装 App。App 看不到 localStorage 或 IndexedDB 标记时，应初始化自己的状态或让用户登录，不要把 Safari 的本地数据当作安装迁移。cookie 可以承载 WebKit 文档描述的已测试登录状态复制机制，但后续写入仍保持分离。

### 运行滑走测试

先记录文档身份，再安排 5 秒后的通知，回到主屏幕，打开 App 切换器并滑走 App。通知出现后点击它，再次采集处理器日志和文档身份。归档的 18.6.2 测试找到一个 window；N38 与 N40 的 time-origin 读数相差 58 ms，在研究记录的舍入范围内，两份记录的导航类型都是 `navigate`，`history.length` 都是 1。归档测试没有记录内存 token；读者复现时可运行上面的 token 检查，把它作为额外信号。不要要求归档的两次 time-origin 读数精确相等。

如需测试无页面状态，先确认 Web Inspector 已不再列出页面，再从不要求页面存活的来源触发通知。记录等待时间，以及是否发生重启或内存压力。如果处理器没有运行，应保留通知、控制台、Service Worker 和系统 build 证据。只有日志显示代码进入 `openWindow()` 分支后，才能把失败归到该调用。

存储测试与文档生命周期测试应分开记录。存活文档会保留当前 JavaScript 状态；重新加载的文档仍能打开已安装 App 自己的持久化存储。两种结果都不能证明 Safari 与 App 共用实时存储。

## 另请参阅

- [Service Workers，`clients.matchAll()`](https://w3c.github.io/ServiceWorker/#clients-matchall) (w3.org)
- [WebKit Features in Safari 17.2](https://webkit.org/blog/14787/webkit-features-in-safari-17-2/) (webkit.org)
- [iOS 18 滑走后的页面生命周期](/zh/lab/behaviour/page-survives-swipe-away/)
- [iOS 主屏幕 App 中的 Safari 存储与 cookie](/zh/lab/behaviour/storage-isolation-and-cookie-copy/)
- [iOS 主屏幕 Web App 的存储隔离](/zh/reference/storage/home-screen-storage-isolation/)