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