# 安装提示 UX：何时、如何展示安装按钮

> 使用 beforeinstallprompt API 自定义 PWA 安装提示的时机、位置与文案的最佳实践，包括 iOS 和桌面端的回退方案。

**一句话：** 捕获 `beforeinstallprompt`，等待有意义的用户信号，再从点击处理器中调用
`prompt()`——首次加载时就弹出冷启动安装弹窗几乎必然被关掉，并且会浪费你唯一的机会。

## 核心事件生命周期

```js
let deferredPrompt = null;

// 1. 保存事件；抑制 mini-infobar。
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  showInstallAffordance(); // 展示你自己的按钮或横幅
});

// 2. 仅在用户手势中触发。
installButton.addEventListener('click', async () => {
  if (!deferredPrompt) return;
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  deferredPrompt = null;         // 事件只能使用一次
  hideInstallAffordance();
  if (outcome === 'accepted') recordInstall();
});

// 3. 安装完成后清理。
window.addEventListener('appinstalled', () => {
  deferredPrompt = null;
  hideInstallAffordance();
});
```

关键规则：
- `e.preventDefault()` 是阻止 mini-infobar 的唯一方式。
- `prompt()` 必须在用户激活上下文（点击、触摸）中调用。在手势外调用会被静默忽略。
- 已保存的事件是**一次性的**：一旦 `userChoice` 解析，同一事件对象无法再次调用 `prompt()`。
  解析后清除引用；如需再次提示，重新监听事件。

## 各平台行为

### Android（Chrome / Chromium）

`beforeinstallprompt` 在浏览器的参与度启发式规则满足后触发。
Chrome 也可能在视口底部显示 mini-infobar——`preventDefault()` 可阻止它。
自定义页内提示让你可以选择合适的时机。用户接受后，Chrome 安装 WebAPK（而非快捷方式）
并触发 `appinstalled`。

### iOS / Safari

`beforeinstallprompt` **不会**在 Safari（iOS 或 macOS）上触发。不存在编程式安装 API。
唯一的安装路径是：用户点击分享图标 → "添加到主屏幕"。你的 UX 职责是**引导**用户
（用指向分享图标的箭头或自定义横幅），而非自行触发对话框。
通过 `window.navigator.standalone === true` 检测应用是否已安装，
以避免向已安装用户显示提示。

### 桌面端（Chrome / Edge）

`beforeinstallprompt` 在 Windows、macOS、Linux 和 ChromeOS 上的 Chrome 和 Edge
中触发。安装体验打开一个对话框窗口而非底部弹出表单。地址栏中有一个小型安装图标作为
mini-infobar 的等效物。自定义 `prompt()` 调用可替代默认流程。安装完成后触发 `appinstalled`。

### Firefox

Android 版 Firefox 支持安装（Firefox 128+），但**不**触发 `beforeinstallprompt`。
不存在 JavaScript 安装 API；用户从浏览器菜单发起安装。桌面版 Firefox 不支持 PWA 安装。

## 何时展示安装 UI

| 信号 | 推荐操作 |
|---|---|
| `beforeinstallprompt` 已触发 | 显示低调的安装入口（按钮或横幅）——不要用阻断式弹窗 |
| 首次页面加载，无参与行为 | 暂不操作；冷启动提示的接受率极低 |
| 用户完成关键任务（结账、读完文章、保存表单） | 此时突出显示安装按钮 |
| 用户一周内多次打开应用 | 内联标题芯片或持久底部栏 |
| 已安装（`display-mode: standalone`） | 隐藏所有安装 UI |
| iOS / Safari | 在 2–3 次访问后显示"点击分享 → 添加到主屏幕"提示 |

## 应避免的 UX 反模式

- **首次加载时的阻断式弹窗。** Chrome 自身的研究表明，冷启动提示几乎总是被关掉。
- **被拒后立即再次提示。** 多次拒绝后浏览器可能停止触发 `beforeinstallprompt`；
  把每次提示机会视若珍贵。
- **把提示藏在隐秘的导航里。** 让安装入口可见但不突兀。
- **遗漏 iOS 用户。** `beforeinstallprompt` 不存在并不意味着 iOS 用户无法安装；
  只是需要换一种方式引导他们。

## 检测已安装状态

```js
const isStandalone =
  window.matchMedia('(display-mode: standalone)').matches ||
  window.navigator.standalone === true; // 仅限 Safari iOS

if (isStandalone) hideInstallAffordance();
```

## 兼容性

`beforeinstallprompt` 的浏览器支持情况各异，请查看 [/compatibility/](/zh/compatibility/)
获取最新数据。

## 实践检查清单

- [ ] 用 `preventDefault()` 捕获 `beforeinstallprompt` 并存储。
- [ ] 仅在事件触发后才显示安装按钮。
- [ ] 在点击处理器内调用 `prompt()`，不在加载时调用。
- [ ] `userChoice` 解析后清除延迟事件引用。
- [ ] 监听 `appinstalled` 以隐藏安装 UI 并记录转化。
- [ ] 独立模式检测可阻止向已安装用户显示 UI。
- [ ] 提供 iOS 回退说明（分享 → 添加到主屏幕）。