Search K
Appearance
Appearance
关键词: Progressive Web Apps, PWA, Service Worker, Web App Manifest, 离线功能, 原生应用体验, 缓存策略, 推送通知
Progressive Web Apps(PWA)是一种使用现代Web技术构建的应用程序,它结合了Web应用的开放性和原生应用的用户体验。
<!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>
<!-- PWA基本配置 -->
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2196F3">
<!-- iOS Safari配置 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="PWA示例">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<!-- 样式 -->
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.app-header {
background-color: #2196F3;
color: white;
padding: 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.app-header h1 {
margin: 0;
font-size: 20px;
}
.app-content {
padding: 16px;
max-width: 600px;
margin: 0 auto;
}
.feature-card {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.feature-card h3 {
margin: 0 0 8px 0;
color: #333;
}
.feature-card p {
margin: 0;
color: #666;
line-height: 1.5;
}
.install-button {
background-color: #4CAF50;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 16px 0;
display: none;
}
.install-button:hover {
background-color: #45a049;
}
.status-indicator {
position: fixed;
top: 10px;
right: 10px;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.online {
background-color: #4CAF50;
color: white;
}
.offline {
background-color: #f44336;
color: white;
}
</style>
</head>
<body>
<div class="status-indicator online" id="status">在线</div>
<header class="app-header">
<h1>PWA示例应用</h1>
</header>
<main class="app-content">
<button class="install-button" id="installButton">安装应用</button>
<div class="feature-card">
<h3>🚀 快速加载</h3>
<p>通过Service Worker缓存,应用能够快速加载,即使在网络条件不佳的情况下也能提供良好体验。</p>
</div>
<div class="feature-card">
<h3>📱 响应式设计</h3>
<p>适配各种屏幕尺寸,在手机、平板和桌面设备上都能完美显示。</p>
</div>
<div class="feature-card">
<h3>🔒 安全连接</h3>
<p>运行在HTTPS上,确保数据传输的安全性。</p>
</div>
<div class="feature-card">
<h3>📶 离线工作</h3>
<p>即使在没有网络连接的情况下,应用仍能正常工作。</p>
</div>
<div class="feature-card">
<h3>🏠 可安装</h3>
<p>可以安装到设备主屏幕,像原生应用一样启动。</p>
</div>
</main>
<script>
// 注册Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('Service Worker注册成功:', registration.scope);
})
.catch(function(error) {
console.log('Service Worker注册失败:', error);
});
});
}
// 处理应用安装
let deferredPrompt;
const installButton = document.getElementById('installButton');
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
installButton.style.display = 'block';
});
installButton.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('用户选择:', outcome);
deferredPrompt = null;
installButton.style.display = 'none';
}
});
// 监听应用安装事件
window.addEventListener('appinstalled', (evt) => {
console.log('应用已安装');
});
// 网络状态监听
const statusIndicator = document.getElementById('status');
function updateOnlineStatus() {
if (navigator.onLine) {
statusIndicator.textContent = '在线';
statusIndicator.className = 'status-indicator online';
} else {
statusIndicator.textContent = '离线';
statusIndicator.className = 'status-indicator offline';
}
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
// 初始化状态
updateOnlineStatus();
</script>
</body>
</html>{
"name": "PWA示例应用",
"short_name": "PWA示例",
"description": "一个完整的PWA应用示例",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"orientation": "portrait-primary",
"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"
}
],
"shortcuts": [
{
"name": "新建笔记",
"short_name": "新建",
"description": "快速创建新笔记",
"url": "/new-note",
"icons": [
{
"src": "/icons/new-note.png",
"sizes": "192x192"
}
]
}
],
"categories": ["productivity", "utilities"],
"lang": "zh-CN",
"dir": "ltr"
}Service Worker是PWA的核心技术,它运行在后台,充当Web应用和网络之间的代理。
// sw.js - Service Worker文件
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/script.js',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png'
];
// 安装事件
self.addEventListener('install', function(event) {
console.log('Service Worker安装中...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
);
});
// 激活事件
self.addEventListener('activate', function(event) {
console.log('Service Worker激活中...');
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
// 拦截网络请求
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 如果缓存中有,直接返回
if (response) {
return response;
}
// 否则发起网络请求
return fetch(event.request)
.then(function(response) {
// 检查是否为有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应,因为响应流只能使用一次
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});// 高级Service Worker - 多种缓存策略
const CACHE_NAME = 'pwa-cache-v2';
const API_CACHE = 'api-cache-v1';
const IMAGE_CACHE = 'image-cache-v1';
// 缓存策略枚举
const CACHE_STRATEGIES = {
CACHE_FIRST: 'cache-first',
NETWORK_FIRST: 'network-first',
STALE_WHILE_REVALIDATE: 'stale-while-revalidate',
NETWORK_ONLY: 'network-only',
CACHE_ONLY: 'cache-only'
};
// 路由配置
const ROUTES = [
{
pattern: /^https:\/\/api\.example\.com\/.*$/,
strategy: CACHE_STRATEGIES.NETWORK_FIRST,
cache: API_CACHE,
maxAge: 5 * 60 * 1000 // 5分钟
},
{
pattern: /.*\.(?:png|jpg|jpeg|svg|gif|webp)$/,
strategy: CACHE_STRATEGIES.CACHE_FIRST,
cache: IMAGE_CACHE,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30天
},
{
pattern: /.*\.(?:js|css|html)$/,
strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE,
cache: CACHE_NAME,
maxAge: 24 * 60 * 60 * 1000 // 24小时
}
];
// 安装事件
self.addEventListener('install', function(event) {
console.log('Service Worker安装中...');
const urlsToCache = [
'/',
'/offline.html',
'/styles.css',
'/script.js'
];
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
// 立即激活新的Service Worker
self.skipWaiting();
});
// 激活事件
self.addEventListener('activate', function(event) {
console.log('Service Worker激活中...');
event.waitUntil(
Promise.all([
// 清理旧缓存
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (![CACHE_NAME, API_CACHE, IMAGE_CACHE].includes(cacheName)) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
}),
// 立即控制所有页面
self.clients.claim()
])
);
});
// 拦截网络请求
self.addEventListener('fetch', function(event) {
const request = event.request;
const url = request.url;
// 找到匹配的路由
const route = ROUTES.find(route => route.pattern.test(url));
if (route) {
event.respondWith(handleRequest(request, route));
} else {
// 默认策略
event.respondWith(
caches.match(request)
.then(response => response || fetch(request))
.catch(() => caches.match('/offline.html'))
);
}
});
// 处理请求的函数
async function handleRequest(request, route) {
const { strategy, cache, maxAge } = route;
switch (strategy) {
case CACHE_STRATEGIES.CACHE_FIRST:
return cacheFirst(request, cache, maxAge);
case CACHE_STRATEGIES.NETWORK_FIRST:
return networkFirst(request, cache, maxAge);
case CACHE_STRATEGIES.STALE_WHILE_REVALIDATE:
return staleWhileRevalidate(request, cache, maxAge);
case CACHE_STRATEGIES.NETWORK_ONLY:
return fetch(request);
case CACHE_STRATEGIES.CACHE_ONLY:
return caches.match(request);
default:
return fetch(request);
}
}
// 缓存优先策略
async function cacheFirst(request, cacheName, maxAge) {
const cachedResponse = await caches.match(request);
if (cachedResponse && !isExpired(cachedResponse, maxAge)) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
return networkResponse;
} catch (error) {
return cachedResponse || new Response('离线时无法访问', { status: 503 });
}
}
// 网络优先策略
async function networkFirst(request, cacheName, maxAge) {
try {
const networkResponse = await fetch(request);
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
return cachedResponse || new Response('离线时无法访问', { status: 503 });
}
}
// 过期重新验证策略
async function staleWhileRevalidate(request, cacheName, maxAge) {
const cachedResponse = await caches.match(request);
const fetchPromise = fetch(request).then(networkResponse => {
const cache = caches.open(cacheName);
cache.then(c => c.put(request, networkResponse.clone()));
return networkResponse;
});
return cachedResponse || fetchPromise;
}
// 检查缓存是否过期
function isExpired(response, maxAge) {
if (!maxAge) return false;
const cachedTime = response.headers.get('sw-cache-time');
if (!cachedTime) return false;
return Date.now() - parseInt(cachedTime) > maxAge;
}
// 后台同步
self.addEventListener('sync', function(event) {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
async function doBackgroundSync() {
// 处理离线时的数据同步
const pendingRequests = await getPendingRequests();
for (const request of pendingRequests) {
try {
await fetch(request);
await removePendingRequest(request);
} catch (error) {
console.log('同步失败:', error);
}
}
}
// 推送通知
self.addEventListener('push', function(event) {
if (event.data) {
const data = event.data.json();
const options = {
body: data.body,
icon: data.icon || '/icons/icon-192x192.png',
badge: '/icons/badge.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: data.primaryKey
},
actions: [
{
action: 'explore',
title: '查看详情',
icon: '/icons/checkmark.png'
},
{
action: 'close',
title: '关闭',
icon: '/icons/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
}
});
// 通知点击事件
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/notification-details')
);
}
});<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>离线笔记应用</title>
<link rel="manifest" href="/manifest.json">
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.app-header {
background-color: #2196F3;
color: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.app-header h1 {
margin: 0;
}
.note-form {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.note-form input,
.note-form textarea {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.note-form textarea {
height: 120px;
resize: vertical;
}
.note-form button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.note-form button:hover {
background-color: #45a049;
}
.note-form button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.notes-container {
display: grid;
gap: 16px;
}
.note-item {
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
}
.note-item h3 {
margin: 0 0 8px 0;
color: #333;
}
.note-item p {
margin: 0 0 12px 0;
color: #666;
line-height: 1.5;
}
.note-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #999;
}
.note-actions {
display: flex;
gap: 8px;
}
.note-actions button {
padding: 4px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.edit-btn {
background-color: #2196F3;
color: white;
}
.delete-btn {
background-color: #f44336;
color: white;
}
.sync-status {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
z-index: 1000;
}
.synced {
background-color: #4CAF50;
color: white;
}
.syncing {
background-color: #FF9800;
color: white;
}
.offline {
background-color: #f44336;
color: white;
}
.pending-sync {
background-color: #2196F3;
color: white;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.offline-indicator {
background-color: #f44336;
color: white;
padding: 8px;
text-align: center;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.offline-indicator.show {
transform: translateY(0);
}
</style>
</head>
<body>
<div class="offline-indicator" id="offlineIndicator">
您当前处于离线状态,数据将在恢复网络连接后自动同步
</div>
<div class="sync-status" id="syncStatus">已同步</div>
<header class="app-header">
<h1>离线笔记应用</h1>
<p>支持离线编辑,数据自动同步</p>
</header>
<div class="note-form">
<input type="text" id="noteTitle" placeholder="笔记标题">
<textarea id="noteContent" placeholder="笔记内容"></textarea>
<button id="saveNote">保存笔记</button>
</div>
<div class="notes-container" id="notesContainer">
<div class="empty-state">
<h3>暂无笔记</h3>
<p>创建您的第一个笔记吧!</p>
</div>
</div>
<script>
class OfflineNoteApp {
constructor() {
this.notes = [];
this.pendingSync = [];
this.isOnline = navigator.onLine;
this.syncStatus = document.getElementById('syncStatus');
this.offlineIndicator = document.getElementById('offlineIndicator');
this.init();
}
async init() {
// 加载本地数据
await this.loadLocalNotes();
// 绑定事件
this.bindEvents();
// 渲染笔记
this.renderNotes();
// 监听网络状态
this.setupNetworkListeners();
// 尝试同步
if (this.isOnline) {
this.syncNotes();
}
}
bindEvents() {
const saveButton = document.getElementById('saveNote');
const titleInput = document.getElementById('noteTitle');
const contentInput = document.getElementById('noteContent');
saveButton.addEventListener('click', () => {
this.saveNote();
});
// 快捷键保存
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
this.saveNote();
}
});
}
setupNetworkListeners() {
window.addEventListener('online', () => {
this.isOnline = true;
this.offlineIndicator.classList.remove('show');
this.syncNotes();
});
window.addEventListener('offline', () => {
this.isOnline = false;
this.offlineIndicator.classList.add('show');
this.updateSyncStatus('离线');
});
// 初始状态
if (!this.isOnline) {
this.offlineIndicator.classList.add('show');
}
}
async saveNote() {
const title = document.getElementById('noteTitle').value.trim();
const content = document.getElementById('noteContent').value.trim();
if (!title || !content) {
alert('请输入标题和内容');
return;
}
const note = {
id: Date.now(),
title: title,
content: content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
synced: false
};
// 保存到本地
this.notes.unshift(note);
await this.saveToLocal();
// 添加到待同步队列
this.pendingSync.push(note);
// 清空表单
document.getElementById('noteTitle').value = '';
document.getElementById('noteContent').value = '';
// 重新渲染
this.renderNotes();
// 尝试同步
if (this.isOnline) {
this.syncNotes();
} else {
this.updateSyncStatus('待同步');
}
}
async deleteNote(id) {
if (confirm('确定要删除这个笔记吗?')) {
this.notes = this.notes.filter(note => note.id !== id);
await this.saveToLocal();
this.renderNotes();
// 如果是已同步的笔记,需要发送删除请求
if (this.isOnline) {
try {
await fetch(`/api/notes/${id}`, {
method: 'DELETE'
});
} catch (error) {
console.log('删除同步失败:', error);
}
}
}
}
async editNote(id) {
const note = this.notes.find(n => n.id === id);
if (note) {
document.getElementById('noteTitle').value = note.title;
document.getElementById('noteContent').value = note.content;
// 删除原笔记
this.notes = this.notes.filter(n => n.id !== id);
await this.saveToLocal();
this.renderNotes();
}
}
renderNotes() {
const container = document.getElementById('notesContainer');
if (this.notes.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>暂无笔记</h3>
<p>创建您的第一个笔记吧!</p>
</div>
`;
return;
}
container.innerHTML = this.notes.map(note => `
<div class="note-item">
<h3>${note.title}</h3>
<p>${note.content}</p>
<div class="note-meta">
<span>创建于: ${new Date(note.createdAt).toLocaleString()}</span>
<div class="note-actions">
<button class="edit-btn" onclick="app.editNote(${note.id})">编辑</button>
<button class="delete-btn" onclick="app.deleteNote(${note.id})">删除</button>
</div>
</div>
${!note.synced ? '<div style="color: #FF9800; font-size: 12px;">待同步</div>' : ''}
</div>
`).join('');
}
async syncNotes() {
if (!this.isOnline || this.pendingSync.length === 0) {
return;
}
this.updateSyncStatus('同步中');
try {
for (const note of this.pendingSync) {
const response = await fetch('/api/notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(note)
});
if (response.ok) {
// 标记为已同步
const localNote = this.notes.find(n => n.id === note.id);
if (localNote) {
localNote.synced = true;
}
}
}
// 清空待同步队列
this.pendingSync = [];
// 保存到本地
await this.saveToLocal();
// 重新渲染
this.renderNotes();
this.updateSyncStatus('已同步');
} catch (error) {
console.error('同步失败:', error);
this.updateSyncStatus('同步失败');
}
}
async loadLocalNotes() {
try {
const saved = localStorage.getItem('offline-notes');
if (saved) {
this.notes = JSON.parse(saved);
// 加载待同步队列
this.pendingSync = this.notes.filter(note => !note.synced);
}
} catch (error) {
console.error('加载本地数据失败:', error);
}
}
async saveToLocal() {
try {
localStorage.setItem('offline-notes', JSON.stringify(this.notes));
} catch (error) {
console.error('保存到本地失败:', error);
}
}
updateSyncStatus(status) {
this.syncStatus.textContent = status;
this.syncStatus.className = 'sync-status';
switch (status) {
case '已同步':
this.syncStatus.classList.add('synced');
break;
case '同步中':
this.syncStatus.classList.add('syncing');
break;
case '离线':
this.syncStatus.classList.add('offline');
break;
case '待同步':
this.syncStatus.classList.add('pending-sync');
break;
default:
this.syncStatus.classList.add('offline');
}
}
}
// 初始化应用
const app = new OfflineNoteApp();
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功');
// 监听Service Worker更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 有新版本可用
if (confirm('发现新版本,是否刷新页面?')) {
window.location.reload();
}
}
});
});
})
.catch(error => {
console.error('Service Worker注册失败:', error);
});
}
// 后台同步
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('background-sync');
});
}
</script>
</body>
</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>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.notification-demo {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.notification-demo h3 {
margin-top: 0;
}
.btn {
background-color: #2196F3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
margin-bottom: 10px;
}
.btn:hover {
background-color: #1976D2;
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.status {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
background-color: #e3f2fd;
border-left: 4px solid #2196F3;
}
.permission-status {
font-weight: bold;
margin-bottom: 10px;
}
.granted {
color: #4CAF50;
}
.denied {
color: #f44336;
}
.default {
color: #FF9800;
}
</style>
</head>
<body>
<h1>PWA推送通知示例</h1>
<div class="notification-demo">
<h3>通知权限状态</h3>
<div class="permission-status" id="permissionStatus">检查中...</div>
<button class="btn" id="requestPermission">请求通知权限</button>
</div>
<div class="notification-demo">
<h3>本地通知</h3>
<button class="btn" onclick="showSimpleNotification()">简单通知</button>
<button class="btn" onclick="showRichNotification()">富媒体通知</button>
<button class="btn" onclick="showActionNotification()">操作按钮通知</button>
</div>
<div class="notification-demo">
<h3>推送通知</h3>
<button class="btn" id="subscribePush">订阅推送</button>
<button class="btn" id="unsubscribePush">取消订阅</button>
<button class="btn" id="sendPushNotification">发送推送通知</button>
<div class="status" id="pushStatus">推送状态:未订阅</div>
</div>
<div class="notification-demo">
<h3>定时提醒</h3>
<button class="btn" onclick="scheduleNotification(5000)">5秒后提醒</button>
<button class="btn" onclick="scheduleNotification(10000)">10秒后提醒</button>
<button class="btn" onclick="scheduleNotification(30000)">30秒后提醒</button>
</div>
<script>
// 检查通知权限
function checkNotificationPermission() {
const status = Notification.permission;
const statusElement = document.getElementById('permissionStatus');
statusElement.textContent = `权限状态: ${status}`;
statusElement.className = `permission-status ${status}`;
return status;
}
// 请求通知权限
async function requestNotificationPermission() {
if (!('Notification' in window)) {
alert('此浏览器不支持通知功能');
return false;
}
const permission = await Notification.requestPermission();
checkNotificationPermission();
return permission === 'granted';
}
// 显示简单通知
function showSimpleNotification() {
if (Notification.permission === 'granted') {
new Notification('简单通知', {
body: '这是一个简单的通知示例',
icon: '/icons/icon-192x192.png'
});
} else {
alert('请先授予通知权限');
}
}
// 显示富媒体通知
function showRichNotification() {
if (Notification.permission === 'granted') {
new Notification('富媒体通知', {
body: '这个通知包含了图片、图标和徽章',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge.png',
image: '/images/notification-image.jpg',
tag: 'rich-notification',
renotify: true,
vibrate: [200, 100, 200],
timestamp: Date.now(),
requireInteraction: true
});
} else {
alert('请先授予通知权限');
}
}
// 显示带操作按钮的通知
function showActionNotification() {
if (Notification.permission === 'granted') {
navigator.serviceWorker.ready.then(registration => {
registration.showNotification('操作通知', {
body: '点击按钮进行操作',
icon: '/icons/icon-192x192.png',
actions: [
{
action: 'reply',
title: '回复',
icon: '/icons/reply.png'
},
{
action: 'archive',
title: '归档',
icon: '/icons/archive.png'
}
]
});
});
} else {
alert('请先授予通知权限');
}
}
// 定时通知
function scheduleNotification(delay) {
if (Notification.permission === 'granted') {
setTimeout(() => {
new Notification('定时提醒', {
body: `${delay / 1000}秒的定时提醒`,
icon: '/icons/icon-192x192.png'
});
}, delay);
alert(`已设置${delay / 1000}秒后的提醒`);
} else {
alert('请先授予通知权限');
}
}
// 推送通知相关
let isSubscribed = false;
let swRegistration = null;
// 初始化推送
function initializePush() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.ready.then(registration => {
swRegistration = registration;
return registration.pushManager.getSubscription();
}).then(subscription => {
isSubscribed = subscription !== null;
updatePushStatus();
if (isSubscribed) {
console.log('已订阅推送');
} else {
console.log('未订阅推送');
}
}).catch(error => {
console.error('初始化推送失败:', error);
});
}
}
// 订阅推送
async function subscribeToPush() {
try {
const subscription = await swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 发送订阅信息到服务器
const response = await fetch('/api/push-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
if (response.ok) {
isSubscribed = true;
updatePushStatus();
console.log('推送订阅成功');
} else {
throw new Error('订阅失败');
}
} catch (error) {
console.error('订阅推送失败:', error);
alert('订阅失败,请重试');
}
}
// 取消订阅
async function unsubscribeFromPush() {
try {
const subscription = await swRegistration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
// 通知服务器取消订阅
await fetch('/api/push-subscription', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
isSubscribed = false;
updatePushStatus();
console.log('取消订阅成功');
}
} catch (error) {
console.error('取消订阅失败:', error);
alert('取消订阅失败,请重试');
}
}
// 发送推送通知
async function sendPushNotification() {
if (!isSubscribed) {
alert('请先订阅推送');
return;
}
try {
const response = await fetch('/api/send-notification', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: '测试推送通知',
body: '这是一条测试推送通知',
icon: '/icons/icon-192x192.png'
})
});
if (response.ok) {
alert('推送通知已发送');
} else {
throw new Error('发送失败');
}
} catch (error) {
console.error('发送推送通知失败:', error);
alert('发送失败,请重试');
}
}
// 更新推送状态
function updatePushStatus() {
const statusElement = document.getElementById('pushStatus');
statusElement.textContent = `推送状态:${isSubscribed ? '已订阅' : '未订阅'}`;
document.getElementById('subscribePush').disabled = isSubscribed;
document.getElementById('unsubscribePush').disabled = !isSubscribed;
document.getElementById('sendPushNotification').disabled = !isSubscribed;
}
// VAPID公钥转换
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;
}
// VAPID公钥(实际使用时需要替换为真实的公钥)
const VAPID_PUBLIC_KEY = 'BFxzgSJYzOadpzjm0eKQCzYSqHZ3QTUzCzYSqHZ3QTUzCzYSqHZ3QTUzCzYSqHZ3QTUzCzYSqHZ3QTUz';
// 事件绑定
document.getElementById('requestPermission').addEventListener('click', requestNotificationPermission);
document.getElementById('subscribePush').addEventListener('click', subscribeToPush);
document.getElementById('unsubscribePush').addEventListener('click', unsubscribeFromPush);
document.getElementById('sendPushNotification').addEventListener('click', sendPushNotification);
// 初始化
checkNotificationPermission();
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(() => {
initializePush();
});
}
</script>
</body>
</html>A: PWA运行在浏览器中,具有跨平台特性,但在某些原生功能访问上可能有限制;原生应用性能更好,但开发和维护成本更高。
A: 根据资源类型选择:静态资源用Cache First,API数据用Network First,频繁更新的内容用Stale While Revalidate。
A: 可以使用浏览器开发者工具的Network面板模拟离线状态,或者使用Chrome DevTools的Application面板测试Service Worker。
A: 需要HTTPS环境、用户授权、Service Worker注册,以及推送服务器的支持(如FCM)。
A: 使用合适的缓存策略、预缓存关键资源、实现资源懒加载、压缩资源文件,以及使用性能监控工具。
下一节预览:下一节我们将学习WebAssembly,重点介绍WebAssembly的基本概念、性能优势和实际应用场景。