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';
  }
}

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax