# id 清单成员

> id 是独立于 start_url 标识已安装 PWA 的字符串：启动 URL 可以更改，浏览器也不会把同一份清单当成第二个应用重复安装。

`id` 是标识 Web 应用的字符串。在这个成员出现之前，各浏览器从别的东西推导身份，通常是
`start_url`，于是改动启动 URL 就让清单变成了另一个应用，之后的安装会与旧的并存。设置 `id` 后，
身份与启动 URL 是两个独立的值：`start_url` 可以搬动，身份保持不变。

Chrome 96、Edge 96、Samsung Internet 17.0、iOS 上的 Safari 16.4 以及 macOS 上的 Safari 17 处理该
成员（BCD `html.manifest.id`）。Firefox 157 能解析它但不起作用，因为桌面版 Firefox 不从清单安装
Web 应用。忽略 `id` 的浏览器沿用原先的身份规则，所以加上该成员不会破坏其他平台上的安装。

## 成员

- **类型**：字符串，相对 `start_url` 的源按 URL 解析。解析到其他源的值会被拒绝；Chromium 记录
  `property 'id' ignored, should be same origin as document.` 并退回默认值。
- **默认值**：解析后的 `start_url`。`id` 缺失、为空或无法解析时，身份就与启动 URL 绑在一起，而这
  正是该成员要避免的情况。
- **示例值**：`"/?homescreen=1"`，对 `https://example.com` 上的应用解析为
  `https://example.com/?homescreen=1`。

处理清单时，解析后的值会与每个已安装应用的身份比较。匹配意味着"更新这个应用"；不匹配意味着"新
应用"。比较的是完整解析后的 URL，所以 `"/"` 与 `"/?v=2"` 是两个不同的应用，值应当选定一次后不再
动。Chromium 对已安装应用每天检查一次更新，多数字段的改动要等该应用的所有窗口关闭后才应用
（web.dev，"How Chrome handles updates to the web app manifest"）；只有 `id` 存在且未变时，改动
`start_url` 才会被当作更新接受。

`navigator.getInstalledRelatedApps()` 读不到 `id`：它报告相关的原生应用，以及（Chrome 80+）在
`related_applications` 中以 `platform: "webapp"` 列出的 PWA，匹配依据是清单 URL 而不是 `id`。

:::observed
Chrome 155（macOS 26，英文界面）的 DevTools > Application > Manifest，**Identity** 一节的
**App Id** 行：清单没有 `id` 时，面板显示计算出的值，后面跟着
`Note: id is not specified in the manifest, start_url is used instead. To specify an App Id that
matches the current identity, set the id field to <计算值>.` 和一个复制按钮。把 `"id"` 设为恰好
这个值，现有安装不受任何影响；设为其他任何值，下一次安装都会成为一个独立的应用。
:::

## 示例

下面的清单依次展示首次发布前该选什么值、`id` 让哪种改动变得安全，以及如何为一个没带 `id` 就发布
的应用找回身份。

### 首次安装前固定身份

一个短的根相对路径就够了；该成员相对 `start_url` 的源解析，写上协议和主机没有意义。启动 URL 带
着一个分析参数，以后可以改而不触及身份。

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

Chrome 96+ 与 Safari 16.4+ 把身份记录为 `https://example.com/`；之后一份
`"start_url": "/dashboard/?source=pwa"` 的清单会更新已安装的应用，而不是再装一个。

### 移动 start_url 而不产生重复安装

身份固定后，改动启动路径的改版只需编辑 `start_url` 与 `scope`。身份那一行是不能动的。

```json
{
  "name": "Ledger",
  "id": "/",
  "start_url": "/home/",
  "scope": "/",
  "display": "standalone"
}
```

在 Chrome 155 上，已安装应用在下一次每日清单检查时拿到新的 `start_url`，并在最后一个应用窗口关闭
后应用它；在忽略 `id` 的浏览器上，只要该浏览器从未以 `start_url` 作为身份键，同样的改动也能正常
工作。

### 为没带 id 就发布的应用补上 id

在声明 `id` 之前安装的应用，其身份是 Chromium 从 `start_url` 算出来的。要保住这些安装，把 `id`
设为 DevTools 显示的那个精确计算值；下面的脚本读取页面链接的清单并打印可供复制的值，清单抓取
失败时给出提示。

```js
async function suggestedId() {
  const link = document.querySelector('link[rel="manifest"]');
  if (!link || !('fetch' in window)) return null; // 没有可检查的东西。
  const manifest = await fetch(link.href).then((r) => r.json()).catch(() => null);
  if (!manifest) return null;
  if (manifest.id) return new URL(manifest.id, location.origin).href;
  return new URL(manifest.start_url ?? '/', link.href).href; // Chromium 的默认身份。
}

const id = await suggestedId();
console.log(id ? `Set "id" to ${new URL(id).pathname + new URL(id).search}` : 'Manifest not readable');
```

对 `"start_url": "/app/?source=pwa"`，打印出的值是 `/app/?source=pwa`；把这个字符串声明为 `id`
就冻结了现有用户已经拥有的身份，此后 `start_url` 可以自由更改。

## 另请参阅

- [start_url 清单成员](/zh/reference/manifest/start-url/)
- [已安装的 PWA 如何获取清单更新](/zh/reference/manifest/manifest-updates/)
- [N+1 安装问题：同一个 PWA、多份互相独立的安装](/zh/reference/installation/n-plus-one/)
- [getInstalledRelatedApps()：检测已安装的原生应用或 PWA 对应物](/zh/reference/installation/get-installed-related-apps/)
- [Web Application Manifest: id member](https://www.w3.org/TR/appmanifest/#id-member)（w3.org）
- [Uniquely identifying PWAs with the web app manifest id property](https://developer.chrome.com/docs/capabilities/pwa-manifest-id)（developer.chrome.com）
- [WebKit Features in Safari 16.4](https://webkit.org/blog/13966/webkit-features-in-safari-16-4/)（webkit.org）