pwa 安装/离线/推送/后台同步 全套高级能力

23 阅读3分钟

安装提示打磨 + 优雅离线兜底 + Push 通知完整闭环 + Background Sync

1. 安装体验优化(让用户“心甘情愿”点安装)

核心 APIbeforeinstallprompt(Android/Chrome 自动 banner;iOS 手动优化)

App.tsx 或主入口添加:

import { useEffect, useState } from 'react'

function InstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
  const [showInstallButton, setShowInstallButton] = useState(false)

  useEffect(() => {
    const handler = (e: any) => {
      e.preventDefault()           // 阻止默认 banner
      setDeferredPrompt(e)
      setShowInstallButton(true)
    }
    window.addEventListener('beforeinstallprompt', handler)

    // iOS 26+ 改进:即使无 manifest 也能以 web app 打开
    return () => window.removeEventListener('beforeinstallprompt', handler)
  }, [])

  const handleInstall = async () => {
    if (!deferredPrompt) return
    deferredPrompt.prompt()
    const { outcome } = await deferredPrompt.userChoice
    console.log('用户选择:', outcome)
    setDeferredPrompt(null)
    setShowInstallButton(false)
  }

  return showInstallButton ? (
    <button onClick={handleInstall} className="fixed bottom-4 right-4 bg-blue-600 text-white px-6 py-3 rounded-2xl shadow-xl">
      📲 添加到主屏幕(像 App 一样用)
    </button>
  ) : null
}

iOS 26+ 新特性(2026 年重大改进):

  • 任何网站“添加到主屏幕”默认以 web app 模式 打开(无地址栏、无 Safari UI)。
  • 无需完美 manifest 也能 standalone。

图标 & 启动屏优化(manifest 已在上讲配置):

  • 必须准备 192×192 + 512×512 + maskable 图标
  • 添加 <meta name="apple-mobile-web-app-capable" content="yes">(兼容旧 iOS)

2. 离线体验升级(不白屏 + 优雅兜底)

方式一:在 Workbox 中加 fallback(推荐)

vite.config.ts 的 runtimeCaching 里补充:

{
  urlPattern: ({ request }) => request.mode === 'navigate',
  handler: 'NetworkFirst',
  options: {
    cacheName: 'pages',
    networkTimeoutSeconds: 3,
    plugins: [
      {
        // 网络失败时返回 offline.html
        handlerDidError: async () => caches.match('/offline.html')
      }
    ]
  }
}

方式二:手动创建 public/offline.html(美化版):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>离线模式 - 我的 PWA</title>
  <style>body { font-family: system-ui; text-align: center; padding-top: 40vh; }</style>
</head>
<body>
  <h1>📴 当前离线</h1>
  <p>已缓存内容可正常浏览</p>
  <button onclick="location.reload()">重试联网</button>
</body>
</html>

预缓存它:globPatterns: ['**/*.{..., offline.html}']

效果:断网打开任何页面 → 自动展示 offline.html(用户感觉“App 还在”)

3. Push Notification 全流程(生产可用)

步骤 1:VAPID 密钥生成(服务器端)

npm install web-push
npx web-push generate-vapid-keys

记录 VAPID_PUBLIC_KEYVAPID_PRIVATE_KEY

步骤 2:客户端订阅(main.tsx 或单独组件)

const publicVapidKey = '你的公钥'

async function subscribeUser() {
  if (!('PushManager' in window)) return alert('浏览器不支持 Push')

  const registration = await navigator.serviceWorker.ready
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
  })

  // 发送到你的后端保存
  await fetch('/api/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
    headers: { 'Content-Type': 'application/json' }
  })
}

// 工具函数
function urlBase64ToUint8Array(base64String: string) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4)
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
  const rawData = window.atob(base64)
  return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)))
}

步骤 3:Service Worker 接收推送(vite-plugin-pwa 会自动合并)

sw.js 自定义(或通过 workbox):

self.addEventListener('push', event => {
  const data = event.data.json()
  const options = {
    body: data.body,
    icon: '/pwa-192x192.png',
    badge: '/badge.png',
    actions: [{ action: 'open', title: '打开 App' }]
  }
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  )
})

self.addEventListener('notificationclick', event => {
  event.notification.close()
  event.waitUntil(clients.openWindow('/'))
})

步骤 4:服务器推送示例(Node.js)

const webPush = require('web-push')
webPush.setVapidDetails('mailto:your@email.com', publicKey, privateKey)

webPush.sendNotification(subscription, JSON.stringify({
  title: '订单已发货!',
  body: '您的包裹正在派送中'
}))

2026 年 iOS 现状

  • iOS 16.4+(含 iOS 26)完整支持 Web Push(需加到主屏幕)
  • 无 silent/background push,reach 低于原生
  • EU 地区更稳定,非 EU 也已开放

4. Background Sync & Periodic Sync(离线后自动同步)

使用 Workbox 插件(Android/Chrome 完美,iOS ❌)

安装:npm install workbox-background-sync

vite.config.ts 添加:

import { BackgroundSyncPlugin } from 'workbox-background-sync'

runtimeCaching: [
  {
    urlPattern: ({ url }) => url.pathname.startsWith('/api/submit-form'),
    handler: 'NetworkOnly',   // 离线时失败 → 进入队列
    options: {
      plugins: [
        new BackgroundSyncPlugin('formQueue', {
          maxRetentionTime: 24 * 60   // 最多保留 24 小时
        })
      ]
    }
  }
]

客户端触发

// 离线提交表单时
fetch('/api/submit-form', { method: 'POST', body: data })
  .catch(() => {
    console.log('离线,已加入后台队列')
  })

Periodic Sync(定期更新内容,Chrome 专属)

// 在 SW 中
self.addEventListener('periodicsync', event => {
  if (event.tag === 'update-content') {
    event.waitUntil(updateCacheAndNotify())
  }
})

// 注册(客户端)
async function registerPeriodicSync() {
  const registration = await navigator.serviceWorker.ready
  await registration.periodicSync.register('update-content', {
    minInterval: 24 * 60 * 60 * 1000   // 每天
  })
}

2026 年平台对比

  • Android/Chrome:完整支持
  • iOS/Safari:Background Sync & Periodic Sync 仍不支持(无时间表)
  • iOS 替代方案:用 Push 唤醒 + 用户打开时同步