# Web Install API（navigator.install）

> navigator.install() 如何安装当前文档或某个 manifest URL、它需要的用户激活与权限，以及它拒绝时使用的 DOMException 名称。

`navigator.install()` 是一个基于 Promise 的方法，用来请求浏览器安装一个 Web 应用：不带参数调用时目标是当前文档所链接的 manifest，带参数时目标是调用方给出的 manifest URL。解释文档把它定位为 `beforeinstallprompt` 的扩展，后者无法安装当前已加载的 Web 应用之外的内容。

## 支持情况

BCD `api.Navigator.install` 记录 Chrome 154 起支持，需要把名为 `#web-app-installation-api` 的偏好开关设为 `Enabled`，并镜像到该记录列出的 Chromium 系浏览器；Firefox 与 Safari 均无支持。该记录标记为实验性且处于标准轨道。

[Chrome Platform Status feature 5183481574850560](https://chromestatus.com/feature/5183481574850560)（chromestatus.com）把 Web Install API 列为在 Chrome 157 中默认启用，并这样概括它：“Starting in Chrome 157, the new Web Install API allows websites to prompt users to install a web app, given a manifest URL and optional manifest ID.”同一页面把规范成熟度记为 "Specification being incubated in a Community Group"，把 Mozilla 的标准立场记为 "No signal"，把 WebKit 的记为 "Oppose"，并分别链接到对应的 standards-positions issue。

## 语法

```js
await navigator.install();
await navigator.install({ manifest: 'https://suite.example/mail/manifest.json' });
await navigator.install({
  manifest: 'https://music.example/manifest.webmanifest',
  manifestId: 'https://music.example/app',
});
```

草案规范在 `Navigator` 的 partial 接口上声明了两个重载，都返回 `Promise<WebInstallResult>`：一个不带参数，另一个接受 `InstallParams` 字典，其中 `manifest` 成员为必填，`manifestId` 成员是可选且可为 null 的 `USVString`。`WebInstallResult` 被声明为空字典。

## 参数

字典可以整体省略，此时安装目标就是当前已加载页面的 manifest。

| 参数 | 类型 | 含义 |
|---|---|---|
| `options.manifest` | `USVString`，必填 | 标识目标 Web 应用的应用 manifest 的 URL。相对值按发起文档的 URL 解析。 |
| `options.manifestId` | `USVString?`，可选 | 一个绝对的 manifest ID 断言：调用方预期的处理后 manifest id。草案说明 null 与省略含义相同。 |

解释文档说明：当且仅当 `manifest` 指向的 JSON 中含有 `id` 时，开发者才可以省略 `manifestId`。当 manifest 没有声明 `id` 成员时，[Web App Manifest](https://www.w3.org/TR/appmanifest/#id-member)（w3.org）会由 `start_url` 计算出 id，而解释文档指出这个计算出的 id 可以在 Chromium DevTools 的 Application 面板中读到。

Promise 兑现为一个 `WebInstallResult`，草案把它定义为一个为未来兼容性预留的空字典。

## 异常

该方法声明的返回类型是 `Promise<WebInstallResult>`，所以下面每一种失败都以 Promise 拒绝的形式到达调用方：请在 `try`/`catch` 中 `await` 它，或者给返回的 Promise 挂一个拒绝处理器。草案的方法步骤对前三种使用规范算法的 `throw` 措辞，而对于返回 Promise 的操作，Web IDL 会把它转成被拒绝的 Promise；解释文档自己的步骤也把这几个名称描述为 Promise 拒绝时使用的名称。草案指出用户激活在参数校验之前就被消耗，这与 Chromium 一致，所以一个无法解析的 URL 同样会花掉这次激活。

| 拒绝 | 发生于 | 何时发生 |
|---|---|---|
| `InvalidStateError` | 方法步骤中，任何请求开始之前 | 文档不是合格的发起文档。草案要求文档完全活跃、是某个顶级可遍历对象的活跃文档、不是 fenced frame 文档、活跃沙箱标志集合为空，且 `web-app-installation` 策略受控特性处于启用状态。 |
| `NotAllowedError` | 方法步骤中，任何请求开始之前 | 文档没有瞬态激活。 |
| `TypeError` | 方法步骤中，任何请求开始之前 | `options.manifest` 无法按文档 URL 解析，或 `options.manifestId` 无法解析为绝对 URL。 |
| `DataError` | 请求的结果 | 结果为无效的安装数据：按草案的说法，所提供的 URL、manifest、身份断言或必需的安装元数据无效或不可用。解释文档列出的情形包括无效的 `manifest` URL、manifest 抓取失败、格式错误的 manifest、manifest 未声明 `id` 而调用方又省略了 `manifestId`，以及 `manifestId` 与计算出的 id 不匹配。 |
| `AbortError` | 请求的结果 | 其他所有未完成的情形。草案的注释写道：“Permission denial, unsupported profiles, navigation, concurrency, user dismissal, and internal failure all map to AbortError.” |

解释文档还把 `NotFoundError`（文档缺失）列入 Promise 可能拒绝的名称，而草案的临时方法步骤把每一个不合格的发起文档都归到 `InvalidStateError`；草案自己把这套名称分类标记为尚未定论。

还有一条拒绝路径没有专属名称：草案为每个顶级可遍历对象只保留一个活跃的 Web 安装请求，因此在对话框已打开时再次调用，会以安装中止收尾。

:::caution
Chrome Platform Status 描述了两种针对该 API 的管理员控制手段：企业策略 [`WebAppInstallByUserEnabled`](https://chromeenterprise.google/policies/#WebAppInstallByUserEnabled)（chromeenterprise.google）会整体禁用用户安装 Web 应用，而 `web-app-installation` 权限策略可以在企业管辖的源上允许或禁止该特性。当 `web-app-installation` 策略受控特性被禁用时，文档不符合发起条件，`navigator.install()` 会在请求开始前以 `InvalidStateError` 拒绝。在请求处理阶段发生的权限或其他策略拒绝，以及用户关闭确认界面，都会映射为 `AbortError`。
:::

## 示例

每个示例都从 `click` 处理器中发起，因为草案在没有瞬态激活时会以 `NotAllowedError` 拒绝。字典形式还要额外经过 `web-app-installation` 权限。

### 用一个按钮安装当前页面

不带参数的形式以文档所链接的 manifest 为目标。在草案的“obtain web install permission”步骤中，来源为当前文档时直接返回 true 而不请求 `web-app-installation` 权限，于是流程直接走到浏览器的确认界面。没有 `id` 成员的 manifest 会由 `start_url` 计算出一个，但解释文档中不带参数的安装步骤在 manifest 文件未声明 `id` 时以 `DataError` 拒绝，所以请显式声明它。

```js
const button = document.querySelector('#install');

button.addEventListener('click', async () => {
  try {
    await navigator.install();
    button.hidden = true;
  } catch (err) {
    if (err.name === 'DataError') {
      // manifest 缺失、无法抓取，或未声明 `id`。
      reportInstallBlocked('manifest');
    } else if (err.name === 'AbortError') {
      // 被关闭、被策略拒绝，或配置不受支持。
      button.textContent = 'Install';
    }
  }
});
```

请按 `err.name` 分支，而不要按消息字符串：上面这些名称是草案与解释文档定义的，而消息文本并未被规定。

### 从目录页安装同一套件中的另一个应用

字典形式指名另一个应用的 manifest。解释文档的步骤以凭据模式 `"omit"` 抓取该 manifest，其开放问题记录了当前的决定：manifest URL 必须与 `start_url` 同源，manifest 内部的相对 URL 按该 manifest URL 解析。当目标 manifest 自身未声明 `id` 时，请传入 `manifestId`。

```js
const CATALOG = [
  { name: 'Mail', manifest: 'https://suite.example/mail/manifest.json' },
  { name: 'Calendar', manifest: 'https://suite.example/calendar/manifest.json' },
];

async function installFromCatalog(entry) {
  try {
    await navigator.install({ manifest: entry.manifest });
    return { ok: true };
  } catch (err) {
    if (err.name === 'TypeError' || err.name === 'DataError') {
      return { ok: false, reason: 'catalog-entry-stale' };
    }
    return { ok: false, reason: 'not-installed' }; // AbortError 以及其他情形
  }
}
```

过期的目录条目正是 `DataError` 存在的场景：解释文档的开放问题把带版本号的 manifest 文件名问题留作未决，并依靠 `DataError` 告诉调用方某个已发布的 URL 已经失效。

### 检测支持并回退到 beforeinstallprompt

权限查询让页面在渲染按钮之前就知道跨源安装是否会被拒绝，而 `'install' in navigator` 告诉它这个方法是否存在。在方法不存在的地方，被延迟保存的 `beforeinstallprompt` 事件是 Chromium 仍然提供的路径，其余浏览器则各有自己的安装入口。

```js
let deferredPrompt = null;
addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();
  deferredPrompt = event;
});

async function installTarget(manifestUrl) {
  if (!('install' in navigator)) {
    if (deferredPrompt) return deferredPrompt.prompt(); // Chromium 更早的入口
    return { ok: false, reason: 'use-browser-menu' }; // 这里没有脚本化安装
  }
  const status = await navigator.permissions
    .query({ name: 'web-app-installation' })
    .catch(() => null); // 名称不受支持：按 Permissions 规范以 TypeError 拒绝
  if (status?.state === 'denied') {
    return { ok: false, reason: 'permission-denied' };
  }
  return navigator.install({ manifest: manifestUrl });
}
```

BCD `api.Permissions.permission_web-app-installation` 记录该权限名称自 Chrome 154 起可用，需要同一个偏好开关，所以 `catch` 分支覆盖的是尚未注册该名称、`query()` 以 `TypeError` 拒绝的浏览器。

:::observed
以下都是精确字符串，各自来自所注明的来源。BCD `api.Navigator.install` 给出的 Chrome 偏好开关为 `#web-app-installation-api`，其 `value_to_set` 为 `Enabled`（[api/Navigator.json](https://github.com/mdn/browser-compat-data/blob/main/api/Navigator.json)，github.com）。草案的声明式对应物定义了 `enum InstallResult { "success", "aborted", "invalid_data" };`，并通过 `InstallResultEvent` 暴露（[Web Install](https://wicg.github.io/install-element/#install-result-event)，wicg.github.io），因此 `<install>` 元素报告的正是 Promise 分别映射到兑现、`AbortError` 与 `DataError` 的那三种结果。Chrome Platform Status 把两项管理员控制手段命名为 `WebAppInstallByUserEnabled` 与 `web-app-installation`（[feature 5183481574850560](https://chromestatus.com/feature/5183481574850560)，chromestatus.com）。
:::

## 另请参阅

- [Web Install](https://wicg.github.io/install-element/#navigator-api)（wicg.github.io）
- [Web Install API explainer](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/WebInstall/explainer.md)（github.com）
- [beforeinstallprompt 事件](/zh/reference/installation/install-prompt/)
- [可安装性判定条件](/zh/reference/installation/installability-criteria/)