# 已安装的 PWA 如何获取清单更新

> Chrome 最多每天重新抓取一次已安装 PWA 的清单，把发生变化的清单排队到所有应用窗口关闭后再应用，桌面端与 Android 应用的字段集合并不相同。

已安装的 PWA 保留一份安装时所用清单的本地副本。Chrome 按计划重新抓取线上清单，与这份副本比较，
并且只在应用的所有窗口都关闭之后才应用变化。哪些字段触发重新检查、哪些字段随后被应用，桌面端
Chrome 与 Android 上的 Chrome 并不相同，而且桌面端路径完全无法更新图标。

这里记录的是 Chrome 的行为，由 Chrome 团队在 web.dev 的
[How Chrome handles updates to the web app manifest](https://web.dev/articles/manifest-updates) 一文中描述；Edge 154 作为 Chromium 浏览器共用桌面端路径。Apple 没有为 Safari 27 的
主屏幕应用或 Dock 应用发布对应说明，Firefox 157 不从清单安装 Web 应用，所以本条目不对二者做
任何断言。

## 工作原理

已安装应用的每次启动，以及在浏览器标签页里对站点的每次访问，都会让 Chrome 检查上一次比较清单的
时间。如果那是在浏览器上次启动之前，或者已经超过 24 小时，Chrome 就重新抓取清单。抓回的值与
本地相同时，计时器重置，不发生别的事。

在桌面端，`name`、`short_name`、`display`、`scope`、`shortcuts`、`start_url`、`theme_color`、
`file_handlers` 中任何一项发生变化都会把新清单排入队列；应用的每个窗口都关闭后安装新清单，然后
除 `icons` 之外的所有字段都被应用，桌面端 Chrome 不更新图标。此外还有两条规则：`start_url` 的
变化只在清单声明了 `id` 时才被应用，因为没有 `id` 时旧的 `start_url` 就是应用的身份；`display`
从 `browser` 改为 `standalone` 不会改变那些选择在标签页中打开应用的用户，用户的窗口偏好优先于
清单。

在 Android 上同样每 24 小时检查一次，但触发列表是 `name`、`short_name`、`icons`、
`background_color`、`display`、`orientation`、`scope`、`shortcuts`、`start_url`、`theme_color`
与 `share_target`，而应用变化意味着向 Google 的铸造服务器请求一个新的 WebAPK。Chrome 会等到
应用关闭、设备在充电且连接 Wi-Fi 之后才去请求；新 WebAPK 安装后，包括图标在内的每个字段都是
最新的。服务器不可达时 Chrome 会退避，最慢每 30 天检查一次。

重命名或移动清单文件会切断这条链：Chrome 重新抓取的是安装时的那个 URL，那里返回 404 就意味着
永远发现不了更新。保持清单 URL 稳定，只改它的内容。

:::observed
Chrome 155（Android 16，英文界面）的 `chrome://webapks`：每个已安装的 WebAPK 都列有
**Manifest URL**、**Manifest Start URL**、**Display Mode**、**Last Update Check Time**、
**Last Update Completion Time** 与 **Update Status** 几行，还有一个 **Check Updates Manually**
链接，可以不等 24 小时计时器直接强制比较。桌面端 Chrome 155 的对应页面是
`chrome://web-app-internals`，它把注册表（含每个应用的清单更新状态）以 JSON 形式整体输出。
:::

## 示例

前两个示例是清单改动及其对已安装用户的影响；第三个是如何不等一天就看到结果。

### 迁移启动 URL 而不产生第二个应用

团队把应用从 `/` 迁到 `/app/`。因为清单声明了 `id`，Chrome 把改过的清单视为同一个应用，在下一次
检查后应用新的 `start_url`；没有 `id` 的话，这个改动在桌面端会被忽略，已安装应用仍然启动 `/`。

```json
{
  "id": "/",
  "name": "Ledger",
  "start_url": "/app/?source=installed",
  "scope": "/",
  "display": "standalone"
}
```

在迁移之前、在一个不改别的东西的发布里先加上 `id`：`id` 必须已经存在于已安装的副本中，之后的
`start_url` 变化才会被识别。

### 更换主题色并知道用户何时能看到

`theme_color` 在两份触发列表上都有，所以品牌换色无需重装就能到达已安装用户。时间线取决于检查
节奏，而不是部署时刻。

```json
{
  "name": "Ledger",
  "theme_color": "#0b5fff",
  "background_color": "#ffffff"
}
```

整天开着应用的桌面用户，要等某次检查发现变化、再关闭最后一个窗口之后才看到新颜色；Android
用户还需要充电加 Wi-Fi 才能让 WebAPK 重新铸造。预期是一两天，很少在 Wi-Fi 下充电的设备更久。

### 测试时强制触发更新检查

Chrome 为开发者提供两条绕过 24 小时节流的路径：重启浏览器（`about://restart` 会重置计时器），
或带 `--disable-manifest-update-throttle` 启动。下面的脚本是应用内的配套检查：它把页面能抓到的
清单与烘进已安装 `start_url` 里的版本戳比较，告诉测试者已安装副本是否仍是旧的。

```js
async function installedManifestIsStale() {
  const installedVersion = new URL(location.href).searchParams.get('v'); // 来自 start_url
  const live = await fetch('/manifest.webmanifest', { cache: 'no-store' }).then((r) => r.json());
  const liveVersion = new URL(live.start_url, location.origin).searchParams.get('v');
  if (!installedVersion || !liveVersion) return false; // 没有版本戳：无可比较
  return installedVersion !== liveVersion;
}
```

页面读不到浏览器存储的副本；`start_url` 里的版本戳是页面唯一能观察到的已安装清单的一部分，
这也是为什么在 `start_url` 里加 `?v=` 成了常见的测试约定。

## 另请参阅

- [id 清单成员](/zh/reference/manifest/id/)
- [start_url 清单成员](/zh/reference/manifest/start-url/)
- [icons 清单成员](/zh/reference/manifest/icons/)
- [WebAPK：Chrome 如何在 Android 上安装 PWA](/zh/reference/installation/webapk/)
- [How Chrome handles updates to the web app manifest](https://web.dev/articles/manifest-updates)（web.dev）
- [Uniquely identifying PWAs with the web app manifest id property](https://developer.chrome.com/docs/capabilities/pwa-manifest-id)（developer.chrome.com）