# iOS 上主屏 Web 应用的存储隔离

> WebKit 对「主屏 Web 应用的网站数据与 Safari 相互隔离」的记载、随之而来的跟踪防护豁免，以及该如何为这种隔离写代码。

被配置成主屏 Web 应用的站点——清单与元数据方面的要求见下文「平台范围」一节——会作为它自己的应用打开，而不是在浏览器里打开，而 WebKit 记载这类应用的网站数据与 Safari 相互隔离。因此，先在 Safari 里登录、再点开该图标的访客，进入的是同一个源，但容器是另一个，那次浏览器会话并没有往里写过东西。在 iOS 26 之前，添加一个不带这项配置的站点，得到的是主屏书签：自 iOS 与 iPadOS 16.4 起它会在默认浏览器中打开，因而不属于本页所描述的情况。在 iOS 26 与 iPadOS 26 上，无论是否配置，每个被添加的站点默认都以 Web 应用打开，除非用户在添加时关闭「Open as Web App」。

## 工作原理

两份 WebKit 文档给出了这条规则及其理由，第三份则说明一个保存下来的站点要满足什么条件才会表现为应用。

### WebKit 对这种隔离的记载

跟踪防护页面有一节的标题是「Home Screen Web Application Domain Exempt From ITP」。它写道「the first-party domain of home screen web applications is exempt from ITP's 7-day cap on all script-writeable storage, i.e. ITP always skips that domain in its website data removal algorithm」，并补充「the website data of home screen web applications is kept isolated from Safari and thus will not be affected by ITP's classification of tracking behavior in Safari」（[Tracking Prevention in WebKit](https://webkit.org/tracking-prevention/)，webkit.org）。

这种分隔是刻意设计，而不是副作用。在 WebKit 的 181849 号缺陷「"Add to homescreen" apps don't share storage with Safari」下，Brent Fulgham 于 2022-02-01 回复：「The current behavior (on Apple platforms) is by design. Home Screen apps are created as isolated entities without shared state with the browser」（[Bug 181849](https://bugs.webkit.org/show_bug.cgi?id=181849)，bugs.webkit.org）。

于是这种隔离一次给了已安装应用两样东西：不受 7 天上限影响的脚本可写存储，以及一个处在 Safari 自身分类结果触及范围之外的容器。它的代价，是任何关于「与访客最初那次浏览器会话保持连续」的假设。

### 平台范围

这条规则适用于主屏 Web 应用，而 WebKit 是按清单文件来界定它的：随站点一起提供一份 `display` 成员设为 `standalone` 或 `fullscreen` 的清单文件，就使该站点成为主屏 Web 应用，此后点击其图标会像 iOS 或 iPadOS 上任何其他应用那样打开这个 Web 应用，而不是在浏览器里打开。它的应用预览出现在 App 切换器中，与 Safari 或任何其他浏览器分开（[Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)，webkit.org）。

没有这项配置的保存站点是另一种东西。WebKit 写道，如果既没有配置成请求 Web 应用行为的清单文件，也没有把站点标记为 web app capable 的 meta 标签，该网站就被保存为主屏书签；并且自 iOS 与 iPadOS 16.4 起，主屏书签会在用户当下的默认浏览器中打开。同一篇文章还记载，iOS 从最初就支持同一个 Web 应用的多次安装，而 iOS 与 iPadOS 16.4 会把用户添加应用时所取的名称与 Manifest ID 结合起来唯一标识它。

iOS 26 与 iPadOS 26 把这个决定从站点手里拿走了。WebKit 的 Safari 26.0 发布说明写道「by default, every website added to the Home Screen opens as a web app」，想要浏览器书签的用户可以在添加到主屏时关闭「Open as Web App」，「even if the site is configured to be a web app」。同一份说明还写明，这项改动没有移除 WebKit 对 Web 应用特性的任何支持（清单中声明的图标仍会被使用），并且「there are now zero requirements for "installability" in Safari」（[WebKit Features in Safari 26.0](https://webkit.org/blog/17333/webkit-features-in-safari-26-0/)，webkit.org）。对本页而言，后果是默认值变了：在这些版本上，一个既没有清单也没有 meta 标签的站点，只要用户没有关掉这个开关，同样以 Web 应用启动。因此，上文按清单与 meta 标签作出的判断适用于 26 之前的版本，而在 26 上由用户的开关决定。[按版本看 iOS 上的 PWA](/zh/reference/platforms/ios-by-version/) 把这项变化放在了其他版本之间。

### 这对用户的代价

引出 Apple 那条答复的缺陷报告，用提交者自己的说法描述了后果：一个「can save entire user projects in to IndexedDB」的 PWA，让使用该 Web 应用的用户面临丢失成果的风险，而且「it's also inconvenient since details like saved logins are not remembered between the two」。同一个讨论串也记下了开发者当时采用的绕法：2018-02-27，Thomas Steiner 描述了用 `window.location.query` 把会话数据从 Safari 带到 Web 应用里，并称其为「the only working hack (that I am aware of)」。

:::caution
把一次已安装启动当作存储层面的冷启动来对待。在 iOS 与 iPadOS 上，主屏 Web 应用的网站数据与 Safari 相互隔离（[Tracking Prevention in WebKit](https://webkit.org/tracking-prevention/)，webkit.org），所以一个只把令牌写在 Safari 会话里的登录流程，在已安装应用中起步时并没有那个令牌。给这个应用一条自己取得令牌的途径。
:::

## 示例

第一个示例从脚本里分辨出这两种载体；第二个报告当前容器里实际有什么，这正是在一次技术支持过程中让差异可见的办法。

### 检测页面正以已安装应用的形态运行

`display-mode` 媒体特性用于判断一个 Web 应用是显示在普通浏览器标签页里，还是以别的方式显示，比如独立应用或全屏模式；在清单中设置了 `display` 的 PWA 会看到该值被反映出来，不过 MDN 指出，当浏览器不支持所请求的模式时，报告出来的值可能与请求的并不相同（[display-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/display-mode)，developer.mozilla.org）。同一页面还补充「any web app can use the Fullscreen API or the Document Picture-in-Picture API to set the display mode, and in this case the value of `display-mode` identifies the mode that was set」，并把 `fullscreen` 关键字记载为适用于「when the app has been put in fullscreen mode by the Fullscreen API or by using the `fullscreen` value of the `display` member of the Wep App Manifest」。因此一个调用过 `Element.requestFullscreen()` 的普通浏览器标签页同样会匹配 `(display-mode: fullscreen)`，单凭这个值并不能认定页面就是那个被隔离的主屏容器。真正专属于这个平台的信号来自 Apple 自己的文档：「You can determine whether a webpage is displaying in standalone mode using the `window.navigator.standalone` read-only Boolean JavaScript property」（[Configuring Web Applications](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html)，developer.apple.com）。

```js
function launchSurface() {
  // Apple 将它记载为一个布尔值，用于报告页面是否处于 standalone 模式。
  if (navigator.standalone === true) return 'installed';
  if (!('matchMedia' in window)) {
    return 'unknown'; // 没有媒体查询接口：按浏览器标签页来对待这次会话。
  }
  if (window.matchMedia('(display-mode: standalone)').matches) return 'installed';
  // Fullscreen API 也会匹配这一条，所以它不能充当已安装信号。
  if (window.matchMedia('(display-mode: fullscreen)').matches) return 'ambiguous';
  return 'browser';
}
```

`unknown` 这条分支返回一个调用方可以据以路由的值，而不是抛出错误，于是在无法查询该特性的地方，引导流程仍走浏览器那条路。只有 `installed` 才是提示应当提供应用内登录、而不是假定会话已经带过来了；`ambiguous` 同样该走浏览器那条路，除非已用别的办法确认过安装，因为被页面自己推入全屏的标签页也会落到那条分支上。

### 报告当前容器里有什么

在 Safari 里和在已安装应用里跑同一个函数，会得到两份可供技术支持人员比对的读数。`CacheStorage.keys()` 以创建顺序兑现该容器所跟踪的每个 `Cache` 的名称（[CacheStorage: keys() method](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/keys)，developer.mozilla.org），而 `window.localStorage` 提供对该文档源的 `Storage` 对象的访问，其中存放的数据跨浏览器会话保存（[Window: localStorage property](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)，developer.mozilla.org）。

```js
async function storageFingerprint() {
  const cacheNames = 'caches' in window ? await caches.keys() : [];
  let localKeys = [];
  try {
    localKeys = Object.keys(localStorage);
  } catch {
    localKeys = []; // 该上下文无法使用存储：报告一个空集合。
  }
  return { surface: launchSurface(), cacheNames, localKeys };
}
```

比较两个容器返回的 `surface`、`cacheNames` 与 `localKeys` 值。仅凭键列表不同，既不能确定差异由隔离造成，也不能排除应用写入失败。

:::observed
181849 号缺陷的复现步骤把这一行为记成了屏幕上的确切字符串。提交者 Ashley Gullen 于 2018-01-19 在 Safari 中于测试页输入 `123` 并按下 Save，把该页添加到主屏，打开主屏应用并按下 Load：「Expected result: text should change to `Loaded: 123`」，「Observed result: text changed to `Loaded: null`」。接着在主屏应用中输入 `456` 并按下 Save，再回到 Safari 中的同一页面按下 Load，得到的是 `Loaded: 123`，而预期为 `Loaded: 456`（[Bug 181849](https://bugs.webkit.org/show_bug.cgi?id=181849)，bugs.webkit.org）。该缺陷页面把这份报告的状态列为 `NEW`，解决结果一栏为空。
:::

## 另请参阅

- [Tracking Prevention in WebKit](https://webkit.org/tracking-prevention/)（webkit.org）
- [Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)（webkit.org）
- [Bug 181849: "Add to homescreen" apps don't share storage with Safari](https://bugs.webkit.org/show_bug.cgi?id=181849)（bugs.webkit.org）
- [WebKit Features in Safari 26.0](https://webkit.org/blog/17333/webkit-features-in-safari-26-0/)（webkit.org）
- [按版本看 iOS 上的 PWA（iOS 11.3 到 iOS 26）](/zh/reference/platforms/ios-by-version/)
- [localStorage 与 sessionStorage](/zh/reference/storage/localstorage/)
- [CacheStorage 与 caches 全局对象](/zh/reference/storage/cache-storage/)
- [淘汰与 best-effort 存储](/zh/reference/storage/eviction/)