跳转到内容

安装中的 API

Web Install API(navigator.install)

发布于

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(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。

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(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 安装请求,因此在对话框已打开时再次调用,会以安装中止收尾。

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

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

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 分支,而不要按消息字符串:上面这些名称是草案与解释文档定义的,而消息文本并未被规定。

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

Section titled “从目录页安装同一套件中的另一个应用”

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

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

Section titled “检测支持并回退到 beforeinstallprompt”

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

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 拒绝的浏览器。

规范

规范状态
Web Install: the navigator.install() APIWICG 草案
Web Install: common request preconditionsWICG 草案
Web Install: permission and user consentWICG 草案