📚 现代前端开发 目录
阶段4:UI与体验
第 28 / 35 课

PWA与离线

让Web应用原生化

📖 核心概念

💻 代码实现

PWA配置与Service Worker ✅ typescript
// next.config.js - PWA配置
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === 'development',
});

module.exports = withPWA({
  // ...next配置
});

// public/manifest.json
const manifest = {
  name: '我的应用',
  short_name: ' MyApp',
  description: '一个现代化的PWA应用',
  start_url: '/',
  display: 'standalone',
  background_color: '#0f172a',
  theme_color: '#06b6d4',
  orientation: 'portrait-primary',
  icons: [
    { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
    { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
    { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
  ],
  categories: ['productivity', 'utilities'],
  screenshots: [
    { src: '/screenshots/home.png', sizes: '1280x720', type: 'image/png', form_factor: 'wide' },
  ],
};

// Service Worker缓存策略
// sw.ts (使用Workbox)
import { registerRoute } from 'workbox-routing';
import {
  CacheFirst, NetworkFirst, StaleWhileRevalidate,
  NetworkOnly,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute } from 'workbox-precaching';

// 预缓存(构建时生成)
precacheAndRoute(self.__WB_MANIFEST);

// 静态资源 - 缓存优先
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 }),
    ],
  })
);

// API请求 - 网络优先
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3,
    plugins: [
      new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }),
    ],
  })
);

// 第三方资源 - 过期重验证
registerRoute(
  ({ url }) => url.origin === 'https://cdn.example.com',
  new StaleWhileRevalidate({ cacheName: 'cdn-cache' })
);
安装与推送通知 ✅ tsx
// PWA安装提示
import { useState, useEffect } from 'react';

function usePWAInstall() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
  const [isInstallable, setIsInstallable] = useState(false);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
      setIsInstallable(true);
    };
    window.addEventListener('beforeinstallprompt', handler);
    return () => window.removeEventListener('beforeinstallprompt', handler);
  }, []);

  const install = async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log('安装结果:', outcome);
    setDeferredPrompt(null);
    setIsInstallable(false);
  };

  return { isInstallable, install };
}

// 推送通知
async function requestNotificationPermission() {
  if (!('Notification' in window)) return false;
  const permission = await Notification.requestPermission();
  return permission === 'granted';
}

async function subscribeToPush(vapidKey: string) {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: vapidKey,
  });
  // 将subscription发送到服务器
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
  });
  return subscription;
}

// 离线状态检测
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const online = () => setIsOnline(true);
    const offline = () => setIsOnline(false);
    window.addEventListener('online', online);
    window.addEventListener('offline', offline);
    return () => {
      window.removeEventListener('online', online);
      window.removeEventListener('offline', offline);
    };
  }, []);

  return isOnline;
}

function OfflineIndicator() {
  const isOnline = useOnlineStatus();
  if (isOnline) return null;
  return (
    <div className="offline-banner" role="alert">
      📴 当前处于离线模式,部分功能可能不可用
    </div>
  );
}

🎯 练习任务

🏆 成就解锁
PWA开发者 — 掌握PWA开发技术,让Web应用拥有原生体验