# 同一源上的多个 PWA

> 说明清单 id 与 scope 如何区分同一源上的多个已安装 PWA、范围在何处重叠，以及页面能在运行时检测什么。

同一个源可以用不同的清单提供多个独立安装的 Web 应用。用清单 `id` 标识应用身份，用 `scope` 划定各应用上下文的导航边界（[Web Application Manifest](https://www.w3.org/TR/appmanifest/#id-member)，W3C）。

## 工作原理

`id` 和 `start_url` 处理后都是 URL，两者同源。清单身份与已安装应用不匹配时，用户代理应将其视为独立应用；匹配时，则用新清单替换已安装应用的清单。`id` 缺失、为空、无效或跨源时，处理结果保持为处理后的 `start_url`（[`id` 成员](https://www.w3.org/TR/appmanifest/#id-member)，W3C）。

`scope` 是另一个 URL。文档 URL 与 scope URL 同源，且文档路径以 scope 路径开头时，该文档位于范围内。`scope` 缺失、为空、无效或不包含 `start_url` 时，默认值是移除文件名、查询和片段后的 `start_url`（[`scope` 成员](https://www.w3.org/TR/appmanifest/#scope-member)，W3C）。

由于匹配使用路径前缀，`"/sales"` 也包含 `/sales-report`；要把边界限定在这个目录，使用 `"/sales/"`。两个应用的 scope 可以包含同一个 URL，但 `scope` 不负责标识已安装应用。应使用不同且稳定的 `id` 表示身份；若每个 URL 应只留在一个应用上下文内，则选择互不重叠的 scope 路径。

所引 BCD 数据集记录了 Chrome 从 96 起、macOS Safari 从 17 起以及 iOS Safari 从 16.4 起对 `id` 的支持。该文件中的 Edge 与 Samsung Internet 使用镜像数据，没有直接列出版本号。Firefox 会解析该成员但不会应用它（BCD `manifests.webapp.id`）。

:::caution
没有显式 `id` 时，处理后的 `start_url` 会成为身份。要给已有应用添加显式 `id`，先在 DevTools 查看计算 App Id。Chrome 的清单身份指南认为采用这个值最安全。添加后，可以在保持同源的前提下更改 `start_url` 或清单路径（[Uniquely identifying PWAs with the web app manifest id property](https://developer.chrome.com/docs/capabilities/pwa-manifest-id)，Chrome for Developers）。
:::

## 示例

销售应用和支持应用共用一个源时，为它们设置不同的身份与目录边界。

### 声明销售应用

从用于安装销售应用的文档链接这份清单。

```json
{
  "name": "Acme Sales",
  "id": "/apps/sales",
  "start_url": "/sales/",
  "scope": "/sales/",
  "display": "standalone"
}
```

`id` 是标识符而不是导航目标，因此规范不要求它位于 `scope` 内。

### 声明支持应用

支持应用的清单使用另一个身份和互不重叠的路径前缀。

```json
{
  "name": "Acme Support",
  "id": "/apps/support",
  "start_url": "/support/",
  "scope": "/support/",
  "display": "standalone"
}
```

在同一源内更改启动 URL 或清单 URL 时，也要保持两个 `id` 值稳定。

### 检测链接的清单并提供回退

这项页面级检查不会复现用户代理对 `id` 或 `scope` 的处理。它检测浏览器是否报告支持 manifest 链接类型，以及当前文档是否声明了清单链接。任一检查失败时，继续提供普通网站导航。链接的 `relList` 是 `DOMTokenList`，其 `supports()` 方法报告是否支持指定 token（[DOMTokenList.supports()](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/supports)，MDN）。`rel~=` 选择器会在 HTML 定义的空格分隔 `rel` token 中匹配 `manifest`。

```js
const probe = document.createElement('link');
const supportsManifestLink = typeof probe.relList?.supports === 'function'
  && probe.relList.supports('manifest');
const manifestLink = document.querySelector('link[rel~="manifest"][href]');

if (!supportsManifestLink || !manifestLink) {
  document.documentElement.dataset.navigation = 'website';
} else {
  document.documentElement.dataset.navigation = 'app-candidate';
  console.info(`Linked manifest: ${manifestLink.href}`);
}
```

这项检查只确认文档是否声明了清单。它不表示浏览器会处理 `id`，也不表示应用已经安装或当前 URL 位于处理后的 scope 内。

## 另请参阅

- [`id` 清单成员](/zh/reference/manifest/id/)
- [`scope` 清单成员](/zh/reference/manifest/scope/)
- [N+1 安装问题](/zh/reference/installation/n-plus-one/)
- [Web Application Manifest: `id` member](https://www.w3.org/TR/appmanifest/#id-member)（W3C）
- [Web Application Manifest: `scope` member](https://www.w3.org/TR/appmanifest/#scope-member)（W3C）