安装 · 概念
N+1 安装问题
发布于
N+1 安装问题是 web 应用安装由浏览器而非操作系统执行所带来的结果:每一个安装过该站点的浏览器都生成自己的应用,拥有自己的存储与自己对已安装状态的记录,所以一台设备可以持有 N 个浏览器的副本,再加上用户下一个尝试的浏览器所装的那一份。Chromium 浏览器(Chrome 73、Edge 79)、iOS 16.4 与 macOS 14 上的 Safari、Windows 上的 Firefox 143 都以这种方式各自拥有安装(MDN “Installing and uninstalling web apps”、web.dev “Installation”)。
已安装的 web 应用运行在创建它的那个浏览器里。Chrome 的桌面应用窗口是一个读取 Chrome 配置文件存储的 Chrome 进程;Edge 的则是读取 Edge 存储的 Edge 进程;iOS 上 Safari 的主屏幕 web 应用使用与 Safari 标签页分离的独立数据存储。由此产生三个后果,MDN 与 web.dev 都有记载:
- 已安装状态只有安装它的浏览器知道。 Edge 知道站点已从 Edge 安装,再次访问时提供 “Open”;Chrome 访问同一站点时提供 “Install”,因为它没有 Edge 那份应用的记录。再从 Chrome 安装一次,设备上就有两个应用。
- 存储按安装隔离。 Cache Storage、IndexedDB、Cookie 与
localStorage都存放在安装方浏览器的配置文件中(iOS 上则在 web 应用自己的容器里),所以在 Edge 副本中登录的用户,在 Chrome 副本中是未登录状态,一方缓存的离线数据在另一方也不存在。 - 卸载按安装进行。 从
edge://apps移除 Edge 副本,chrome://apps中的 Chrome 副本仍在,反之亦然。
在同一浏览器内,行为取决于平台:
| 平台 | 同一浏览器再次安装同一站点 | 身份键 |
|---|---|---|
| Chrome 与 Edge,桌面端(Windows、macOS、Linux、ChromeOS) | 拒绝;安装入口消失,浏览器改为提供打开已有应用 | manifest id,缺省回退到 start_url |
| Chrome,Android(WebAPK) | 拒绝;每个浏览器每个 manifest id 只有一个 WebAPK |
manifest id |
| Chrome,Android(WebAPK 服务器不可达时的快捷方式回退) | 允许;每个快捷方式指向同一份浏览器存储 | 仅快捷方式 |
| Safari,iOS 与 iPadOS | 有意允许,用用户输入的名称区分副本;每份副本存储隔离 | manifest id 加添加时输入的名称 |
| Safari,macOS 14+ | 允许;再次 “Add to Dock” 会创建第二个 web 应用 | 添加时输入的名称 |
manifest 的 id 成员让浏览器在 start_url 变化后仍能认出已安装过的站点;没有它,身份就是 start_url,改动该 URL 会让 Chromium 把一次更新当成一个新应用。在 iOS 上,同一 id 刻意不作为去重键:WebKit 把多份安装(工作账号与个人账号)记载为一项特性,并用 id 与用户为应用起的名字一起,让每份副本的专注模式与通知设置相互独立。
页面无法查询其他浏览器的安装。getInstalledRelatedApps() 只在 manifest 把该 PWA 列入 related_applications、且在同一浏览器中时才报告它已安装;display-mode: standalone 只说明当前这一份正以安装态运行。因此站点要做的选择是:让重复安装变得廉价(无状态页面、登录后在任何副本上恢复数据),还是用明确的文案引导用户只用一个浏览器,并接受这种引导本身无法强制执行。
以下行为可以在装有两个 Chromium 浏览器的同一台机器和一部 iPhone 上复现。
在 iOS 上不改名称重新添加站点同样会生成第二个图标,系统不会合并它们。相反,在 Chrome 桌面端访问已安装的站点时,地址栏不再显示安装图标,三点菜单项从 “Install [应用名]…” 变为 “Open in [应用名]”。
- Installing and uninstalling web apps(developer.mozilla.org)
- Web Push for Web Apps on iOS and iPadOS(webkit.org)
- Web Application Manifest: id member(w3.org)
idmanifest 成员- getInstalledRelatedApps()
- 卸载已安装的 PWA
- iOS 添加到主屏幕
规范
| 规范 | 状态 |
|---|---|
| 无。 | |