HTML5 PWA开发全攻略 | 离线应用与性能优化实战

2025-08-14 0 973

一、PWA技术全景

本教程将基于原生HTML5技术构建一个完整的渐进式Web应用(PWA),实现原生应用般的用户体验。

核心技术:

  • Service Worker离线缓存
  • Web App Manifest
  • 客户端存储方案
  • 推送通知API
  • 性能优化策略

实战案例:

  1. 离线优先的天气应用
  2. 后台同步数据
  3. 添加到主屏幕
  4. 网络状态感知
  5. 资源预缓存

二、项目基础架构

1. 项目目录结构

pwa-weather-app/
├── index.html          # 应用入口
├── manifest.json       # Web应用清单
├── service-worker.js   # Service Worker脚本
├── assets/
│   ├── css/app.css     # 样式文件
│   ├── js/app.js       # 主JavaScript文件
│   └── images/         # 应用图标等资源
└── data/               # 本地数据存储

2. 基础HTML结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天气PWA应用</title>
    <meta name="theme-color" content="#2196F3">
    <meta name="description" content="一个离线可用的天气应用">
    <link rel="manifest" href="/manifest.json" rel="external nofollow" >
    <link rel="icon" href="/assets/images/icon-192.png" rel="external nofollow" >
    <link rel="stylesheet" href="/assets/css/app.css" rel="external nofollow"  rel="external nofollow" >
</head>
<body>
    <div class="app-container">
        <header class="app-header">
            <h1>天气PWA</h1>
            <button id="refresh-btn" aria-label="刷新数据"></button>
        </header>
        
        <main class="weather-content">
            <div class="current-weather">
                <!-- 动态内容将通过JavaScript填充 -->
            </div>
        </main>
        
        <div class="offline-status" hidden>
            您当前处于离线状态,显示的是缓存数据
        </div>
    </div>
    
    <script src="/assets/js/app.js" type="module"></script>
</body>
</html>

三、核心功能实现

1. Web应用清单(manifest.json)

{
  "name": "天气PWA",
  "short_name": "天气",
  "description": "一个离线可用的天气应用",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#2196F3",
  "background_color": "#ffffff",
  "icons": [
    {
      "src": "/assets/images/icon-72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/assets/images/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/assets/images/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "related_applications": [],
  "prefer_related_applications": false
}

2. Service Worker注册

// assets/js/app.js
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            const registration = await navigator.serviceWorker.register('/service-worker.js');
            console.log('ServiceWorker注册成功:', registration.scope);
            
            // 检查更新
            registration.addEventListener('updatefound', () => {
                const newWorker = registration.installing;
                newWorker.addEventListener('statechange', () => {
                    if (newWorker.state === 'installed') {
                        if (navigator.serviceWorker.controller) {
                            // 新版本可用
                            showUpdateNotification();
                        }
                    }
                });
            });
        } catch (error) {
            console.error('ServiceWorker注册失败:', error);
        }
    });
}

function showUpdateNotification() {
    const notification = document.createElement('div');
    notification.className = 'update-notification';
    notification.innerHTML = `
        <p>新版本可用,点击刷新以更新应用</p>
        <button id="refresh-app">刷新</button>
    `;
    
    document.body.appendChild(notification);
    
    document.getElementById('refresh-app').addEventListener('click', () => {
        window.location.reload();
    });
}

四、Service Worker实现

1. 缓存策略实现

// service-worker.js
const CACHE_NAME = 'weather-app-v1';
const OFFLINE_URL = '/offline.html';
const PRECACHE_URLS = [
    '/',
    '/index.html',
    '/assets/css/app.css',
    '/assets/js/app.js',
    '/assets/images/icon-192.png',
    OFFLINE_URL
];

// 安装阶段 - 预缓存关键资源
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(PRECACHE_URLS))
            .then(() => self.skipWaiting())
    );
});

// 激活阶段 - 清理旧缓存
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(() => self.clients.claim())
    );
});

// 请求拦截 - 缓存优先策略
self.addEventListener('fetch', event => {
    if (event.request.mode === 'navigate') {
        event.respondWith(
            fetch(event.request)
                .catch(() => caches.match(OFFLINE_URL))
        );
    } else {
        event.respondWith(
            caches.match(event.request)
                .then(response => response || fetch(event.request))
        );
    }
});

2. 后台数据同步

// 注册后台同步
async function registerBackgroundSync() {
    if ('SyncManager' in window) {
        try {
            const registration = await navigator.serviceWorker.ready;
            await registration.sync.register('sync-weather-data');
            console.log('后台同步已注册');
        } catch (error) {
            console.error('后台同步注册失败:', error);
        }
    }
}

// 在Service Worker中处理同步事件
self.addEventListener('sync', event => {
    if (event.tag === 'sync-weather-data') {
        event.waitUntil(
            syncWeatherData()
                .then(() => showNotification('数据已同步'))
                .catch(error => console.error('同步失败:', error))
        );
    }
});

async function syncWeatherData() {
    const cache = await caches.open('weather-data');
    const response = await fetch('https://api.weather.com/data');
    await cache.put('weather-data', response.clone());
    return response;
}

五、客户端存储方案

1. IndexedDB数据存储

// assets/js/db.js
const DB_NAME = 'WeatherDB';
const DB_VERSION = 1;
const STORE_NAME = 'locations';

let db;

export function openDB() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);
        
        request.onerror = () => reject(request.error);
        request.onsuccess = () => {
            db = request.result;
            resolve(db);
        };
        
        request.onupgradeneeded = event => {
            const db = event.target.result;
            if (!db.objectStoreNames.contains(STORE_NAME)) {
                const store = db.createObjectStore(STORE_NAME, {
                    keyPath: 'id',
                    autoIncrement: true
                });
                store.createIndex('name', 'name', { unique: false });
            }
        };
    });
}

export function addLocation(location) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(STORE_NAME, 'readwrite');
        const store = transaction.objectStore(STORE_NAME);
        const request = store.add(location);
        
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
    });
}

export function getLocations() {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(STORE_NAME, 'readonly');
        const store = transaction.objectStore(STORE_NAME);
        const request = store.getAll();
        
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result);
    });
}

2. 网络状态检测

// assets/js/network.js
export function initNetworkStatus() {
    const statusElement = document.querySelector('.offline-status');
    
    function updateNetworkStatus() {
        if (navigator.onLine) {
            statusElement.hidden = true;
        } else {
            statusElement.hidden = false;
        }
    }
    
    // 初始检测
    updateNetworkStatus();
    
    // 监听网络变化
    window.addEventListener('online', updateNetworkStatus);
    window.addEventListener('offline', updateNetworkStatus);
    
    // 返回当前状态
    return {
        isOnline: () => navigator.onLine,
        addListener: (callback) => {
            window.addEventListener('online', callback);
            window.addEventListener('offline', callback);
        }
    };
}

六、推送通知实现

1. 通知权限请求

// assets/js/notifications.js
export async function requestNotificationPermission() {
    if (!('Notification' in window)) {
        console.log('当前浏览器不支持通知');
        return false;
    }
    
    if (Notification.permission === 'granted') {
        return true;
    }
    
    const permission = await Notification.requestPermission();
    return permission === 'granted';
}

export function showNotification(title, options = {}) {
    if (!('Notification' in window) || Notification.permission !== 'granted') {
        return;
    }
    
    const notification = new Notification(title, {
        icon: '/assets/images/icon-192.png',
        badge: '/assets/images/icon-72.png',
        ...options
    });
    
    notification.onclick = () => {
        window.focus();
        notification.close();
    };
    
    return notification;
}

2. 推送事件处理

// 在Service Worker中
self.addEventListener('push', event => {
    const data = event.data.json();
    
    const options = {
        body: data.body,
        icon: '/assets/images/icon-192.png',
        badge: '/assets/images/icon-72.png',
        data: {
            url: data.url
        }
    };
    
    event.waitUntil(
        self.registration.showNotification(data.title, options)
    );
});

self.addEventListener('notificationclick', event => {
    event.notification.close();
    
    event.waitUntil(
        clients.matchAll({
            type: 'window'
        }).then(clientList => {
            if (clientList.length > 0) {
                return clientList[0].focus();
            }
            return clients.openWindow(event.notification.data.url);
        })
    );
});

七、性能优化策略

1. 资源预加载

<!-- 在HTML头部预加载关键资源 -->
<link rel="preload" href="/assets/css/app.css" rel="external nofollow"  rel="external nofollow"  as="style">
<link rel="preload" href="/assets/js/app.js" rel="external nofollow"  as="script">
<link rel="preload" href="/assets/images/weather-icons.png" rel="external nofollow"  as="image">

<!-- 预连接API端点 -->
<link rel="preconnect" href="https://api.weather.com" rel="external nofollow"  rel="external nofollow" >
<link rel="dns-prefetch" href="https://api.weather.com" rel="external nofollow"  rel="external nofollow" >

// 动态加载非关键资源
function loadLazyResources() {
    const lazyScripts = [
        '/assets/js/analytics.js',
        '/assets/js/map.js'
    ];
    
    lazyScripts.forEach(src => {
        const script = document.createElement('script');
        script.src = src;
        document.body.appendChild(script);
    });
}

// 在页面主要内容加载完成后执行
window.addEventListener('DOMContentLoaded', loadLazyResources);

2. 性能监测

// 使用Performance API监测关键指标
function measurePerformance() {
    window.addEventListener('load', () => {
        setTimeout(() => {
            const timing = performance.timing;
            const metrics = {
                dns: timing.domainLookupEnd - timing.domainLookupStart,
                tcp: timing.connectEnd - timing.connectStart,
                ttfb: timing.responseStart - timing.requestStart,
                pageLoad: timing.loadEventEnd - timing.navigationStart,
                domReady: timing.domComplete - timing.domLoading
            };
            
            console.log('性能指标:', metrics);
            
            // 发送到分析服务器
            if ('sendBeacon' in navigator) {
                navigator.sendBeacon('/analytics', JSON.stringify(metrics));
            }
        }, 0);
    });
}

// 使用Web Vitals监测核心用户体验指标
import {getCLS, getFID, getLCP} from 'web-vitals';

function reportWebVitals() {
    getCLS(console.log);
    getFID(console.log);
    getLCP(console.log);
    
    // 或者发送到分析服务器
    function sendToAnalytics(metric) {
        const body = JSON.stringify(metric);
        navigator.sendBeacon('/analytics', body);
    }
    
    getCLS(sendToAnalytics);
    getFID(sendToAnalytics);
    getLCP(sendToAnalytics);
}

八、总结与扩展

本教程构建了一个完整的PWA应用:

  1. 实现了离线优先策略
  2. 开发了Service Worker缓存
  3. 配置了Web应用清单
  4. 添加了推送通知
  5. 优化了性能表现

扩展方向:

  • Web组件集成
  • Web Share API
  • 支付请求API
  • Web Assembly优化

完整项目代码已开源:https://github.com/example/pwa-weather-app

HTML5 PWA开发全攻略 | 离线应用与性能优化实战
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 html HTML5 PWA开发全攻略 | 离线应用与性能优化实战 https://www.taomawang.com/web/html/825.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务