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/