存储中的平台
iOS 上主屏 Web 应用的存储隔离
发布于 更新于
被配置成主屏 Web 应用的站点——清单与元数据方面的要求见下文「平台范围」一节——会作为它自己的应用打开,而不是在浏览器里打开,而 WebKit 记载这类应用的网站数据与 Safari 相互隔离。因此,先在 Safari 里登录、再点开该图标的访客,进入的是同一个源,但容器是另一个,那次浏览器会话并没有往里写过东西。在 iOS 26 之前,添加一个不带这项配置的站点,得到的是主屏书签:自 iOS 与 iPadOS 16.4 起它会在默认浏览器中打开,因而不属于本页所描述的情况。在 iOS 26 与 iPadOS 26 上,无论是否配置,每个被添加的站点默认都以 Web 应用打开,除非用户在添加时关闭「Open as Web App」。
两份 WebKit 文档给出了这条规则及其理由,第三份则说明一个保存下来的站点要满足什么条件才会表现为应用。
WebKit 对这种隔离的记载
Section titled “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,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,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,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,webkit.org)。对本页而言,后果是默认值变了:在这些版本上,一个既没有清单也没有 meta 标签的站点,只要用户没有关掉这个开关,同样以 Web 应用启动。因此,上文按清单与 meta 标签作出的判断适用于 26 之前的版本,而在 26 上由用户的开关决定。按版本看 iOS 上的 PWA 把这项变化放在了其他版本之间。
这对用户的代价
Section titled “这对用户的代价”引出 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)」。
第一个示例从脚本里分辨出这两种载体;第二个报告当前容器里实际有什么,这正是在一次技术支持过程中让差异可见的办法。
检测页面正以已安装应用的形态运行
Section titled “检测页面正以已安装应用的形态运行”display-mode 媒体特性用于判断一个 Web 应用是显示在普通浏览器标签页里,还是以别的方式显示,比如独立应用或全屏模式;在清单中设置了 display 的 PWA 会看到该值被反映出来,不过 MDN 指出,当浏览器不支持所请求的模式时,报告出来的值可能与请求的并不相同(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,developer.apple.com)。
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 同样该走浏览器那条路,除非已用别的办法确认过安装,因为被页面自己推入全屏的标签页也会落到那条分支上。
报告当前容器里有什么
Section titled “报告当前容器里有什么”在 Safari 里和在已安装应用里跑同一个函数,会得到两份可供技术支持人员比对的读数。CacheStorage.keys() 以创建顺序兑现该容器所跟踪的每个 Cache 的名称(CacheStorage: keys() method,developer.mozilla.org),而 window.localStorage 提供对该文档源的 Storage 对象的访问,其中存放的数据跨浏览器会话保存(Window: localStorage property,developer.mozilla.org)。
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 值。仅凭键列表不同,既不能确定差异由隔离造成,也不能排除应用写入失败。
- Tracking Prevention in WebKit(webkit.org)
- Web Push for Web Apps on iOS and iPadOS(webkit.org)
- Bug 181849: “Add to homescreen” apps don’t share storage with Safari(bugs.webkit.org)
- WebKit Features in Safari 26.0(webkit.org)
- 按版本看 iOS 上的 PWA(iOS 11.3 到 iOS 26)
- localStorage 与 sessionStorage
- CacheStorage 与 caches 全局对象
- 淘汰与 best-effort 存储
规范
| 规范 | 状态 |
|---|---|
| 无。 | |