渐进式 Web 应用(Progressive Web Applications,简称 PWA)是一种结合了最佳 Web 和原生应用特性的现代 Web 应用程序。它们可靠、快速且具有吸引力,无需应用商店即可安装,并能在各种设备上提供类似原生应用的体验。
PWA 技术的出现解决了传统 Web 应用与原生应用之间的差距,为开发者提供了一种更加灵活、高效的应用开发方式。本指南将带您深入了解 PWA 的核心概念、技术基础以及实际开发流程。
PWA 即使在网络条件不稳定或离线状态下也能正常工作。这主要通过 Service Worker 技术实现,它允许应用缓存关键资源并在离线时提供内容。
PWA 加载迅速且交互流畅,即使在低速网络环境下也能提供良好的用户体验。这通过应用外壳架构(App Shell)、资源缓存和预加载等技术实现。
PWA 提供类似原生应用的用户体验,包括全屏显示、推送通知和主屏幕图标等功能,增强用户参与度和留存率。
PWA 采用渐进增强的设计理念,能够适应不同设备和浏览器的能力,在支持度较低的环境中仍能提供基本功能,在现代浏览器中则提供完整体验。
PWA 必须通过 HTTPS 提供服务,确保数据传输的安全性和完整性。
Service Worker 是 PWA 的核心技术,它是一种在浏览器后台运行的脚本,能够拦截网络请求、缓存资源和实现离线功能。
// 注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker 注册成功:', registration.scope);
})
.catch(error => {
console.log('Service Worker 注册失败:', error);
});
});
}
Web App Manifest 是一个 JSON 文件,定义了应用的名称、图标、启动 URL 和显示模式等信息,使 Web 应用能够被安装到设备主屏幕。
{
"name": "我的 PWA 应用",
"short_name": "PWA",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"icons": [
{
"src": "/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
PWA 可以采用多种缓存策略,根据应用需求选择合适的策略:
// 缓存优先策略示例
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request)
.then(fetchResponse => {
return caches.open('my-cache')
.then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
});
首先,创建一个基础的 Web 应用,确保它具有响应式设计,能够适应不同设备屏幕。
创建 manifest.json
文件并在 HTML 中引用:
<link rel="manifest" href="/manifest.json">
创建 Service Worker 脚本 sw.js
,实现资源缓存和离线功能:
// 缓存名称和要缓存的资源列表
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
// 安装 Service Worker 并缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
);
});
// 拦截网络请求并从缓存提供资源
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 如果在缓存中找到匹配的资源,则返回缓存的版本
if (response) {
return response;
}
// 否则从网络获取资源
return fetch(event.request)
.then(response => {
// 检查是否是有效的响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应,因为响应是流,只能使用一次
const responseToCache = response.clone();
// 将新获取的资源添加到缓存
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// 激活新版本 Service Worker 并清理旧缓存
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 删除不在白名单中的缓存
return caches.delete(cacheName);
}
})
);
})
);
});
创建一个离线页面,当用户离线且请求的资源未缓存时显示:
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>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px 20px;
}
.offline-icon {
font-size: 48px;
margin-bottom: 20px;
}
style>
head>
<body>
<div class="offline-icon">div>
<h1>您当前处于离线状态h1>
<p>请检查您的网络连接并重试。p>
<button onclick="window.location.reload()">重试button>
body>
html>
添加推送通知功能,增强用户参与度:
// 请求通知权限
function requestNotificationPermission() {
if ('Notification' in window) {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('通知权限已授予');
// 订阅推送服务
subscribeToPushService();
}
});
}
}
// 订阅推送服务
function subscribeToPushService() {
navigator.serviceWorker.ready.then(registration => {
// 检查推送服务是否可用
if (!('pushManager' in registration)) {
console.log('推送服务不可用');
return;
}
// 订阅推送
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY')
})
.then(subscription => {
console.log('用户已订阅:', subscription.endpoint);
// 将订阅信息发送到服务器
sendSubscriptionToServer(subscription);
})
.catch(error => {
console.error('推送订阅失败:', error);
});
});
}
// Base64 URL 解码为 Uint8Array
function 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;
}
// 在 Service Worker 中处理推送事件
self.addEventListener('push', event => {
if (event.data) {
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/notification-icon.png',
badge: '/images/notification-badge.png',
data: {
url: data.url
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
}
});
// 处理通知点击事件
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.notification.data && event.notification.data.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
实现后台同步功能,确保用户操作在网络恢复后能够完成:
// 注册后台同步
function registerBackgroundSync() {
navigator.serviceWorker.ready.then(registration => {
if ('sync' in registration) {
// 注册同步标签
registration.sync.register('sync-data')
.then(() => {
console.log('后台同步已注册');
})
.catch(error => {
console.error('后台同步注册失败:', error);
});
}
});
}
// 在 Service Worker 中处理同步事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(
syncData()
);
}
});
// 同步数据到服务器
function syncData() {
return fetch('/api/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
timestamp: new Date().toISOString()
})
})
.then(response => {
if (response.ok) {
console.log('数据同步成功');
} else {
throw new Error('数据同步失败');
}
})
.catch(error => {
console.error('同步错误:', error);
throw error;
});
}
应用外壳架构是一种 PWA 设计模式,它将应用的核心界面与数据分离。外壳包含应用的基本 HTML、CSS 和 JavaScript,可以快速加载并缓存,提供即时的用户体验。
// 缓存应用外壳
self.addEventListener('install', event => {
event.waitUntil(
caches.open('app-shell-v1')
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles/shell.css',
'/scripts/shell.js',
'/images/logo.png',
'/images/header-bg.jpg',
'/offline.html'
]);
})
);
});
使用 预加载关键资源,减少加载时间:
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.jpg" as="image">
<link rel="preload" href="/scripts/critical.js" as="script">
使用现代图像格式(如 WebP)和响应式图像,减少加载时间:
<picture>
<source srcset="/images/hero.webp" type="image/webp">
<source srcset="/images/hero.jpg" type="image/jpeg">
<img src="/images/hero.jpg" alt="Hero Image" loading="lazy">
picture>
使用代码分割技术,只加载当前页面所需的 JavaScript:
// 使用动态导入
if (document.querySelector('.product-gallery')) {
import('./product-gallery.js')
.then(module => {
module.initGallery();
})
.catch(error => {
console.error('加载产品画廊模块失败:', error);
});
}
Twitter Lite 是一个典型的 PWA 成功案例,它通过以下方式优化用户体验:
PWA 技术正在不断发展,未来将有更多创新:
渐进式 Web 应用(PWA)代表了 Web 应用开发的未来方向,它结合了 Web 的开放性和原生应用的用户体验。通过实现 Service Worker、Web App Manifest 和推送通知等技术,开发者可以创建快速、可靠且具有吸引力的应用,满足现代用户的需求。
随着浏览器支持的不断改进和新 API 的引入,PWA 的能力将继续扩展,为用户提供更丰富的体验。现在正是开始探索和采用 PWA 技术的最佳时机,为您的用户提供卓越的 Web 应用体验。