Web平台 #

一、Web平台概述 #

1.1 Web平台特点 #

Capacitor Web平台允许你的应用在浏览器中运行,提供与原生平台一致的API体验。

特点 说明
无需打包 直接部署到Web服务器
PWA支持 可安装为渐进式Web应用
API一致 与原生平台相同的JavaScript API
快速开发 支持热重载开发

1.2 平台检测 #

typescript
import { Capacitor } from '@capacitor/core';

const platform = Capacitor.getPlatform();
// 'ios' | 'android' | 'web'

const isWeb = Capacitor.getPlatform() === 'web';
const isNative = Capacitor.isNativePlatform();

二、PWA配置 #

2.1 创建manifest.json #

json
// public/manifest.json
{
    "name": "My App",
    "short_name": "MyApp",
    "description": "A Capacitor application",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#4a90d9",
    "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"
        }
    ],
    "screenshots": [
        {
            "src": "/screenshots/screenshot1.png",
            "sizes": "1280x720",
            "type": "image/png"
        }
    ],
    "categories": ["utilities", "productivity"],
    "shortcuts": [
        {
            "name": "New Task",
            "short_name": "New",
            "description": "Create a new task",
            "url": "/new",
            "icons": [{ "src": "/icons/new-task.png", "sizes": "96x96" }]
        }
    ]
}

2.2 HTML配置 #

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    
    <!-- PWA配置 -->
    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#4a90d9">
    <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="My App">
    
    <!-- iOS图标 -->
    <link rel="apple-touch-icon" href="/icons/icon-192x192.png">
    
    <!-- Favicon -->
    <link rel="icon" type="image/png" href="/icons/favicon.png">
    
    <title>My App</title>
</head>
<body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
</body>
</html>

三、Service Worker #

3.1 使用Vite PWA插件 #

bash
npm install vite-plugin-pwa -D

3.2 配置Vite PWA #

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
    plugins: [
        react(),
        VitePWA({
            registerType: 'autoUpdate',
            includeAssets: ['favicon.ico', 'icons/*.png'],
            manifest: {
                name: 'My App',
                short_name: 'MyApp',
                description: 'A Capacitor application',
                theme_color: '#4a90d9',
                background_color: '#ffffff',
                display: 'standalone',
                icons: [
                    {
                        src: '/icons/icon-192x192.png',
                        sizes: '192x192',
                        type: 'image/png'
                    },
                    {
                        src: '/icons/icon-512x512.png',
                        sizes: '512x512',
                        type: 'image/png'
                    }
                ]
            },
            workbox: {
                globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
                runtimeCaching: [
                    {
                        urlPattern: /^https:\/\/api\.example\.com\/.*/i,
                        handler: 'NetworkFirst',
                        options: {
                            cacheName: 'api-cache',
                            expiration: {
                                maxEntries: 100,
                                maxAgeSeconds: 60 * 60 * 24
                            }
                        }
                    }
                ]
            }
        })
    ]
});

3.3 手动注册Service Worker #

typescript
// src/main.tsx
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => {
                console.log('SW registered:', registration);
            })
            .catch(error => {
                console.log('SW registration failed:', error);
            });
    });
}

四、响应式设计 #

4.1 安全区域适配 #

css
/* 安全区域变量 */
:root {
    --safe-area-inset-top: env(safe-area-inset-top);
    --safe-area-inset-bottom: env(safe-area-inset-bottom);
    --safe-area-inset-left: env(safe-area-inset-left);
    --safe-area-inset-right: env(safe-area-inset-right);
}

/* 应用安全区域 */
body {
    padding-top: var(--safe-area-inset-top);
    padding-bottom: var(--safe-area-inset-bottom);
    padding-left: var(--safe-area-inset-left);
    padding-right: var(--safe-area-inset-right);
}

/* 固定底部元素 */
.bottom-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding-bottom: var(--safe-area-inset-bottom);
}

4.2 响应式布局 #

css
/* 移动优先 */
.container {
    padding: 16px;
}

/* 平板 */
@media (min-width: 768px) {
    .container {
        padding: 24px;
        max-width: 720px;
        margin: 0 auto;
    }
}

/* 桌面 */
@media (min-width: 1024px) {
    .container {
        padding: 32px;
        max-width: 960px;
    }
}

/* 大屏 */
@media (min-width: 1280px) {
    .container {
        max-width: 1200px;
    }
}

4.3 触摸友好 #

css
/* 最小触摸目标 */
button, a, .clickable {
    min-height: 44px;
    min-width: 44px;
}

/* 禁用触摸高亮 */
* {
    -webkit-tap-highlight-color: transparent;
}

/* 平滑滚动 */
html {
    scroll-behavior: smooth;
}

/* 触摸滚动 */
.scrollable {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}

五、Web插件实现 #

5.1 插件Web实现模式 #

typescript
// src/plugins/my-plugin/web.ts
import { WebPlugin } from '@capacitor/core';
import type { MyPlugin } from './definitions';

export class MyPluginWeb extends WebPlugin implements MyPlugin {
    async doSomething(options: { param: string }): Promise<{ result: string }> {
        // Web特定实现
        console.log('Web implementation:', options);
        
        return {
            result: `Web: ${options.param}`
        };
    }
}

5.2 使用Web API #

typescript
// 相机Web实现示例
export class CameraWeb extends WebPlugin implements CameraPlugin {
    async getPhoto(options: CameraOptions): Promise<Photo> {
        // 使用input[type=file]
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'image/*';
        
        if (options.source === CameraSource.Camera) {
            input.capture = 'environment';
        }
        
        return new Promise((resolve, reject) => {
            input.onchange = async (event) => {
                const file = (event.target as HTMLInputElement).files?.[0];
                if (!file) {
                    reject(new Error('No file selected'));
                    return;
                }
                
                const dataUrl = await this.readFileAsDataURL(file);
                resolve({
                    dataUrl,
                    format: 'jpeg'
                });
            };
            
            input.click();
        });
    }
    
    private readFileAsDataURL(file: File): Promise<string> {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result as string);
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }
}

5.3 存储Web实现 #

typescript
// Preferences Web实现
export class PreferencesWeb extends WebPlugin implements PreferencesPlugin {
    private prefix = '_cap_';
    
    async get(options: { key: string }): Promise<{ value: string | null }> {
        const value = localStorage.getItem(this.prefix + options.key);
        return { value };
    }
    
    async set(options: { key: string; value: string }): Promise<void> {
        localStorage.setItem(this.prefix + options.key, options.value);
    }
    
    async remove(options: { key: string }): Promise<void> {
        localStorage.removeItem(this.prefix + options.key);
    }
    
    async clear(): Promise<void> {
        for (const key of Object.keys(localStorage)) {
            if (key.startsWith(this.prefix)) {
                localStorage.removeItem(key);
            }
        }
    }
    
    async keys(): Promise<{ keys: string[] }> {
        const keys = Object.keys(localStorage)
            .filter(k => k.startsWith(this.prefix))
            .map(k => k.slice(this.prefix.length));
        return { keys };
    }
}

六、平台适配 #

6.1 条件渲染 #

tsx
import { Capacitor } from '@capacitor/core';

function MyComponent() {
    const isWeb = Capacitor.getPlatform() === 'web';
    
    return (
        <div>
            {isWeb ? (
                <WebFeature />
            ) : (
                <NativeFeature />
            )}
        </div>
    );
}

6.2 功能降级 #

typescript
import { Camera, CameraSource } from '@capacitor/camera';

async function takePhoto() {
    try {
        const photo = await Camera.getPhoto({
            quality: 90,
            source: CameraSource.Camera,
            resultType: 'uri'
        });
        
        return photo;
    } catch (error) {
        // Web平台可能不支持某些功能
        console.warn('Camera not available, using fallback');
        return fallbackPhotoInput();
    }
}

async function fallbackPhotoInput() {
    // 使用HTML input作为降级方案
    return new Promise((resolve) => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'image/*';
        input.onchange = (e) => {
            const file = (e.target as HTMLInputElement).files?.[0];
            if (file) {
                resolve({ webPath: URL.createObjectURL(file) });
            }
        };
        input.click();
    });
}

6.3 平台特定样式 #

typescript
// 添加平台类名
import { Capacitor } from '@capacitor/core';

document.body.classList.add(`platform-${Capacitor.getPlatform()}`);

if (Capacitor.isNativePlatform()) {
    document.body.classList.add('platform-native');
}
css
/* 平台特定样式 */
.platform-ios .header {
    padding-top: env(safe-area-inset-top);
}

.platform-android .header {
    padding-top: 24px;
}

.platform-web .header {
    padding-top: 16px;
}

七、部署配置 #

7.1 静态托管 #

bash
# 构建
npm run build

# 部署到静态服务器
# - Nginx
# - Apache
# - Vercel
# - Netlify
# - GitHub Pages

7.2 Nginx配置 #

nginx
server {
    listen 80;
    server_name example.com;
    root /var/www/app;
    index index.html;
    
    # SPA路由
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # 缓存静态资源
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # Service Worker不缓存
    location /sw.js {
        add_header Cache-Control "no-cache";
    }
}

7.3 Vercel配置 #

json
// vercel.json
{
    "rewrites": [
        { "source": "/(.*)", "destination": "/index.html" }
    ],
    "headers": [
        {
            "source": "/sw.js",
            "headers": [
                { "key": "Cache-Control", "value": "no-cache" }
            ]
        }
    ]
}

八、性能优化 #

8.1 代码分割 #

typescript
// 路由懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

8.2 图片优化 #

typescript
// 使用WebP格式
<picture>
    <source srcset="/images/photo.webp" type="image/webp">
    <img src="/images/photo.jpg" alt="Photo">
</picture>

// 懒加载
<img loading="lazy" src="/images/photo.jpg" alt="Photo">

8.3 预加载 #

html
<link rel="preload" href="/fonts/font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preconnect" href="https://api.example.com">

九、总结 #

9.1 Web平台要点 #

要点 说明
PWA 支持安装和离线
响应式 适配各种屏幕
降级 功能降级处理
性能 优化加载速度

9.2 下一步 #

了解Web平台后,让我们学习 原生代码调用

最后更新:2026-03-28