# 进阶

> 以渐进增强方式接入推送、文件访问与平台集成等生产级能力，每一步都先做兼容性检查。

完成本指南后，应用会在浏览器支持的地方提供 Web Push、文件访问、后台同步与图标角标，在不支持的地方只失去那一项功能，而不是整个页面。每项能力都用同一个三段式模式包裹：一次特性检测、一个先于增强写好的回退分支，以及发布前在本站查一次兼容性。请先完成[入门](/zh/guides/getting-started/)、[让它可安装](/zh/guides/installable/)与[离线策略](/zh/guides/offline/)；这里的每项能力都假定已注册 Service Worker 且 manifest 可安装。

## 把能力闸门只写一次

把检测放进一个模块，让界面、Service Worker 与埋点对「什么可用」有一致的答案。闸门读取的是对象图而不是 User-Agent，每项能力的回退是一个具体动作，而不是一个被禁用的按钮：

```js
export const capabilities = {
  push: 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window,
  fileSystemAccess: 'showOpenFilePicker' in window,
  backgroundSync: 'serviceWorker' in navigator && 'SyncManager' in window,
  badging: 'setAppBadge' in navigator,
};

export const fallbacks = {
  push: () => showEmailDigestOptIn(),            // 用服务端邮件摘要代替推送
  fileSystemAccess: () => showDownloadAndUpload(), // <a download> 加 <input type="file">
  backgroundSync: () => retryQueueOnNextLoad(),    // 在 'online' 事件时冲刷 IndexedDB 队列
  badging: () => showUnreadCountInTitle(),         // document.title = `(3) Field Notes`
};

export function withCapability(name, enhanced) {
  return capabilities[name] ? enhanced() : fallbacks[name]();
}
```

API 存在只是必要条件，不能证明功能对某个用户可用：iOS Safari 的浏览器标签页里有 `PushManager`，但 WebKit 只向添加到主屏幕的 Web 应用授予推送（iOS 与 iPadOS 16.4，2023-03）。每个闸门都要配上本站对应的兼容性条目，并读它的备注列。

## 在用户手势之后接入 Web Push

web.dev 的推送概览把工作分成三步：请求权限并订阅的客户端代码、负责发送的服务端代码，以及接收并显示通知的 Service Worker 代码。权限请求必须跟在点击之类的手势之后；WebKit 对主屏幕 Web 应用的要求相同。用你的 VAPID 公钥订阅（web.dev："the combination of the private and public key is known as the application server keys"）：

```js
async function enablePush(vapidPublicKey) {
  if (!capabilities.push) return fallbacks.push();
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return fallbacks.push();
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: vapidPublicKey,
  });
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(subscription),
  });
}
```

订阅对象中的 `endpoint` 指向浏览器厂商的推送服务；web.dev 把它的域名描述为 "essentially the push service"，路径则是客户端标识。把整个订阅对象存在服务端，发送返回 404 或 410 时删除它。在 Service Worker 里，`push` 监听器调用 `self.registration.showNotification()`；收到推送却什么都不显示的处理器会被视为静默推送，浏览器不允许这样做。

## 用同样的方式守护文件访问与后台同步

`showOpenFilePicker()` 让编辑器类应用读写用户选中的文件；回退是每个浏览器都支持的一对，`<input type="file">` 负责读，`<a download>` 负责写。Background Sync 让 worker 在网络恢复时重放离线期间的写入；回退是把队列放在 IndexedDB 里，在 `online` 事件或下次页面加载时冲刷。两者都通过 `withCapability()` 调用，让两个分支在源码里挨在一起，回退路径就不会悄悄腐坏。

## 在 DevTools 中测试增强路径

Chrome 的 **Service workers** 面板有 **Push** 与 **Sync** 链接，无需服务器即可向活动 worker 派发 `push` 与 `sync` 事件，让你在后端尚不存在时先验证接收侧。用 **Offline** 确认排队的写入在刷新后仍在，并在取消勾选后被重放。

:::observed
Chrome DevTools 中 **Push** 链接旁的 **Push data** 输入框默认是纯文本载荷 `Test push message from DevTools`（DevTools 前端 `front_end/panels/application/ServiceWorkersView.ts` 中的字符串 `testPushMessageFromDevtools`，main 分支，2026-10-03 读取）。直接解析 `event.data.json()` 的 `push` 处理器会在这个载荷上抛错，因为它不是 JSON；先读 `event.data.text()`，或给 JSON 解析加保护，否则 DevTools 测试会在 **Source** 旁显示一个真实服务器推送不会产生的错误计数。
:::

## 另请参阅

- [Web Push](/zh/reference/notifications/web-push/)
- [File System Access](/zh/reference/capabilities/file-system-access/)
- [Background Sync](/zh/reference/service-worker/background-sync/)
- [角标](/zh/reference/installation/badging/)
- [按特性查看兼容性](/zh/compatibility/by-feature/)
- [Push notifications overview](https://web.dev/articles/push-notifications-overview)（web.dev）
- [Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)（webkit.org）

← 返回[指南](/zh/guides/)总览。