PWA技术深度解析

PWA技术深度解析:构建现代化Web应用的核心技术栈

目录

  1. PWA技术概述
  2. PWA核心特性
  3. [Service Worker详解](#Service Worker详解 "#service-worker%E8%AF%A6%E8%A7%A3")
  4. [Web App Manifest配置](#Web App Manifest配置 "#web-app-manifest%E9%85%8D%E7%BD%AE")
  5. 缓存策略实现
  6. 离线功能开发
  7. 推送通知机制
  8. 性能优化实践
  9. 最佳实践总结

PWA技术概述

Progressive Web App(渐进式Web应用)是一种结合了Web和原生应用最佳特性的技术方案。PWA通过现代Web API和渐进式增强策略,为用户提供类似原生应用的体验。

PWA的核心价值

PWA技术解决了传统Web应用的几个关键痛点:

  • 性能问题:通过Service Worker实现智能缓存
  • 离线体验:支持离线访问和数据同步
  • 用户参与度:支持推送通知和桌面安装
  • 跨平台兼容:一套代码多端运行

技术架构优势

javascript 复制代码
// PWA核心技术栈示例
const PWACore = {
  serviceWorker: 'sw.js',
  manifest: 'manifest.json',
  cacheStrategy: 'Cache First',
  offlineSupport: true,
  pushNotifications: true,
  installable: true
};

// 检测PWA支持
function checkPWASupport() {
  const features = {
    serviceWorker: 'serviceWorker' in navigator,
    manifest: 'manifest' in document.createElement('link'),
    notification: 'Notification' in window,
    pushManager: 'PushManager' in window
  };
  
  console.log('PWA Features Support:', features);
  return features;
}

缓存策略实现

PWA的缓存策略是性能优化的核心,不同的资源类型需要采用不同的缓存策略以达到最佳的用户体验。

常见缓存策略

graph TD A[请求发起] --> B{缓存策略类型} B -->|Cache First| C[检查缓存] C -->|命中| D[返回缓存] C -->|未命中| E[网络请求] E --> F[缓存响应] F --> G[返回响应] B -->|Network First| H[网络请求] H -->|成功| I[缓存响应] I --> J[返回响应] H -->|失败| K[检查缓存] K -->|有缓存| L[返回缓存] K -->|无缓存| M[返回错误] B -->|Stale While Revalidate| N[返回缓存] N --> O[后台更新] O --> P[更新缓存]

缓存策略实现

javascript 复制代码
// 缓存策略管理器
class CacheStrategies {
  constructor() {
    this.strategies = {
      cacheFirst: this.cacheFirst.bind(this),
      networkFirst: this.networkFirst.bind(this),
      staleWhileRevalidate: this.staleWhileRevalidate.bind(this),
      cacheOnly: this.cacheOnly.bind(this),
      networkOnly: this.networkOnly.bind(this)
    };
  }
  
  // 缓存优先策略 - 适用于不经常变化的资源
  async cacheFirst(request, cacheName = 'cache-first') {
    try {
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      if (cachedResponse) {
        return cachedResponse;
      }
      
      const networkResponse = await fetch(request);
      if (networkResponse.ok) {
        cache.put(request, networkResponse.clone());
      }
      
      return networkResponse;
    } catch (error) {
      console.error('Cache First策略失败:', error);
      throw error;
    }
  }
  
  // 网络优先策略 - 适用于需要最新数据的API
  async networkFirst(request, cacheName = 'network-first', timeout = 3000) {
    try {
      const cache = await caches.open(cacheName);
      
      // 设置网络请求超时
      const networkPromise = this.fetchWithTimeout(request, timeout);
      
      try {
        const networkResponse = await networkPromise;
        if (networkResponse.ok) {
          cache.put(request, networkResponse.clone());
        }
        return networkResponse;
      } catch (networkError) {
        console.warn('网络请求失败,尝试缓存:', networkError);
        const cachedResponse = await cache.match(request);
        
        if (cachedResponse) {
          return cachedResponse;
        }
        
        throw networkError;
      }
    } catch (error) {
      console.error('Network First策略失败:', error);
      throw error;
    }
  }
  
  // 先返回缓存,同时更新策略 - 适用于频繁更新但可容忍旧数据的资源
  async staleWhileRevalidate(request, cacheName = 'stale-while-revalidate') {
    try {
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      // 后台更新缓存
      const updateCache = async () => {
        try {
          const networkResponse = await fetch(request);
          if (networkResponse.ok) {
            await cache.put(request, networkResponse.clone());
          }
        } catch (error) {
          console.warn('后台更新缓存失败:', error);
        }
      };
      
      updateCache(); // 不等待
      
      if (cachedResponse) {
        return cachedResponse;
      }
      
      // 如果没有缓存,等待网络响应
      return await fetch(request);
    } catch (error) {
      console.error('Stale While Revalidate策略失败:', error);
      throw error;
    }
  }
  
  // 仅缓存策略 - 适用于完全离线的资源
  async cacheOnly(request, cacheName = 'cache-only') {
    try {
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      if (cachedResponse) {
        return cachedResponse;
      }
      
      throw new Error('缓存中未找到资源');
    } catch (error) {
      console.error('Cache Only策略失败:', error);
      throw error;
    }
  }
  
  // 仅网络策略 - 适用于敏感数据或分析
  async networkOnly(request) {
    try {
      return await fetch(request);
    } catch (error) {
      console.error('Network Only策略失败:', error);
      throw error;
    }
  }
  
  // 带超时的fetch
  fetchWithTimeout(request, timeout) {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(`请求超时: ${timeout}ms`));
      }, timeout);
      
      fetch(request)
        .then(response => {
          clearTimeout(timeoutId);
          resolve(response);
        })
        .catch(error => {
          clearTimeout(timeoutId);
          reject(error);
        });
    });
  }
  
  // 根据请求类型选择策略
  getStrategy(request) {
    const url = new URL(request.url);
    const pathname = url.pathname;
    const fileExtension = pathname.split('.').pop().toLowerCase();
    
    // 静态资源使用缓存优先
    if (['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'woff', 'woff2'].includes(fileExtension)) {
      return 'cacheFirst';
    }
    
    // API请求使用网络优先
    if (pathname.startsWith('/api/') || pathname.startsWith('/graphql')) {
      return 'networkFirst';
    }
    
    // HTML文档使用先返回缓存再更新
    if (request.destination === 'document') {
      return 'staleWhileRevalidate';
    }
    
    // 默认策略
    return 'networkFirst';
  }
}

// Service Worker中的缓存策略应用
class ServiceWorkerCache {
  constructor() {
    this.cacheStrategies = new CacheStrategies();
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    self.addEventListener('fetch', (event) => {
      event.respondWith(this.handleFetch(event.request));
    });
  }
  
  async handleFetch(request) {
    // 跳过非GET请求
    if (request.method !== 'GET') {
      return fetch(request);
    }
    
    try {
      const strategy = this.cacheStrategies.getStrategy(request);
      return await this.cacheStrategies.strategies[strategy](request);
    } catch (error) {
      return this.handleFetchError(request, error);
    }
  }
  
  async handleFetchError(request, error) {
    console.error('请求处理失败:', error);
    
    // 如果是文档请求,返回离线页面
    if (request.destination === 'document') {
      try {
        const cache = await caches.open('offline-pages');
        const offlinePage = await cache.match('/offline.html');
        if (offlinePage) {
          return offlinePage;
        }
      } catch (cacheError) {
        console.error('获取离线页面失败:', cacheError);
      }
    }
    
    // 返回基本的错误响应
    return new Response('网络错误', {
      status: 408,
      statusText: 'Network Timeout'
    });
  }
}

缓存管理工具

javascript 复制代码
// 缓存管理器
class CacheManager {
  constructor() {
    this.maxCacheSize = 50 * 1024 * 1024; // 50MB
    this.maxCacheAge = 7 * 24 * 60 * 60 * 1000; // 7天
  }
  
  // 清理过期缓存
  async cleanExpiredCache() {
    try {
      const cacheNames = await caches.keys();
      
      for (const cacheName of cacheNames) {
        await this.cleanCacheByAge(cacheName);
      }
      
      console.log('过期缓存清理完成');
    } catch (error) {
      console.error('清理过期缓存失败:', error);
    }
  }
  
  async cleanCacheByAge(cacheName) {
    try {
      const cache = await caches.open(cacheName);
      const requests = await cache.keys();
      
      for (const request of requests) {
        const response = await cache.match(request);
        if (response) {
          const cachedDate = response.headers.get('date');
          if (cachedDate) {
            const ageInMs = Date.now() - new Date(cachedDate).getTime();
            if (ageInMs > this.maxCacheAge) {
              await cache.delete(request);
              console.log(`删除过期缓存: ${request.url}`);
            }
          }
        }
      }
    } catch (error) {
      console.error(`清理缓存 ${cacheName} 失败:`, error);
    }
  }
  
  // 控制缓存大小
  async limitCacheSize(cacheName, maxItems = 50) {
    try {
      const cache = await caches.open(cacheName);
      const requests = await cache.keys();
      
      if (requests.length > maxItems) {
        // 删除最老的缓存项
        const itemsToDelete = requests.slice(0, requests.length - maxItems);
        
        for (const request of itemsToDelete) {
          await cache.delete(request);
        }
        
        console.log(`缓存 ${cacheName} 大小已限制为 ${maxItems} 项`);
      }
    } catch (error) {
      console.error(`限制缓存大小失败:`, error);
    }
  }
  
  // 获取缓存统计信息
  async getCacheStats() {
    try {
      const cacheNames = await caches.keys();
      const stats = {};
      
      for (const cacheName of cacheNames) {
        const cache = await caches.open(cacheName);
        const requests = await cache.keys();
        
        let totalSize = 0;
        for (const request of requests) {
          const response = await cache.match(request);
          if (response && response.headers.get('content-length')) {
            totalSize += parseInt(response.headers.get('content-length'));
          }
        }
        
        stats[cacheName] = {
          itemCount: requests.length,
          size: totalSize,
          sizeFormatted: this.formatBytes(totalSize)
        };
      }
      
      return stats;
    } catch (error) {
      console.error('获取缓存统计失败:', error);
      return {};
    }
  }
  
  formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
  
  // 预缓存资源
  async precacheResources(resources) {
    try {
      const cache = await caches.open('precache-v1');
      
      for (const resource of resources) {
        try {
          const response = await fetch(resource);
          if (response.ok) {
            await cache.put(resource, response);
            console.log(`预缓存成功: ${resource}`);
          }
        } catch (error) {
          console.warn(`预缓存失败: ${resource}`, error);
        }
      }
    } catch (error) {
      console.error('预缓存失败:', error);
    }
  }
}

PWA核心特性

1. 渐进式增强

PWA采用渐进式增强策略,确保在不同环境下都能正常工作:

javascript 复制代码
// 特性检测和渐进式加载
class PWAEnhancement {
  constructor() {
    this.features = this.detectFeatures();
    this.init();
  }
  
  detectFeatures() {
    return {
      serviceWorker: 'serviceWorker' in navigator,
      pushManager: 'PushManager' in window,
      notification: 'Notification' in window,
      backgroundSync: 'serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype
    };
  }
  
  async init() {
    // 基础功能始终可用
    this.setupBasicFeatures();
    
    // 高级功能按需启用
    if (this.features.serviceWorker) {
      await this.setupServiceWorker();
    }
    
    if (this.features.notification) {
      this.setupNotifications();
    }
  }
  
  setupBasicFeatures() {
    // 基础Web功能实现
    console.log('设置基础功能');
  }
  
  async setupServiceWorker() {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('Service Worker注册成功:', registration);
    } catch (error) {
      console.error('Service Worker注册失败:', error);
    }
  }
  
  setupNotifications() {
    // 通知功能设置
    console.log('设置通知功能');
  }
}

2. 响应式设计

PWA必须在所有设备上提供一致的用户体验:

javascript 复制代码
// 响应式断点管理
class ResponsiveManager {
  constructor() {
    this.breakpoints = {
      mobile: '(max-width: 768px)',
      tablet: '(min-width: 769px) and (max-width: 1024px)',
      desktop: '(min-width: 1025px)'
    };
    this.setupMediaQueries();
  }
  
  setupMediaQueries() {
    Object.entries(this.breakpoints).forEach(([device, query]) => {
      const mediaQuery = window.matchMedia(query);
      mediaQuery.addListener(this.handleDeviceChange.bind(this, device));
      
      if (mediaQuery.matches) {
        this.handleDeviceChange(device, mediaQuery);
      }
    });
  }
  
  handleDeviceChange(device, mediaQuery) {
    if (mediaQuery.matches) {
      document.body.setAttribute('data-device', device);
      this.optimizeForDevice(device);
    }
  }
  
  optimizeForDevice(device) {
    // 根据设备类型优化资源加载
    const optimizations = {
      mobile: () => this.loadMobileAssets(),
      tablet: () => this.loadTabletAssets(),
      desktop: () => this.loadDesktopAssets()
    };
    
    optimizations[device]?.();
  }
}

Service Worker详解

Service Worker是PWA的核心技术,它是一个运行在浏览器后台的脚本,独立于网页主线程,为PWA提供了强大的后台处理能力。

Service Worker生命周期

Service Worker的生命周期包含多个关键阶段:

graph TD A[注册Service Worker] --> B[下载sw.js文件] B --> C[安装阶段 install] C --> D{安装成功?} D -->|成功| E[等待激活] D -->|失败| F[安装失败] E --> G[激活阶段 activate] G --> H[控制页面] H --> I[监听事件] I --> J[fetch/message/sync] J --> K[更新检测] K --> A

Service Worker注册和管理

javascript 复制代码
// Service Worker管理器
class ServiceWorkerManager {
  constructor() {
    this.registration = null;
    this.isUpdateAvailable = false;
  }
  
  async register() {
    if (!('serviceWorker' in navigator)) {
      console.warn('Service Worker不被支持');
      return false;
    }
    
    try {
      this.registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/'
      });
      
      console.log('Service Worker注册成功:', this.registration);
      
      // 监听更新
      this.registration.addEventListener('updatefound', () => {
        this.handleUpdate();
      });
      
      // 检查现有的Service Worker
      if (this.registration.active) {
        console.log('Service Worker已激活');
      }
      
      return true;
    } catch (error) {
      console.error('Service Worker注册失败:', error);
      return false;
    }
  }
  
  handleUpdate() {
    const newWorker = this.registration.installing;
    
    newWorker.addEventListener('statechange', () => {
      if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
        this.isUpdateAvailable = true;
        this.showUpdateNotification();
      }
    });
  }
  
  showUpdateNotification() {
    // 显示更新通知给用户
    const updateBanner = document.createElement('div');
    updateBanner.className = 'update-banner';
    updateBanner.innerHTML = `
      <div class="update-content">
        <span>新版本可用</span>
        <button onclick="this.parentNode.parentNode.remove(); window.location.reload();">
          更新
        </button>
      </div>
    `;
    document.body.appendChild(updateBanner);
  }
  
  async skipWaiting() {
    if (this.registration && this.registration.waiting) {
      this.registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    }
  }
}

Service Worker文件实现

javascript 复制代码
// sw.js - Service Worker主文件
const CACHE_NAME = 'pwa-cache-v1';
const STATIC_CACHE = 'static-cache-v1';
const DYNAMIC_CACHE = 'dynamic-cache-v1';

// 需要缓存的静态资源
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/css/main.css',
  '/js/app.js',
  '/images/logo.png',
  '/manifest.json'
];

// 安装事件
self.addEventListener('install', (event) => {
  console.log('Service Worker: 安装中...');
  
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then((cache) => {
        console.log('Service Worker: 缓存静态资源');
        return cache.addAll(STATIC_ASSETS);
      })
      .then(() => {
        console.log('Service Worker: 安装完成');
        return self.skipWaiting(); // 强制激活新的Service Worker
      })
  );
});

// 激活事件
self.addEventListener('activate', (event) => {
  console.log('Service Worker: 激活中...');
  
  event.waitUntil(
    caches.keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cacheName) => {
            if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
              console.log('Service Worker: 删除旧缓存', cacheName);
              return caches.delete(cacheName);
            }
          })
        );
      })
      .then(() => {
        console.log('Service Worker: 激活完成');
        return self.clients.claim(); // 立即控制所有页面
      })
  );
});

// 网络请求拦截
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') {
    return;
  }
  
  event.respondWith(
    caches.match(event.request)
      .then((cachedResponse) => {
        if (cachedResponse) {
          // 缓存命中,返回缓存
          return cachedResponse;
        }
        
        // 缓存未命中,发起网络请求
        return fetch(event.request)
          .then((response) => {
            // 检查响应是否有效
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            
            // 克隆响应(响应只能使用一次)
            const responseToCache = response.clone();
            
            // 动态缓存策略
            if (shouldCache(event.request)) {
              caches.open(DYNAMIC_CACHE)
                .then((cache) => {
                  cache.put(event.request, responseToCache);
                });
            }
            
            return response;
          });
      })
      .catch(() => {
        // 网络失败,返回离线页面
        if (event.request.destination === 'document') {
          return caches.match('/offline.html');
        }
      })
  );
});

// 判断是否应该缓存请求
function shouldCache(request) {
  const url = new URL(request.url);
  
  // 缓存同源资源
  if (url.origin === location.origin) {
    return true;
  }
  
  // 缓存CDN资源
  const cdnHosts = [
    'cdn.jsdelivr.net',
    'unpkg.com',
    'fonts.googleapis.com'
  ];
  
  return cdnHosts.includes(url.hostname);
}

// 监听来自主线程的消息
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

Web App Manifest配置

Web App Manifest是一个JSON文件,它提供了PWA应用的元数据信息,使应用能够被安装到设备主屏幕上,并提供类似原生应用的启动体验。

Manifest基本配置

json 复制代码
{
  "name": "我的PWA应用",
  "short_name": "PWA App",
  "description": "一个功能强大的渐进式Web应用",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait-primary",
  "theme_color": "#2196F3",
  "background_color": "#ffffff",
  "lang": "zh-CN",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "新建文档",
      "short_name": "新建",
      "description": "快速创建新文档",
      "url": "/new",
      "icons": [
        {
          "src": "/icons/new-document.png",
          "sizes": "96x96"
        }
      ]
    }
  ],
  "categories": ["productivity", "utilities"],
  "screenshots": [
    {
      "src": "/screenshots/desktop-1.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide",
      "label": "桌面端主界面"
    },
    {
      "src": "/screenshots/mobile-1.png",
      "sizes": "360x640",
      "type": "image/png",
      "form_factor": "narrow",
      "label": "移动端主界面"
    }
  ]
}

Manifest动态管理

javascript 复制代码
// Manifest管理器
class ManifestManager {
  constructor() {
    this.manifestElement = null;
    this.init();
  }
  
  init() {
    this.createManifestLink();
    this.generateManifest();
  }
  
  createManifestLink() {
    this.manifestElement = document.createElement('link');
    this.manifestElement.rel = 'manifest';
    this.manifestElement.href = '/manifest.json';
    document.head.appendChild(this.manifestElement);
  }
  
  generateManifest() {
    const manifest = {
      name: this.getAppName(),
      short_name: this.getShortName(),
      description: this.getDescription(),
      start_url: this.getStartUrl(),
      scope: this.getScope(),
      display: this.getDisplayMode(),
      orientation: this.getOrientation(),
      theme_color: this.getThemeColor(),
      background_color: this.getBackgroundColor(),
      icons: this.generateIcons(),
      shortcuts: this.generateShortcuts()
    };
    
    // 动态创建manifest
    this.saveManifest(manifest);
  }
  
  getAppName() {
    return document.title || 'PWA应用';
  }
  
  getShortName() {
    const metaAppName = document.querySelector('meta[name="application-name"]');
    return metaAppName ? metaAppName.content : 'PWA';
  }
  
  getDescription() {
    const metaDescription = document.querySelector('meta[name="description"]');
    return metaDescription ? metaDescription.content : '渐进式Web应用';
  }
  
  getStartUrl() {
    return window.location.pathname || '/';
  }
  
  getScope() {
    return '/';
  }
  
  getDisplayMode() {
    // 根据屏幕尺寸自适应
    const isMobile = window.innerWidth <= 768;
    return isMobile ? 'standalone' : 'minimal-ui';
  }
  
  getOrientation() {
    const isLandscape = window.innerWidth > window.innerHeight;
    return isLandscape ? 'landscape' : 'portrait';
  }
  
  getThemeColor() {
    const metaTheme = document.querySelector('meta[name="theme-color"]');
    return metaTheme ? metaTheme.content : '#2196F3';
  }
  
  getBackgroundColor() {
    const computedStyle = getComputedStyle(document.body);
    return computedStyle.backgroundColor || '#ffffff';
  }
  
  generateIcons() {
    const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
    return sizes.map(size => ({
      src: `/icons/icon-${size}x${size}.png`,
      sizes: `${size}x${size}`,
      type: 'image/png',
      purpose: size >= 512 ? 'any maskable' : 'any'
    }));
  }
  
  generateShortcuts() {
    // 基于路由生成快捷方式
    const routes = [
      { name: '首页', url: '/', icon: 'home' },
      { name: '设置', url: '/settings', icon: 'settings' },
      { name: '关于', url: '/about', icon: 'info' }
    ];
    
    return routes.map(route => ({
      name: route.name,
      short_name: route.name,
      description: `跳转到${route.name}`,
      url: route.url,
      icons: [
        {
          src: `/icons/${route.icon}.png`,
          sizes: '96x96'
        }
      ]
    }));
  }
  
  async saveManifest(manifest) {
    try {
      // 如果支持,可以动态更新manifest
      if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
        navigator.serviceWorker.controller.postMessage({
          type: 'UPDATE_MANIFEST',
          manifest: manifest
        });
      }
      
      // 或者通过Blob URL临时创建
      const blob = new Blob([JSON.stringify(manifest, null, 2)], {
        type: 'application/json'
      });
      const url = URL.createObjectURL(blob);
      this.manifestElement.href = url;
      
    } catch (error) {
      console.error('保存manifest失败:', error);
    }
  }
  
  // 检测安装状态
  checkInstallation() {
    return new Promise((resolve) => {
      if (window.matchMedia('(display-mode: standalone)').matches) {
        resolve({ installed: true, source: 'standalone' });
      } else if (window.navigator.standalone === true) {
        resolve({ installed: true, source: 'ios' });
      } else if (document.referrer.includes('android-app://')) {
        resolve({ installed: true, source: 'android' });
      } else {
        resolve({ installed: false });
      }
    });
  }
}

离线功能开发

离线功能是PWA的核心特性之一,让用户在网络不可用时仍能使用应用的基本功能。

网络状态检测

javascript 复制代码
// 网络状态管理器
class NetworkManager {
  constructor() {
    this.isOnline = navigator.onLine;
    this.connectionType = this.getConnectionType();
    this.setupEventListeners();
    this.notifyCallbacks = [];
  }
  
  setupEventListeners() {
    window.addEventListener('online', () => {
      this.isOnline = true;
      this.notifyStatusChange('online');
    });
    
    window.addEventListener('offline', () => {
      this.isOnline = false;
      this.notifyStatusChange('offline');
    });
    
    // 监听连接类型变化
    if ('connection' in navigator) {
      navigator.connection.addEventListener('change', () => {
        this.connectionType = this.getConnectionType();
        this.notifyStatusChange('connection-change');
      });
    }
  }
  
  getConnectionType() {
    if ('connection' in navigator) {
      return {
        type: navigator.connection.effectiveType,
        downlink: navigator.connection.downlink,
        rtt: navigator.connection.rtt,
        saveData: navigator.connection.saveData
      };
    }
    return null;
  }
  
  onStatusChange(callback) {
    this.notifyCallbacks.push(callback);
  }
  
  notifyStatusChange(type) {
    this.notifyCallbacks.forEach(callback => {
      callback({
        type,
        isOnline: this.isOnline,
        connectionType: this.connectionType
      });
    });
  }
  
  // 智能网络检测
  async checkConnectivity() {
    if (!this.isOnline) {
      return false;
    }
    
    try {
      // 尝试请求一个小文件来确认网络连接
      const response = await fetch('/ping.txt', {
        method: 'HEAD',
        cache: 'no-cache'
      });
      return response.ok;
    } catch (error) {
      return false;
    }
  }
}

离线数据同步

javascript 复制代码
// 离线数据同步管理器
class OfflineSyncManager {
  constructor() {
    this.dbName = 'PWA_OfflineDB';
    this.dbVersion = 1;
    this.db = null;
    this.syncQueue = [];
    this.init();
  }
  
  async init() {
    await this.openDatabase();
    await this.processQueuedRequests();
  }
  
  openDatabase() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.dbVersion);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        
        // 创建离线数据存储
        if (!db.objectStoreNames.contains('offlineData')) {
          const store = db.createObjectStore('offlineData', { keyPath: 'id', autoIncrement: true });
          store.createIndex('timestamp', 'timestamp');
          store.createIndex('type', 'type');
        }
        
        // 创建同步队列存储
        if (!db.objectStoreNames.contains('syncQueue')) {
          const syncStore = db.createObjectStore('syncQueue', { keyPath: 'id', autoIncrement: true });
          syncStore.createIndex('timestamp', 'timestamp');
        }
      };
    });
  }
  
  // 存储离线数据
  async storeOfflineData(type, data) {
    try {
      const transaction = this.db.transaction(['offlineData'], 'readwrite');
      const store = transaction.objectStore('offlineData');
      
      const record = {
        type,
        data,
        timestamp: Date.now(),
        synced: false
      };
      
      await new Promise((resolve, reject) => {
        const request = store.add(record);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      console.log(`离线数据已存储: ${type}`);
    } catch (error) {
      console.error('存储离线数据失败:', error);
    }
  }
  
  // 获取离线数据
  async getOfflineData(type) {
    try {
      const transaction = this.db.transaction(['offlineData'], 'readonly');
      const store = transaction.objectStore('offlineData');
      const index = store.index('type');
      
      return new Promise((resolve, reject) => {
        const request = index.getAll(type);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
    } catch (error) {
      console.error('获取离线数据失败:', error);
      return [];
    }
  }
  
  // 添加到同步队列
  async addToSyncQueue(action, data) {
    try {
      const transaction = this.db.transaction(['syncQueue'], 'readwrite');
      const store = transaction.objectStore('syncQueue');
      
      const queueItem = {
        action,
        data,
        timestamp: Date.now(),
        retryCount: 0
      };
      
      await new Promise((resolve, reject) => {
        const request = store.add(queueItem);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      console.log(`已添加到同步队列: ${action}`);
    } catch (error) {
      console.error('添加到同步队列失败:', error);
    }
  }
  
  // 处理同步队列
  async processQueuedRequests() {
    if (!navigator.onLine) return;
    
    try {
      const transaction = this.db.transaction(['syncQueue'], 'readwrite');
      const store = transaction.objectStore('syncQueue');
      
      const queuedItems = await new Promise((resolve, reject) => {
        const request = store.getAll();
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      for (const item of queuedItems) {
        try {
          await this.syncItem(item);
          // 同步成功,从队列中删除
          await new Promise((resolve, reject) => {
            const deleteRequest = store.delete(item.id);
            deleteRequest.onsuccess = () => resolve();
            deleteRequest.onerror = () => reject(deleteRequest.error);
          });
        } catch (error) {
          console.error(`同步失败: ${item.action}`, error);
          // 增加重试次数
          item.retryCount++;
          if (item.retryCount < 3) {
            store.put(item);
          } else {
            // 重试次数过多,删除
            store.delete(item.id);
          }
        }
      }
    } catch (error) {
      console.error('处理同步队列失败:', error);
    }
  }
  
  async syncItem(item) {
    const response = await fetch(`/api/${item.action}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(item.data)
    });
    
    if (!response.ok) {
      throw new Error(`同步失败: ${response.status}`);
    }
    
    return response.json();
  }
}

离线页面实现

javascript 复制代码
// 离线页面管理
class OfflinePageManager {
  constructor() {
    this.offlinePagePath = '/offline.html';
    this.fallbackContent = this.createFallbackContent();
  }
  
  createFallbackContent() {
    return `
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>离线模式</title>
        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
          }
          .offline-container {
            background: white;
            border-radius: 12px;
            padding: 40px;
            text-align: center;
            box-shadow: 0 10px 30px rgba(0,0,0,0.2);
            max-width: 400px;
          }
          .offline-icon {
            font-size: 48px;
            margin-bottom: 20px;
          }
          .offline-title {
            font-size: 24px;
            font-weight: 600;
            color: #333;
            margin-bottom: 10px;
          }
          .offline-message {
            color: #666;
            line-height: 1.5;
            margin-bottom: 30px;
          }
          .retry-button {
            background: #667eea;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 6px;
            font-size: 16px;
            cursor: pointer;
            transition: background 0.3s;
          }
          .retry-button:hover {
            background: #5a6fd8;
          }
        </style>
      </head>
      <body>
        <div class="offline-container">
          <div class="offline-icon">📡</div>
          <h1 class="offline-title">您处于离线状态</h1>
          <p class="offline-message">
            网络连接似乎出现了问题。请检查您的网络连接,或稍后重试。
          </p>
          <button class="retry-button" onclick="window.location.reload()">
            重新尝试
          </button>
        </div>
        
        <script>
          // 监听网络状态变化
          window.addEventListener('online', () => {
            window.location.reload();
          });
        </script>
      </body>
      </html>
    `;
  }
  
  // 在Service Worker中使用
  async handleOfflineRequest(request) {
    try {
      // 尝试从缓存获取离线页面
      const cache = await caches.open('offline-pages');
      const offlinePage = await cache.match(this.offlinePagePath);
      
      if (offlinePage) {
        return offlinePage;
      }
      
      // 如果没有缓存的离线页面,返回备用内容
      return new Response(this.fallbackContent, {
        headers: {
          'Content-Type': 'text/html; charset=utf-8'
        }
      });
    } catch (error) {
      console.error('处理离线请求失败:', error);
      return new Response('离线模式', {
        status: 503,
        statusText: 'Service Unavailable'
      });
    }
  }
}

推送通知机制

推送通知让PWA能够在后台与用户保持连接,提升用户参与度。

推送通知权限管理

javascript 复制代码
// 推送通知管理器
class PushNotificationManager {
  constructor() {
    this.registration = null;
    this.subscription = null;
    this.vapidPublicKey = 'YOUR_VAPID_PUBLIC_KEY';
  }
  
  async init() {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
      console.warn('推送通知不被支持');
      return false;
    }
    
    try {
      this.registration = await navigator.serviceWorker.ready;
      return true;
    } catch (error) {
      console.error('推送通知初始化失败:', error);
      return false;
    }
  }
  
  // 请求通知权限
  async requestPermission() {
    if (!('Notification' in window)) {
      console.warn('通知不被支持');
      return false;
    }
    
    let permission = Notification.permission;
    
    if (permission === 'default') {
      permission = await Notification.requestPermission();
    }
    
    if (permission === 'granted') {
      console.log('通知权限已获得');
      return true;
    } else {
      console.log('通知权限被拒绝');
      return false;
    }
  }
  
  // 订阅推送服务
  async subscribeToPush() {
    try {
      const subscription = await this.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
      });
      
      this.subscription = subscription;
      
      // 将订阅信息发送到服务器
      await this.sendSubscriptionToServer(subscription);
      
      console.log('推送订阅成功');
      return subscription;
    } catch (error) {
      console.error('推送订阅失败:', error);
      return null;
    }
  }
  
  // 取消推送订阅
  async unsubscribeFromPush() {
    try {
      if (this.subscription) {
        await this.subscription.unsubscribe();
        this.subscription = null;
        console.log('推送订阅已取消');
      }
    } catch (error) {
      console.error('取消推送订阅失败:', error);
    }
  }
  
  // 发送订阅信息到服务器
  async sendSubscriptionToServer(subscription) {
    try {
      const response = await fetch('/api/push/subscribe', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          subscription: subscription.toJSON(),
          timestamp: Date.now()
        })
      });
      
      if (!response.ok) {
        throw new Error(`订阅失败: ${response.status}`);
      }
    } catch (error) {
      console.error('发送订阅信息失败:', error);
    }
  }
  
  // 显示本地通知
  async showLocalNotification(title, options = {}) {
    if (!this.registration) {
      console.error('Service Worker未注册');
      return;
    }
    
    const defaultOptions = {
      icon: '/icons/icon-192x192.png',
      badge: '/icons/badge-72x72.png',
      tag: 'default',
      requireInteraction: false,
      data: {
        timestamp: Date.now()
      }
    };
    
    const notificationOptions = { ...defaultOptions, ...options };
    
    try {
      await this.registration.showNotification(title, notificationOptions);
    } catch (error) {
      console.error('显示通知失败:', error);
    }
  }
  
  // 工具函数:Base64转Uint8Array
  urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, '+')
      .replace(/_/g, '/');
    
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    
    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    
    return outputArray;
  }
}

性能优化实践

PWA性能优化涉及多个层面,从资源加载到运行时优化。

资源预加载策略

javascript 复制代码
// 资源预加载管理器
class ResourcePreloader {
  constructor() {
    this.criticalResources = [];
    this.prefetchResources = [];
    this.observedElements = new Set();
  }
  
  // 预加载关键资源
  preloadCriticalResources(resources) {
    resources.forEach(resource => {
      const link = document.createElement('link');
      link.rel = 'preload';
      link.href = resource.url;
      link.as = resource.type;
      
      if (resource.type === 'font') {
        link.crossOrigin = 'anonymous';
      }
      
      document.head.appendChild(link);
    });
  }
  
  // 智能预获取
  intelligentPrefetch() {
    // 基于用户行为预测下一步需要的资源
    this.observeUserInteractions();
    this.prefetchOnHover();
    this.prefetchOnViewport();
  }
  
  observeUserInteractions() {
    document.addEventListener('mouseover', (event) => {
      const link = event.target.closest('a[href]');
      if (link && !this.observedElements.has(link.href)) {
        this.observedElements.add(link.href);
        this.prefetchResource(link.href);
      }
    });
  }
  
  prefetchOnHover() {
    document.addEventListener('mouseenter', (event) => {
      const target = event.target;
      if (target.dataset.prefetch && !this.observedElements.has(target.dataset.prefetch)) {
        this.observedElements.add(target.dataset.prefetch);
        this.prefetchResource(target.dataset.prefetch);
      }
    }, true);
  }
  
  prefetchOnViewport() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const element = entry.target;
          const prefetchUrl = element.dataset.prefetch;
          
          if (prefetchUrl && !this.observedElements.has(prefetchUrl)) {
            this.observedElements.add(prefetchUrl);
            this.prefetchResource(prefetchUrl);
          }
        }
      });
    }, { threshold: 0.1 });
    
    document.querySelectorAll('[data-prefetch]').forEach(element => {
      observer.observe(element);
    });
  }
  
  prefetchResource(url) {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = url;
    document.head.appendChild(link);
  }
}

最佳实践总结

javascript 复制代码
// PWA最佳实践检查器
class PWABestPracticesChecker {
  constructor() {
    this.checks = [
      { name: 'HTTPS', check: this.checkHTTPS },
      { name: 'Service Worker', check: this.checkServiceWorker },
      { name: 'Web App Manifest', check: this.checkManifest },
      { name: 'Responsive Design', check: this.checkResponsive },
      { name: 'Performance', check: this.checkPerformance }
    ];
  }
  
  async runAllChecks() {
    const results = {};
    
    for (const check of this.checks) {
      try {
        results[check.name] = await check.check();
      } catch (error) {
        results[check.name] = { passed: false, error: error.message };
      }
    }
    
    return results;
  }
  
  checkHTTPS() {
    return {
      passed: location.protocol === 'https:' || location.hostname === 'localhost',
      message: location.protocol === 'https:' ? 'HTTPS已启用' : '建议使用HTTPS'
    };
  }
  
  async checkServiceWorker() {
    const hasServiceWorker = 'serviceWorker' in navigator;
    const isRegistered = hasServiceWorker && (await navigator.serviceWorker.getRegistration()) !== undefined;
    
    return {
      passed: isRegistered,
      message: isRegistered ? 'Service Worker已注册' : '未检测到Service Worker'
    };
  }
  
  checkManifest() {
    const manifestLink = document.querySelector('link[rel="manifest"]');
    
    return {
      passed: !!manifestLink,
      message: manifestLink ? 'Web App Manifest已配置' : '缺少Web App Manifest'
    };
  }
  
  checkResponsive() {
    const viewportMeta = document.querySelector('meta[name="viewport"]');
    const hasViewport = !!viewportMeta;
    const hasResponsiveContent = viewportMeta && viewportMeta.content.includes('width=device-width');
    
    return {
      passed: hasViewport && hasResponsiveContent,
      message: hasViewport && hasResponsiveContent ? '响应式设计已配置' : '建议优化响应式设计'
    };
  }
  
  async checkPerformance() {
    if ('performance' in window && 'navigation' in performance) {
      const navTiming = performance.getEntriesByType('navigation')[0];
      const loadTime = navTiming.loadEventEnd - navTiming.fetchStart;
      
      return {
        passed: loadTime < 3000,
        message: `页面加载时间: ${loadTime}ms`,
        value: loadTime
      };
    }
    
    return {
      passed: false,
      message: '无法检测性能指标'
    };
  }
}

// 使用示例
const checker = new PWABestPracticesChecker();
checker.runAllChecks().then(results => {
  console.log('PWA最佳实践检查结果:', results);
});

总结

PWA技术通过Service Worker、Web App Manifest、推送通知等核心技术,为Web应用带来了接近原生应用的用户体验。合理的缓存策略、完善的离线功能、以及性能优化措施,共同构建了现代化的Web应用解决方案。

通过本文的详细介绍和代码示例,您可以:

  1. 掌握PWA核心概念:理解渐进式增强的设计理念
  2. 实现Service Worker:管理缓存、处理网络请求、支持离线功能
  3. 配置Web App Manifest:让应用可安装,提供原生体验
  4. 优化缓存策略:根据资源类型选择合适的缓存方案
  5. 开发离线功能:确保应用在网络不稳定时正常工作
  6. 集成推送通知:提升用户参与度和留存率
  7. 性能优化实践:通过预加载、智能缓存等技术提升性能

PWA代表了Web应用的未来发展方向,是构建高质量、高性能Web应用的重要技术栈。


缓存策略实现

PWA的缓存策略是性能优化的核心,不同的资源类型需要采用不同的缓存策略以达到最佳的用户体验。

常见缓存策略

graph TD A[请求发起] --> B{缓存策略类型} B -->|Cache First| C[检查缓存] C -->|命中| D[返回缓存] C -->|未命中| E[网络请求] E --> F[缓存响应] F --> G[返回响应] B -->|Network First| H[网络请求] H -->|成功| I[缓存响应] I --> J[返回响应] H -->|失败| K[检查缓存] K -->|有缓存| L[返回缓存] K -->|无缓存| M[返回错误] B -->|Stale While Revalidate| N[返回缓存] N --> O[后台更新] O --> P[更新缓存]

缓存策略实现

javascript 复制代码
// 缓存策略管理器
class CacheStrategies {
  constructor() {
    this.strategies = {
      cacheFirst: this.cacheFirst.bind(this),
      networkFirst: this.networkFirst.bind(this),
      staleWhileRevalidate: this.staleWhileRevalidate.bind(this),
      cacheOnly: this.cacheOnly.bind(this),
      networkOnly: this.networkOnly.bind(this)
    };
  }
  
  // 缓存优先策略 - 适用于不经常变化的资源
  async cacheFirst(request, cacheName = 'cache-first') {
    try {
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      if (cachedResponse) {
        return cachedResponse;
      }
      
      const networkResponse = await fetch(request);
      if (networkResponse.ok) {
        cache.put(request, networkResponse.clone());
      }
      
      return networkResponse;
    } catch (error) {
      console.error('Cache First策略失败:', error);
      throw error;
    }
  }
  
  // 网络优先策略 - 适用于需要最新数据的API
  async networkFirst(request, cacheName = 'network-first', timeout = 3000) {
    try {
      const cache = await caches.open(cacheName);
      
      // 设置网络请求超时
      const networkPromise = this.fetchWithTimeout(request, timeout);
      
      try {
        const networkResponse = await networkPromise;
        if (networkResponse.ok) {
          cache.put(request, networkResponse.clone());
        }
        return networkResponse;
      } catch (networkError) {
        console.warn('网络请求失败,尝试缓存:', networkError);
        const cachedResponse = await cache.match(request);
        
        if (cachedResponse) {
          return cachedResponse;
        }
        
        throw networkError;
      }
    } catch (error) {
      console.error('Network First策略失败:', error);
      throw error;
    }
  }
  
  // 先返回缓存,同时更新策略 - 适用于频繁更新但可容忍旧数据的资源
  async staleWhileRevalidate(request, cacheName = 'stale-while-revalidate') {
    try {
      const cache = await caches.open(cacheName);
      const cachedResponse = await cache.match(request);
      
      // 后台更新缓存
      const updateCache = async () => {
        try {
          const networkResponse = await fetch(request);
          if (networkResponse.ok) {
            await cache.put(request, networkResponse.clone());
          }
        } catch (error) {
          console.warn('后台更新缓存失败:', error);
        }
      };
      
      updateCache(); // 不等待
      
      if (cachedResponse) {
        return cachedResponse;
      }
      
      // 如果没有缓存,等待网络响应
      return await fetch(request);
    } catch (error) {
      console.error('Stale While Revalidate策略失败:', error);
      throw error;
    }
  }
  
  // 带超时的fetch
  fetchWithTimeout(request, timeout) {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(`请求超时: ${timeout}ms`));
      }, timeout);
      
      fetch(request)
        .then(response => {
          clearTimeout(timeoutId);
          resolve(response);
        })
        .catch(error => {
          clearTimeout(timeoutId);
          reject(error);
        });
    });
  }
  
  // 根据请求类型选择策略
  getStrategy(request) {
    const url = new URL(request.url);
    const pathname = url.pathname;
    const fileExtension = pathname.split('.').pop().toLowerCase();
    
    // 静态资源使用缓存优先
    if (['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'woff', 'woff2'].includes(fileExtension)) {
      return 'cacheFirst';
    }
    
    // API请求使用网络优先
    if (pathname.startsWith('/api/') || pathname.startsWith('/graphql')) {
      return 'networkFirst';
    }
    
    // HTML文档使用先返回缓存再更新
    if (request.destination === 'document') {
      return 'staleWhileRevalidate';
    }
    
    // 默认策略
    return 'networkFirst';
  }
}

相关推荐
颜酱23 分钟前
使用useReducer和Context进行React中的页面内部数据共享
前端·javascript·react.js
Data_Adventure30 分钟前
大屏应用中的动态缩放适配工具
前端
wenke00a37 分钟前
C函数实现strcopy strcat strcmp strstr
c语言·前端
AiMuo42 分钟前
FLJ性能战争战报:完全抛弃 Next.js 打包链路,战术背断性选择 esbuild 自建 Worker 脚本经验
前端·性能优化
Lefan43 分钟前
解决重复请求与取消未响应请求
前端
混水的鱼44 分钟前
React + antd 实现文件预览与下载组件(支持图片、PDF、Office)
前端·react.js
程序员嘉逸1 小时前
🎨 CSS属性完全指南:从入门到精通的样式秘籍
前端
Jackson_Mseven1 小时前
🧺 Monorepo 是什么?一锅端的大杂烩式开发幸福生活
前端·javascript·架构
我想说一句1 小时前
JavaScript数组:轻松愉快地玩透它
前端·javascript
binggg1 小时前
AI 编程不靠运气,Kiro Spec 工作流复刻全攻略
前端·claude·cursor