关于 pwa-storefront
该技能指导开发者为自定义或无头电商前端构建渐进式 Web 应用商店。内容涵盖离线目录、缓存策略、主屏幕安装及订单通知等实现。
主要功能
- 01创建 Web App Manifest
- 02使用 Workbox 注册服务工作线程
- 03缓存目录并提供离线页面
- 04实现 Web Push 通知
- 05检测在线状态并更新界面
适用场景
- 01为网络不稳定地区提供离线浏览
- 02让顾客将商店添加到主屏幕
- 03通过缓存加快重复页面加载
- 04发送订单状态或补货通知
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill pwa-storefront安装后在 Claude Code 中输入/pwa-storefront确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
tile.jsonevals/push-notifications-and-offline-ux/criteria.jsontask.mdevals/service-worker-caching-strategies/criteria.jsontask.mdevals/web-app-manifest-and-installability/criteria.jsontask.md详细介绍
PWA 商店前台
概述
渐进式 Web 应用(PWA)商店前台兼具 Web 的覆盖范围与类似原生应用的功能:离线浏览商品目录、推送通知、安装到主屏幕,以及从缓存快速重复加载。Service Worker 会拦截网络请求并实施缓存策略,让商店在网络不稳定时仍能正常使用。本技能涵盖使用 Workbox 实现 Service Worker、创建 Web App Manifest、缓存商品目录,以及发送订单更新推送通知。
何时使用本技能
- 当客户所在地区的移动互联网连接不稳定时
- 当你希望支持“添加到主屏幕”,在无需原生应用的情况下提高用户再次使用的意愿时
- 当你希望通过从缓存提供资源,让页面再次加载时可以即时呈现时
- 当你希望发送订单状态更新、到货提醒或促销推送通知时
- 当你构建移动优先的商店前台,并需要在用户体验质量上与原生应用竞争时
前置条件和平台说明
本技能面向自定义/无头商店前台(Node.js、Python 或类似后端)。代码示例使用 TypeScript/Node.js,可调整后用于任何技术栈。
Shopify:Shopify Hydrogen 是 Shopify 的无头框架。使用 Shopify 作为商务后端并搭配自定义前端,或将 Shopify 与其他同类最佳服务结合时,可采用 MACH/可组合模式。 WooCommerce:WooCommerce 可通过其 REST API 和 WPGraphQL 作为无头后端。当你将前端与 WordPress 解耦时,可采用这些模式。 Magento:Magento 的 GraphQL API 和 PWA Studio 支持无头架构。在 MACH 技术栈中将 Magento 用作后端服务时,可采用这些可组合模式。
你需要:
- Node.js 18+(或改用你的后端语言)
- PostgreSQL(或你偏好的关系型数据库)
- Redis,用于缓存/队列
- 电子邮件发送服务(SendGrid、AWS SES 或 Postmark)
- CDN(Cloudflare、CloudFront 或 Fastly)
核心说明
-
创建 Web App Manifest
Manifest 可让应用安装到 Android 和 iOS 上(iOS 仅部分支持):
// public/manifest.json { "name": "My Commerce Store", "short_name": "MyStore", "description": "Fast, reliable shopping from anywhere", "start_url": "/?source=pwa", "display": "standalone", "background_color": "#ffffff", "theme_color": "#1a1a2e", "orientation": "portrait-primary", "icons": [ { "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ], "screenshots": [ { "src": "/screenshots/home.png", "sizes": "390x844", "type": "image/png", "form_factor": "narrow" } ], "categories": ["shopping"], "share_target": { "action": "/search", "method": "GET", "params": { "title": "q" } } }在 HTML 中添加链接:
<link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#1a1a2e"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <link rel="apple-touch-icon" href="/icons/icon-192x192.png"> -
使用 Workbox 注册 Service Worker
npm install workbox-webpack-plugin # or for Vite: npm install vite-plugin-pwa使用
vite-plugin-pwa(推荐用于 Vite/Next.js 项目):// vite.config.ts import {VitePWA} from 'vite-plugin-pwa'; export default { plugins: [ VitePWA({ registerType: 'autoUpdate', workbox: { globPatterns: ['**/*.{js,css,html,svg,png,webp,woff2}'], runtimeCaching: [ { urlPattern: /^https:\/\/fonts\.googleapis\.com/, handler: 'CacheFirst', options: {cacheName: 'google-fonts-cache', expiration: {maxAgeSeconds: 60 * 60 * 24 * 365}}, }, { urlPattern: /\/api\/products/, handler: 'StaleWhileRevalidate', options: { cacheName: 'products-cache', expiration: {maxEntries: 500, maxAgeSeconds: 60 * 60 * 24}, // 24h cacheableResponse: {statuses: [0, 200]}, }, }, { urlPattern: /\/api\/collections/, handler: 'NetworkFirst', options: { cacheName: 'collections-cache', networkTimeoutSeconds: 3, expiration: {maxEntries: 50, maxAgeSeconds: 60 * 60}, }, }, ], }, manifest: {/* inline manifest or path */}, }), ], }; -
实现用于离线商品目录的自定义 Service Worker
如需精细控制,可直接编写 Service Worker:
// public/sw.js import {precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching'; import {registerRoute} from 'workbox-routing'; import {StaleWhileRevalidate, CacheFirst, NetworkFirst} from 'workbox-strategies'; import {ExpirationPlugin} from 'workbox-expiration'; import {BackgroundSyncPlugin} from 'workbox-background-sync'; // Precache app shell (injected by build tool) precacheAndRoute(self.__WB_MANIFEST); cleanupOutdatedCaches(); // Product images: Cache-first with 7-day expiry registerRoute( ({url}) => url.hostname.includes('cdn.shopify.com') || url.pathname.includes('/product-images/'), new CacheFirst({ cacheName: 'product-images', plugins: [ new ExpirationPlugin({maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 7}), ], }) ); // Product API: Stale-while-revalidate (show cached, refresh in background) registerRoute( ({url}) => url.pathname.startsWith('/api/products') || url.pathname.startsWith('/api/collections'), new StaleWhileRevalidate({ cacheName: 'api-products', plugins: [ new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 60 * 60 * 24}), ], }) ); // Background sync for cart operations when offline const cartSyncPlugin = new BackgroundSyncPlugin('cart-sync-queue', { maxRetentionTime: 24 * 60, // Retry for 24 hours }); registerRoute( ({url, request}) => url.pathname.startsWith('/api/cart') && request.method !== 'GET', new NetworkFirst({plugins: [cartSyncPlugin]}), 'POST' ); -
显示离线备用页面
// In the service worker import {setCatchHandler, setDefaultHandler} from 'workbox-routing'; // Precache the offline page during installation precacheAndRoute([{url: '/offline', revision: '1'}]); // Serve offline page for navigation requests when network fails setCatchHandler(async ({event}) => { if (event.request.destination === 'document') { return caches.match('/offline'); } return Response.error(); });// app/offline/page.tsx export default function OfflinePage() { return ( <div className="flex flex-col items-center justify-center min-h-screen"> <h1>You're offline</h1> <p>Check your connection. Recently viewed products are still available below.</p> <RecentlyViewedProducts /> {/* Reads from IndexedDB */} </div> ); } -
实现 Web Push 通知
// client: subscribe to push notifications async function subscribeToPush() { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!), }); await fetch('/api/push/subscribe', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({subscription, customerId: user.id}), }); } // server: send push notification for order status update import webpush from 'web-push'; webpush.setVapidDetails( 'mailto:[email protected]', process.env.VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! ); export async function sendOrderUpdatePush(customerId: string, order: Order) { const subscriptions = await db.pushSubscriptions.findByCustomer(customerId); await Promise.allSettled( subscriptions.map(sub => webpush.sendNotification(sub.data, JSON.stringify({ title: `Order #${order.number} Update`, body: `Your order is now ${order.status}`, icon: '/icons/icon-192x192.png', url: `/orders/${order.id}`, tag: `order-${order.id}`, })) ) ); }在 Service Worker 中处理推送事件:
self.addEventListener('push', (event) => { const data = event.data?.json(); event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: data.icon, badge: '/icons/badge-72x72.png', data: {url: data.url}, tag: data.tag, renotify: true, }) ); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data.url)); }); -
在 UI 中检测并响应离线状态
// hooks/use-online-status.ts import {useState, useEffect} from 'react'; export function useOnlineStatus() { const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true); useEffect(() => { const setOnline = () => setIsOnline(true); const setOffline = () => setIsOnline(false); window.addEventListener('online', setOnline); window.addEventListener('offline', setOffline); return () => { window.removeEventListener('online', setOnline); window.removeEventListener('offline', setOffline); }; }, []); return isOnline; } // Usage in a component function CartButton() { const isOnline = useOnlineStatus(); return ( <button disabled={!isOnline} title={isOnline ? undefined : 'You are offline'}> Add to Cart </button> ); }
示例
Lighthouse PWA 审核清单(自动化)
# Install Lighthouse CLI
npm install -g lighthouse
# Audit PWA criteria
lighthouse https://mystore.com --preset=desktop --only-categories=pwa --output=json --output-path=./lighthouse-pwa.json
# Key scores to target:
# - "Installable" checks: manifest, service worker, HTTPS
# - "PWA Optimized" checks: themed address bar, offline page, mobile viewport
用于离线浏览的 IndexedDB 商品目录缓存
import {openDB} from 'idb';
const db = await openDB('catalog-db', 1, {
upgrade(db) {
db.createObjectStore('products', {keyPath: 'id'});
db.createObjectStore('collections', {keyPath: 'id'});
},
});
// Store products when user browses online
export async function cacheProductsLocally(products: Product[]) {
const tx = db.transaction('products', 'readwrite');
await Promise.all([...products.map(p => tx.store.put(p)), tx.done]);
}
// Retrieve from IDB when offline
export async function getProductFromCache(id: string): Promise<Product | null> {
return db.get('products', id) ?? null;
}
最佳实践
- 对商品数据使用
StaleWhileRevalidate— 用户会立即看到缓存内容,同时 Service Worker 在后台获取最新数据 - 绝不缓存购物车或结账页面 — 这些页面必须始终保持最新;对
/cart、/checkout和账户页面使用NetworkOnly策略 - 为 Service Worker 缓存命名时加入版本号 — 更新应用时递增缓存名称,以便自动清除过期资源
- 在 Chrome DevTools 中测试离线模式 — 使用 Network 标签页中的“Offline”限速选项,在部署前验证离线体验
- 仅生成一次 VAPID 密钥并妥善保管 — 丢失 VAPID 私钥意味着所有现有推送订阅都会失效;请将密钥存储在密钥管理器中
- 在适当的情境下请求推送权限 — 仅在相关操作之后(例如下单或用户关注到货提醒时)请求用户允许通知,以提高选择加入率
- 设置合理的缓存大小限制 — 使用带有
maxEntries的ExpirationPlugin,避免 Service Worker 缓存占用过多设备存储空间
常见陷阱
| 问题 | 解决方案 |
|---|---|
| 部署后 Service Worker 未更新 | 使用 registerType: 'autoUpdate',并在 Service Worker 中调用 skipWaiting() 以立即接管;显示“有新版本可用”提示 |
| iOS 上未显示推送通知 | iOS 要求用户先将 PWA 添加到主屏幕;iOS Safari 上的 Web Push 要求 iOS 16.4+ 且显示模式为 standalone |
| 价格变更后仍提供缓存的 API 响应 | 适当设置 maxAgeSeconds;通过在部署时更新缓存名称来按需失效缓存 |
| 后台同步静默失败 | 将后台同步包装在 try/catch 中并记录错误;通过 DevTools 的 Application → Background Sync 面板进行测试 |
| 应用无法通过 Lighthouse 安装性审计 | 检查以下项目:HTTPS、包含 512×512 可遮罩图标的有效清单、已注册的 Service Worker,以及响应状态为 200 的 start_url |
相关技能
- @jamstack-storefront
- @image-optimization-cdn
- @edge-commerce
- @monitoring-alerting-commerce