mcpField
P
pwa-setup

pwa-setup

github.com/dadbodgeoff开发79240 次安装10 周前更新
代码仓库

关于 pwa-setup

pwa-setup 是一项用于配置渐进式 Web 应用的开发技能,适合希望提供可安装、类应用浏览体验的 Web 团队。内容涵盖清单、移动端显示、安全区域和安装提示,并提供 Next.js 示例。

主要功能

  1. 01配置 Web App Manifest
  2. 02设置移动端元数据
  3. 03处理刘海屏安全区域
  4. 04安装提示 Hook 与横幅
  5. 05PWA 图标要求

适用场景

  1. 01让 Web 应用支持安装
  2. 02为移动用户提供主屏幕入口
  3. 03适配刘海屏布局

安装

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

$npx skills add https://github.com/dadbodgeoff/drift --skill pwa-setup

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

详细介绍

PWA 配置

配置渐进式 Web 应用,带来类应用的浏览器体验。

何时使用此技能

  • 用户想要“安装”你的 Web 应用
  • 移动端用户需要从主屏幕访问
  • 需要实现类应用行为,但不想开发原生应用
  • 需要支持带刘海的设备(iPhone 等)

核心概念

PWA 需要:

  1. Web 应用清单 - 应用元数据和图标
  2. 移动端 meta 标签 - 视口和主题配置
  3. 安全区域 - 适配带刘海的设备
  4. 安装提示 - 自定义安装体验

实现

TypeScript(Next.js)
// app/manifest.ts
import type { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "My SaaS App",
    short_name: "MySaaS",
    description: "Your app description here",
    start_url: '/dashboard',
    display: 'standalone',
    background_color: '#0f172a',
    theme_color: '#14b8a6',
    orientation: 'portrait-primary',
    
    icons: [
      {
        src: '/icons/icon-192.png',
        sizes: '192x192',
        type: 'image/png',
        purpose: 'any',
      },
      {
        src: '/icons/icon-512.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'any',
      },
      {
        src: '/icons/icon-maskable.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'maskable',
      },
    ],
    
    shortcuts: [
      { name: 'Dashboard', url: '/dashboard', description: 'Go to dashboard' },
      { name: 'Settings', url: '/settings', description: 'App settings' },
    ],
    
    categories: ['productivity', 'utilities'],
  };
}
根布局元数据
// app/layout.tsx
import type { Metadata, Viewport } from 'next';

export const metadata: Metadata = {
  title: "My SaaS App",
  description: "Your app description",
  
  appleWebApp: {
    capable: true,
    statusBarStyle: 'black-translucent',
    title: "MySaaS",
  },
  
  applicationName: "MySaaS",
  
  openGraph: {
    title: "My SaaS App",
    description: "Your app description",
    type: 'website',
    siteName: "MySaaS",
  },
};

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 1,
  userScalable: false,
  themeColor: '#14b8a6',
  viewportFit: 'cover',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="bg-neutral-900 text-neutral-cream min-h-screen">
        {children}
      </body>
    </html>
  );
}
安全区域 CSS
/* globals.css */

/* Safe area for bottom navigation (notched devices) */
.safe-area-bottom {
  padding-bottom: env(safe-area-inset-bottom, 0);
}

/* Safe area for top header */
.safe-area-top {
  padding-top: env(safe-area-inset-top, 0);
}

/* Full safe area padding */
.safe-area-all {
  padding-top: env(safe-area-inset-top, 0);
  padding-right: env(safe-area-inset-right, 0);
  padding-bottom: env(safe-area-inset-bottom, 0);
  padding-left: env(safe-area-inset-left, 0);
}
安装提示 Hook
// hooks/useInstallPrompt.ts
'use client';

import { useState, useEffect } from 'react';

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export function useInstallPrompt() {
  const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
  const [isInstalled, setIsInstalled] = useState(false);

  useEffect(() => {
    if (window.matchMedia('(display-mode: standalone)').matches) {
      setIsInstalled(true);
      return;
    }

    const handler = (e: Event) => {
      e.preventDefault();
      setInstallPrompt(e as BeforeInstallPromptEvent);
    };

    window.addEventListener('beforeinstallprompt', handler);
    return () => window.removeEventListener('beforeinstallprompt', handler);
  }, []);

  const promptInstall = async () => {
    if (!installPrompt) return false;

    await installPrompt.prompt();
    const { outcome } = await installPrompt.userChoice;

    if (outcome === 'accepted') {
      setIsInstalled(true);
      setInstallPrompt(null);
    }

    return outcome === 'accepted';
  };

  return {
    canInstall: !!installPrompt && !isInstalled,
    isInstalled,
    promptInstall,
  };
}
安装横幅组件
// components/InstallBanner.tsx
'use client';

import { useInstallPrompt } from '@/hooks/useInstallPrompt';

export function InstallBanner() {
  const { canInstall, promptInstall } = useInstallPrompt();

  if (!canInstall) return null;

  return (
    <div className="fixed bottom-20 left-4 right-4 bg-primary-600 text-white p-4 rounded-lg shadow-lg md:hidden">
      <p className="text-sm mb-2">Install our app for a better experience</p>
      <button
        onClick={promptInstall}
        className="w-full py-2 bg-white text-primary-600 rounded font-medium"
      >
        Install App
      </button>
    </div>
  );
}
带安全区域的移动端导航
// components/MobileNav.tsx
export function MobileNav() {
  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-neutral-800 border-t border-neutral-700 z-30 md:hidden safe-area-bottom">
      <div className="flex justify-around py-2">
        <NavItem href="/dashboard" icon={HomeIcon} label="Home" />
        <NavItem href="/search" icon={SearchIcon} label="Search" />
        <NavItem href="/settings" icon={SettingsIcon} label="Settings" />
      </div>
    </nav>
  );
}

图标要求

public/
├── icons/
│   ├── icon-192.png      # Standard icon
│   ├── icon-512.png      # Large icon
│   └── icon-maskable.png # Maskable (with safe zone padding)
├── favicon.ico
└── apple-touch-icon.png  # 180x180 for iOS
可遮罩图标安全区域
┌─────────────────────┐
│                     │
│   ┌───────────┐     │
│   │   LOGO    │     │  ← Content in center 80%
│   └───────────┘     │
│                     │
└─────────────────────┘

使用示例

测试 PWA
  1. Chrome DevTools → Application → Manifest
  2. Lighthouse → PWA 审核
  3. 移动端 → 添加到主屏幕
检测独立模式
function isStandalone(): boolean {
  return window.matchMedia('(display-mode: standalone)').matches ||
         (window.navigator as any).standalone === true;
}

最佳实践

  1. 使用带安全区域的可遮罩图标
  2. 设置 theme_color 以匹配品牌
  3. 为带刘海的设备处理安全区域
  4. 在合适的时机提供安装提示
  5. 在真实移动设备上进行测试

常见错误

  • 缺少可遮罩图标(在 Android 上显示难看)
  • 未处理安全区域(内容延伸到刘海下方)
  • 立即显示安装提示(令人厌烦)
  • start_url 错误(打开了错误的页面)
  • 缺少 apple-touch-icon(iOS 回退方案)

相关模式

  • design-tokens - 统一主题风格
  • mobile-components - 响应式组件

评论

支持 Markdown