Appearance
Capacitor混合开发 - 现代跨平台应用开发
1. Capacitor简介
1.1 什么是Capacitor
typescript
const capacitorOverview = {
definition: 'Ionic团队开发的跨平台运行时',
features: {
webNative: '将Web应用打包为原生应用',
plugins: '统一的原生功能API',
codesharing: '与Web应用共享代码',
customNative: '支持自定义原生代码',
liveReload: '热更新支持'
},
advantages: [
'现代化的API设计',
'优秀的TypeScript支持',
'活跃的社区',
'灵活的插件系统',
'与Web技术无缝集成'
],
vsOthers: {
cordova: '更现代,API更简洁,性能更好',
reactNative: '基于Web,学习成本低,代码复用率高',
flutter: '使用熟悉的Web技术栈'
}
};1.2 核心概念
typescript
// Capacitor架构
const capacitorArchitecture = {
layers: {
webLayer: {
description: 'Web应用层',
tech: ['React', 'Vue', 'Angular', '原生JS'],
build: 'Vite/Webpack等构建工具'
},
bridgeLayer: {
description: '桥接层',
role: 'Web与Native通信',
api: 'Capacitor Core API'
},
nativeLayer: {
description: '原生层',
platforms: ['iOS (Swift)', 'Android (Kotlin/Java)'],
access: '通过插件访问原生功能'
}
},
plugins: {
official: [
'Camera', 'Filesystem', 'Geolocation',
'Storage', 'Network', 'Device'
],
community: [
'@capacitor-community/*',
'第三方插件',
'自定义插件'
]
}
};2. 项目初始化
2.1 安装和配置
bash
# 创建React应用
npm create vite@latest my-app -- --template react-ts
cd my-app
# 安装Capacitor
npm install @capacitor/core @capacitor/cli
# 初始化Capacitor
npx cap init
# 添加平台
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
# 安装常用插件
npm install @capacitor/camera @capacitor/filesystem @capacitor/storage2.2 配置文件
typescript
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'My App',
webDir: 'dist',
server: {
// 开发环境配置
url: process.env.DEV_SERVER_URL,
cleartext: true
},
plugins: {
SplashScreen: {
launchShowDuration: 2000,
backgroundColor: '#ffffff',
showSpinner: true,
spinnerColor: '#2196F3'
},
Keyboard: {
resize: 'body',
style: 'dark',
resizeOnFullScreen: true
},
StatusBar: {
style: 'light',
backgroundColor: '#2196F3'
}
},
ios: {
contentInset: 'always'
},
android: {
allowMixedContent: true,
captureInput: true,
webContentsDebuggingEnabled: true
}
};
export default config;2.3 构建配置
json
// package.json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"cap:sync": "cap sync",
"cap:open:ios": "cap open ios",
"cap:open:android": "cap open android",
"ios:dev": "npm run build && cap sync ios && cap open ios",
"android:dev": "npm run build && cap sync android && cap open android",
"ios:build": "npm run build && cap sync ios && cap build ios",
"android:build": "npm run build && cap sync android && cap build android"
}
}3. 核心插件使用
3.1 Camera相机插件
typescript
// src/hooks/useCamera.ts
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { useState } from 'react';
export function useCamera() {
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
const takePicture = async () => {
setLoading(true);
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 90,
allowEditing: true,
width: 1024,
height: 1024
});
setImage(photo.webPath);
return photo;
} catch (error) {
console.error('Camera error:', error);
throw error;
} finally {
setLoading(false);
}
};
const pickFromGallery = async () => {
setLoading(true);
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Photos,
quality: 90
});
setImage(photo.webPath);
return photo;
} catch (error) {
console.error('Gallery error:', error);
throw error;
} finally {
setLoading(false);
}
};
return {
image,
loading,
takePicture,
pickFromGallery
};
}
// 使用
function CameraExample() {
const { image, loading, takePicture, pickFromGallery } = useCamera();
return (
<div>
{image && <img src={image} alt="Captured" />}
<button onClick={takePicture} disabled={loading}>
{loading ? '处理中...' : '拍照'}
</button>
<button onClick={pickFromGallery} disabled={loading}>
从相册选择
</button>
</div>
);
}3.2 Filesystem文件系统
typescript
// src/services/fileService.ts
import {
Filesystem,
Directory,
Encoding
} from '@capacitor/filesystem';
export class FileService {
// 写入文件
async writeFile(
filename: string,
data: string,
directory: Directory = Directory.Documents
) {
try {
await Filesystem.writeFile({
path: filename,
data,
directory,
encoding: Encoding.UTF8
});
return { success: true };
} catch (error) {
console.error('Write file error:', error);
throw error;
}
}
// 读取文件
async readFile(
filename: string,
directory: Directory = Directory.Documents
) {
try {
const result = await Filesystem.readFile({
path: filename,
directory,
encoding: Encoding.UTF8
});
return result.data;
} catch (error) {
console.error('Read file error:', error);
throw error;
}
}
// 删除文件
async deleteFile(
filename: string,
directory: Directory = Directory.Documents
) {
try {
await Filesystem.deleteFile({
path: filename,
directory
});
return { success: true };
} catch (error) {
console.error('Delete file error:', error);
throw error;
}
}
// 列出目录文件
async listFiles(directory: Directory = Directory.Documents) {
try {
const result = await Filesystem.readdir({
path: '',
directory
});
return result.files;
} catch (error) {
console.error('List files error:', error);
throw error;
}
}
// 下载文件
async downloadFile(url: string, filename: string) {
try {
const response = await fetch(url);
const blob = await response.blob();
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onloadend = async () => {
const base64data = reader.result as string;
await Filesystem.writeFile({
path: filename,
data: base64data,
directory: Directory.Documents
});
resolve({ success: true, path: filename });
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error('Download file error:', error);
throw error;
}
}
}3.3 Storage本地存储
typescript
// src/hooks/useCapacitorStorage.ts
import { Storage } from '@capacitor/storage';
import { useState, useEffect } from 'react';
export function useCapacitorStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(initialValue);
const [loading, setLoading] = useState(true);
// 加载存储的值
useEffect(() => {
const loadValue = async () => {
try {
const { value } = await Storage.get({ key });
if (value) {
setStoredValue(JSON.parse(value));
}
} catch (error) {
console.error('Storage get error:', error);
} finally {
setLoading(false);
}
};
loadValue();
}, [key]);
// 保存值
const setValue = async (value: T) => {
try {
setStoredValue(value);
await Storage.set({
key,
value: JSON.stringify(value)
});
} catch (error) {
console.error('Storage set error:', error);
throw error;
}
};
// 删除值
const removeValue = async () => {
try {
setStoredValue(initialValue);
await Storage.remove({ key });
} catch (error) {
console.error('Storage remove error:', error);
throw error;
}
};
// 清空所有存储
const clearAll = async () => {
try {
await Storage.clear();
} catch (error) {
console.error('Storage clear error:', error);
throw error;
}
};
return {
value: storedValue,
setValue,
removeValue,
clearAll,
loading
};
}
// 使用
function StorageExample() {
const { value, setValue, removeValue } = useCapacitorStorage('user', null);
return (
<div>
<p>User: {value?.name}</p>
<button onClick={() => setValue({ name: 'John' })}>
Save User
</button>
<button onClick={removeValue}>
Remove User
</button>
</div>
);
}3.4 Geolocation地理位置
typescript
// src/hooks/useGeolocation.ts
import { Geolocation, Position } from '@capacitor/geolocation';
import { useState, useEffect } from 'react';
export function useGeolocation() {
const [position, setPosition] = useState<Position | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
// 获取当前位置
const getCurrentPosition = async () => {
setLoading(true);
setError(null);
try {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
});
setPosition(position);
return position;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
};
// 监听位置变化
const watchPosition = () => {
const watchId = Geolocation.watchPosition(
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
},
(position, err) => {
if (err) {
setError(err.message);
} else if (position) {
setPosition(position);
}
}
);
return () => {
Geolocation.clearWatch({ id: watchId });
};
};
return {
position,
error,
loading,
getCurrentPosition,
watchPosition
};
}
// 使用
function GeolocationExample() {
const { position, error, loading, getCurrentPosition } = useGeolocation();
return (
<div>
<button onClick={getCurrentPosition} disabled={loading}>
{loading ? '获取中...' : '获取位置'}
</button>
{error && <p>Error: {error}</p>}
{position && (
<div>
<p>纬度: {position.coords.latitude}</p>
<p>经度: {position.coords.longitude}</p>
<p>精度: {position.coords.accuracy}m</p>
</div>
)}
</div>
);
}3.5 Network网络状态
typescript
// src/hooks/useNetwork.ts
import { Network } from '@capacitor/network';
import { useState, useEffect } from 'react';
export function useNetwork() {
const [status, setStatus] = useState<{
connected: boolean;
connectionType: string;
}>({
connected: true,
connectionType: 'unknown'
});
useEffect(() => {
// 获取当前状态
Network.getStatus().then(setStatus);
// 监听状态变化
const handler = Network.addListener('networkStatusChange', setStatus);
return () => {
handler.remove();
};
}, []);
return status;
}
// 使用
function NetworkExample() {
const { connected, connectionType } = useNetwork();
return (
<div>
<p>状态: {connected ? '已连接' : '未连接'}</p>
<p>类型: {connectionType}</p>
{!connected && (
<div className="offline-banner">
您当前处于离线状态
</div>
)}
</div>
);
}3.6 Device设备信息
typescript
// src/hooks/useDevice.ts
import { Device, DeviceInfo } from '@capacitor/device';
import { useState, useEffect } from 'react';
export function useDevice() {
const [info, setInfo] = useState<DeviceInfo | null>(null);
useEffect(() => {
const getInfo = async () => {
const deviceInfo = await Device.getInfo();
setInfo(deviceInfo);
};
getInfo();
}, []);
return info;
}
// 获取设备ID
export async function getDeviceId() {
const { identifier } = await Device.getId();
return identifier;
}
// 获取电池信息
export async function getBatteryInfo() {
const batteryInfo = await Device.getBatteryInfo();
return {
level: batteryInfo.batteryLevel,
isCharging: batteryInfo.isCharging
};
}
// 使用
function DeviceExample() {
const device = useDevice();
if (!device) return <div>Loading...</div>;
return (
<div>
<p>平台: {device.platform}</p>
<p>操作系统: {device.operatingSystem}</p>
<p>版本: {device.osVersion}</p>
<p>型号: {device.model}</p>
<p>制造商: {device.manufacturer}</p>
</div>
);
}4. 社区插件
4.1 安装社区插件
bash
# HTTP插件
npm install @capacitor-community/http
# SQLite数据库
npm install @capacitor-community/sqlite
# Admob广告
npm install @capacitor-community/admob
# Stripe支付
npm install @capacitor-community/stripe
# FCM推送
npm install @capacitor-community/fcm4.2 HTTP插件使用
typescript
// src/services/httpService.ts
import { Http } from '@capacitor-community/http';
export class CapacitorHttpService {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async get<T>(url: string, params?: any): Promise<T> {
const response = await Http.get({
url: `${this.baseURL}${url}`,
params,
headers: {
'Content-Type': 'application/json'
}
});
return response.data;
}
async post<T>(url: string, data?: any): Promise<T> {
const response = await Http.post({
url: `${this.baseURL}${url}`,
data,
headers: {
'Content-Type': 'application/json'
}
});
return response.data;
}
// 下载文件
async downloadFile(url: string, filePath: string) {
const response = await Http.downloadFile({
url,
filePath,
method: 'GET'
});
return response.path;
}
// 上传文件
async uploadFile(url: string, filePath: string, name: string) {
const response = await Http.uploadFile({
url,
name,
filePath,
method: 'POST'
});
return response.data;
}
}4.3 SQLite数据库
typescript
// src/services/databaseService.ts
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
export class DatabaseService {
private sqlite: SQLiteConnection;
private db: any;
async init() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
this.db = await this.sqlite.createConnection(
'mydb',
false,
'no-encryption',
1
);
await this.db.open();
await this.createTables();
}
private async createTables() {
const createTableSQL = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
await this.db.execute(createTableSQL);
}
async getUsers() {
const result = await this.db.query('SELECT * FROM users');
return result.values;
}
async createUser(name: string, email: string) {
const result = await this.db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email]
);
return result.changes?.lastId;
}
async updateUser(id: number, name: string, email: string) {
await this.db.run(
'UPDATE users SET name = ?, email = ? WHERE id = ?',
[name, email, id]
);
}
async deleteUser(id: number) {
await this.db.run('DELETE FROM users WHERE id = ?', [id]);
}
async close() {
await this.db.close();
}
}5. 自定义插件开发
5.1 创建插件
bash
# 创建插件
npm init @capacitor/plugin my-plugin
# 进入插件目录
cd my-plugin
# 安装依赖
npm install5.2 定义插件接口
typescript
// src/definitions.ts
export interface MyPluginPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
showToast(options: {
text: string;
duration?: 'short' | 'long'
}): Promise<void>;
vibrate(options: { duration?: number }): Promise<void>;
}5.3 Web实现
typescript
// src/web.ts
import { WebPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';
export class MyPluginWeb extends WebPlugin implements MyPluginPlugin {
async echo(options: { value: string }): Promise<{ value: string }> {
console.log('ECHO', options);
return options;
}
async showToast(options: { text: string; duration?: 'short' | 'long' }): Promise<void> {
// Web实现
const toast = document.createElement('div');
toast.textContent = options.text;
toast.className = 'toast';
document.body.appendChild(toast);
setTimeout(() => {
document.body.removeChild(toast);
}, options.duration === 'long' ? 3000 : 2000);
}
async vibrate(options: { duration?: number }): Promise<void> {
if ('vibrate' in navigator) {
navigator.vibrate(options.duration || 200);
}
}
}5.4 iOS实现
swift
// ios/Plugin/MyPlugin.swift
import Foundation
import Capacitor
@objc(MyPlugin)
public class MyPlugin: CAPPlugin {
@objc func echo(_ call: CAPPluginCall) {
let value = call.getString("value") ?? ""
call.resolve(["value": value])
}
@objc func showToast(_ call: CAPPluginCall) {
let text = call.getString("text") ?? ""
let duration = call.getString("duration") ?? "short"
DispatchQueue.main.async {
// 显示Toast的实现
}
call.resolve()
}
@objc func vibrate(_ call: CAPPluginCall) {
let duration = call.getInt("duration") ?? 200
// 震动实现
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
call.resolve()
}
}5.5 Android实现
java
// android/src/main/java/com/example/myplugin/MyPlugin.java
package com.example.myplugin;
import android.os.Vibrator;
import android.widget.Toast;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "MyPlugin")
public class MyPlugin extends Plugin {
@PluginMethod
public void echo(PluginCall call) {
String value = call.getString("value");
JSObject ret = new JSObject();
ret.put("value", value);
call.resolve(ret);
}
@PluginMethod
public void showToast(PluginCall call) {
String text = call.getString("text");
String duration = call.getString("duration", "short");
int toastDuration = duration.equals("long")
? Toast.LENGTH_LONG
: Toast.LENGTH_SHORT;
Toast.makeText(
getContext(),
text,
toastDuration
).show();
call.resolve();
}
@PluginMethod
public void vibrate(PluginCall call) {
int duration = call.getInt("duration", 200);
Vibrator vibrator = (Vibrator) getContext()
.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
vibrator.vibrate(duration);
}
call.resolve();
}
}6. 平台特定代码
6.1 iOS配置
xml
<!-- ios/App/App/Info.plist -->
<dict>
<!-- 相机权限 -->
<key>NSCameraUsageDescription</key>
<string>需要访问相机以拍照</string>
<!-- 相册权限 -->
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择照片</string>
<!-- 位置权限 -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要访问您的位置</string>
<!-- 麦克风权限 -->
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风以录音</string>
</dict>6.2 Android配置
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest>
<!-- 权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<application>
<!-- 文件访问权限 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>7. 调试和测试
7.1 Web调试
bash
# 启动开发服务器
npm run dev
# 在浏览器中测试
# http://localhost:51737.2 iOS调试
bash
# 构建并打开Xcode
npm run build
npx cap sync ios
npx cap open ios
# 在Xcode中运行
# Product -> Run7.3 Android调试
bash
# 构建并打开Android Studio
npm run build
npx cap sync android
npx cap open android
# 在Android Studio中运行
# Run -> Run 'app'7.4 Live Reload
typescript
// capacitor.config.ts - 开发配置
const config: CapacitorConfig = {
server: {
url: 'http://192.168.1.100:5173', // 你的开发服务器地址
cleartext: true
}
};
// 然后运行
// npm run dev
// npx cap sync
// 在设备上运行应用8. 最佳实践
typescript
const capacitorBestPractices = {
development: [
'使用TypeScript',
'模块化插件封装',
'统一错误处理',
'添加Loading状态',
'权限检查'
],
performance: [
'按需加载插件',
'优化Web资源',
'使用原生导航',
'缓存策略',
'减少桥接调用'
],
platform: [
'平台特定优化',
'统一的UI/UX',
'处理平台差异',
'测试所有平台',
'遵循平台规范'
],
security: [
'安全存储敏感数据',
'HTTPS通信',
'代码混淆',
'权限最小化',
'输入验证'
]
};9. 总结
Capacitor混合开发的核心要点:
- 核心插件: Camera, Filesystem, Storage等
- 社区插件: HTTP, SQLite等扩展
- 自定义插件: 满足特殊需求
- 平台配置: iOS和Android特定设置
- 调试方法: Web, iOS, Android调试
- 性能优化: 减少桥接,优化资源
- 最佳实践: 模块化,类型安全,错误处理
掌握Capacitor可以高效构建跨平台应用。