mcpField
P
pwa-storefront

pwa-storefront

github.com/finsilabs开发6074 次安装6 天前更新
代码仓库

关于 pwa-storefront

该技能指导开发者为自定义或无头电商前端构建渐进式 Web 应用商店。内容涵盖离线目录、缓存策略、主屏幕安装及订单通知等实现。

主要功能

  1. 01创建 Web App Manifest
  2. 02使用 Workbox 注册服务工作线程
  3. 03缓存目录并提供离线页面
  4. 04实现 Web Push 通知
  5. 05检测在线状态并更新界面

适用场景

  1. 01为网络不稳定地区提供离线浏览
  2. 02让顾客将商店添加到主屏幕
  3. 03通过缓存加快重复页面加载
  4. 04发送订单状态或补货通知

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill pwa-storefront

安装后在 Claude Code 中输入/pwa-storefront确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

tile.json
evals/push-notifications-and-offline-ux/
criteria.json
task.md
evals/service-worker-caching-strategies/
criteria.json
task.md
evals/web-app-manifest-and-installability/
criteria.json
task.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)

核心说明

  1. 创建 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">
    
  2. 使用 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 */},
        }),
      ],
    };
    
  3. 实现用于离线商品目录的自定义 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'
    );
    
  4. 显示离线备用页面

    // 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>
      );
    }
    
  5. 实现 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));
    });
    
  6. 在 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

评论

支持 Markdown