PWA离线应用开发实战:Service Worker缓存策略与后台同步

PWA技术栈与离线应用核心机制

渐进式Web应用(PWA)是一套结合Web技术与原生应用体验的技术方案,核心由三部分组成:Service Worker(离线缓存与后台任务)、Web App Manifest(安装元数据)和Push API(消息推送)。前端开发中PWA的最大价值在于离线可用性——用户在断网或弱网环境下仍能访问已缓存页面,网络恢复后自动同步数据。Web性能优化的常规手段如CDN加速、资源压缩在弱网场景下效果有限,Service Worker的缓存拦截机制在HTTP层之下工作,能在请求发出之前直接返回缓存内容,实现毫秒级响应。

Service Worker注册与生命周期管理

Service Worker运行在独立线程中,不能直接操作DOM,通过postMessage与主线程通信。其生命周期分为安装(install)、激活(activate)和运行(fetch/push/sync)三个阶段。

// service-worker.js 注册逻辑(主线程)
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            const reg = await navigator.serviceWorker.register('/sw.js', {
                scope: '/',
                updateViaCache: 'none'
            });
            
            reg.addEventListener('updatefound', () => {
                const newWorker = reg.installing;
                newWorker.addEventListener('statechange', () => {
                    if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                        showUpdateNotification();
                    }
                });
            });
        } catch (err) {
            console.error('SW注册失败:', err);
        }
    });
}

缓存策略设计与实现

不同资源类型需要不同的缓存策略。HTML文档采用Network First(网络优先),确保用户看到最新内容;CSS/JS等静态资源采用Cache First(缓存优先),更新通过版本号控制;API请求采用Stale-While-Revalidate(先缓存后更新),平衡响应速度与数据新鲜度。

// sw.js - Service Worker 核心
const CACHE_VERSION = 'v3';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const RUNTIME_CACHE = `runtime-${CACHE_VERSION}`;
const OFFLINE_URL = '/offline.html';

const PRECACHE_URLS = [
    '/', '/offline.html', '/css/app.min.css', '/js/app.min.js', '/images/logo.svg'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(STATIC_CACHE)
            .then(cache => cache.addAll(PRECACHE_URLS))
            .then(() => self.skipWaiting())
    );
});

self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys()
            .then(keys => Promise.all(
                keys
                    .filter(key => key !== STATIC_CACHE && key !== RUNTIME_CACHE)
                    .map(key => caches.delete(key))
            ))
            .then(() => self.clients.claim())
    );
});

self.addEventListener('fetch', (event) => {
    const { request } = event;
    const url = new URL(request.url);
    if (request.method !== 'GET') return;
    
    if (request.mode === 'navigate') {
        event.respondWith(networkFirstStrategy(request));
        return;
    }
    if (url.pathname.match(/\.(css|js|woff2|svg|png|jpg)$/)) {
        event.respondWith(cacheFirstStrategy(request));
        return;
    }
    if (url.pathname.startsWith('/api/')) {
        event.respondWith(staleWhileRevalidateStrategy(request));
        return;
    }
});

async function networkFirstStrategy(request) {
    try {
        const networkResponse = await fetch(request);
        const cache = await caches.open(RUNTIME_CACHE);
        cache.put(request, networkResponse.clone());
        return networkResponse;
    } catch (err) {
        const cachedResponse = await caches.match(request);
        if (cachedResponse) return cachedResponse;
        return caches.match(OFFLINE_URL);
    }
}

async function cacheFirstStrategy(request) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) return cachedResponse;
    try {
        const networkResponse = await fetch(request);
        const cache = await caches.open(STATIC_CACHE);
        cache.put(request, networkResponse.clone());
        return networkResponse;
    } catch (err) {
        return new Response('离线且无缓存', { status: 503 });
    }
}

async function staleWhileRevalidateStrategy(request) {
    const cache = await caches.open(RUNTIME_CACHE);
    const cachedResponse = await cache.match(request);
    const fetchPromise = fetch(request).then(networkResponse => {
        cache.put(request, networkResponse.clone());
        return networkResponse;
    }).catch(() => cachedResponse);
    return cachedResponse || fetchPromise;
}

后台同步与数据冲突处理

Background Sync API允许在网络恢复后自动重放离线期间的写操作。对于表单提交、数据编辑等需要持久化的操作,使用Background Sync比手动轮询网络状态更可靠。同步任务由浏览器调度,即使页面已关闭也会执行。

// 注册后台同步任务
async function registerSync(tag) {
    if ('serviceWorker' in navigator && 'SyncManager' in window) {
        const reg = await navigator.serviceWorker.ready;
        try {
            await reg.sync.register(tag);
        } catch (err) {
            retryQueue(tag);
        }
    } else {
        retryQueue(tag);
    }
}

// 主线程:提交表单时触发同步
async function submitForm(formData) {
    await saveToIndexedDB('pending-actions', {
        action: 'submit',
        data: formData,
        timestamp: Date.now()
    });
    await registerSync('form-submit');
    updateUI(formData);
}

// Service Worker:处理同步事件
self.addEventListener('sync', (event) => {
    if (event.tag === 'form-submit') {
        event.waitUntil(syncFormData());
    }
});

async function syncFormData() {
    const db = await openIndexedDB();
    const pending = await getAllFromStore(db, 'pending-actions');
    for (const item of pending) {
        try {
            const response = await fetch('/api/submit', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(item.data)
            });
            if (response.ok) {
                const serverData = await response.json();
                await updateLocalData(serverData);
                await deleteFromStore(db, 'pending-actions', item.id);
            } else if (response.status === 409) {
                const serverVersion = await response.json();
                await resolveConflict(item, serverVersion);
            }
        } catch (err) {
            throw err;
        }
    }
}

Web App Manifest与安装体验

Manifest文件定义PWA的图标、名称、启动方式和主题色。完整配置后,浏览器会提示用户”添加到主屏幕”,安装后的PWA在桌面有独立图标,以全屏模式启动。

// manifest.json
{
    "name": "任务管理系统",
    "short_name": "Tasks",
    "description": "离线可用的任务管理应用",
    "start_url": "/?source=pwa",
    "display": "standalone",
    "orientation": "portrait",
    "background_color": "#ffffff",
    "theme_color": "#1976d2",
    "icons": [
        {
            "src": "/icons/icon-192.png",
            "sizes": "192x192",
            "type": "image/png",
            "purpose": "any"
        },
        {
            "src": "/icons/icon-512.png",
            "sizes": "512x512",
            "type": "image/png",
            "purpose": "maskable"
        }
    ],
    "shortcuts": [
        {
            "name": "新建任务",
            "short_name": "新建",
            "url": "/tasks/new",
            "icons": [{ "src": "/icons/shortcut-new.png", "sizes": "96x96" }]
        }
    ]
}

IndexedDB离线数据存储

Service Worker的Cache API只能存储Response对象,应用业务数据需要IndexedDB存储。IndexedDB是浏览器内置的NoSQL数据库,支持事务、索引和游标查询,存储容量远超localStorage。

class IndexedDBHelper {
    constructor(dbName, version) {
        this.dbName = dbName;
        this.version = version;
        this.db = null;
    }
    
    async open() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(this.dbName, this.version);
            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                if (!db.objectStoreNames.contains('tasks')) {
                    const store = db.createObjectStore('tasks', { 
                        keyPath: 'id', autoIncrement: true 
                    });
                    store.createIndex('status', 'status', { unique: false });
                    store.createIndex('updatedAt', 'updatedAt', { unique: false });
                }
            };
            request.onsuccess = (event) => {
                this.db = event.target.result;
                resolve(this.db);
            };
            request.onerror = () => reject(request.error);
        });
    }
    
    async put(storeName, data) {
        return new Promise((resolve, reject) => {
            const tx = this.db.transaction(storeName, 'readwrite');
            const store = tx.objectStore(storeName);
            const req = store.put(data);
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }
    
    async getAll(storeName, indexName, query) {
        return new Promise((resolve, reject) => {
            const tx = this.db.transaction(storeName, 'readonly');
            const store = tx.objectStore(storeName);
            const source = indexName ? store.index(indexName) : store;
            const req = source.getAll(query);
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }
}

// 使用示例
const db = new IndexedDBHelper('TaskApp', 1);
await db.open();
await db.put('tasks', { 
    title: '完成API文档', status: 'pending', updatedAt: Date.now() 
});
const pendingTasks = await db.getAll('tasks', 'status', 'pending');

PWA的离线缓存策略需要根据应用类型调整。内容型应用(新闻、博客)侧重离线阅读,Cache First配合定期更新;工具型应用(编辑器、表单)侧重数据同步,IndexedDB存储加Background Sync回传;实时型应用(聊天、协作)需要在恢复连接后增量同步。缓存版本管理通过CACHE_VERSION变量控制,每次发版更新版本号,激活阶段自动清理旧缓存。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/pwa-li-xian-ying-yong-kai-fa-shi-zhan-serviceworker-huan/

(0)
小编小编
上一篇 2小时前
下一篇 2小时前

相关推荐