第一个应用 #

一、项目概述 #

我们将创建一个简单的待办事项应用,涵盖Capacitor开发的核心流程:

  • 使用React + TypeScript构建Web应用
  • 添加iOS和Android平台
  • 使用Capacitor插件访问原生功能
  • 在模拟器和真机上运行调试

二、创建项目 #

2.1 初始化项目 #

bash
# 创建Vite + React + TypeScript项目
npm create vite@latest todo-app -- --template react-ts

# 进入项目目录
cd todo-app

# 安装依赖
npm install

2.2 添加Capacitor #

bash
# 安装Capacitor核心包和CLI
npm install @capacitor/core @capacitor/cli

# 初始化Capacitor
npx cap init "Todo App" "com.example.todoapp"

# 按提示确认配置
# App name: Todo App
# App ID: com.example.todoapp
# Web dir: dist

2.3 项目结构 #

text
todo-app/
├── src/
│   ├── App.tsx
│   ├── App.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── public/
│   └── vite.svg
├── capacitor.config.json
├── package.json
├── tsconfig.json
└── vite.config.ts

三、开发Web应用 #

3.1 安装依赖 #

bash
# 安装UI库(可选)
npm install @ionic/react
npm install ionicons

3.2 创建应用代码 #

App.tsx #

tsx
import { useState } from 'react';
import './App.css';

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

function App() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [inputValue, setInputValue] = useState('');

    const addTodo = () => {
        if (inputValue.trim()) {
            setTodos([
                ...todos,
                {
                    id: Date.now(),
                    text: inputValue.trim(),
                    completed: false
                }
            ]);
            setInputValue('');
        }
    };

    const toggleTodo = (id: number) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };

    const deleteTodo = (id: number) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    return (
        <div className="app">
            <header className="header">
                <h1>Todo App</h1>
            </header>

            <div className="input-container">
                <input
                    type="text"
                    value={inputValue}
                    onChange={(e) => setInputValue(e.target.value)}
                    placeholder="添加新任务..."
                    onKeyPress={(e) => e.key === 'Enter' && addTodo()}
                />
                <button onClick={addTodo}>添加</button>
            </div>

            <ul className="todo-list">
                {todos.map(todo => (
                    <li key={todo.id} className={todo.completed ? 'completed' : ''}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span>{todo.text}</span>
                        <button onClick={() => deleteTodo(todo.id)}>删除</button>
                    </li>
                ))}
            </ul>

            {todos.length === 0 && (
                <p className="empty">暂无任务,添加一个吧!</p>
            )}
        </div>
    );
}

export default App;

App.css #

css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: #f5f5f5;
    min-height: 100vh;
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
}

.app {
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
}

.header {
    text-align: center;
    margin-bottom: 30px;
    padding-top: 20px;
}

.header h1 {
    font-size: 28px;
    color: #333;
}

.input-container {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}

.input-container input {
    flex: 1;
    padding: 12px 16px;
    border: 1px solid #ddd;
    border-radius: 8px;
    font-size: 16px;
    outline: none;
}

.input-container input:focus {
    border-color: #4a90d9;
}

.input-container button {
    padding: 12px 24px;
    background-color: #4a90d9;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 16px;
    cursor: pointer;
}

.input-container button:active {
    background-color: #357abd;
}

.todo-list {
    list-style: none;
}

.todo-list li {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px;
    background: white;
    border-radius: 8px;
    margin-bottom: 10px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.todo-list li.completed span {
    text-decoration: line-through;
    color: #999;
}

.todo-list li input[type="checkbox"] {
    width: 20px;
    height: 20px;
}

.todo-list li span {
    flex: 1;
    font-size: 16px;
}

.todo-list li button {
    padding: 6px 12px;
    background-color: #e74c3c;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 14px;
    cursor: pointer;
}

.empty {
    text-align: center;
    color: #999;
    margin-top: 40px;
}

3.3 配置Vite #

typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [react()],
    build: {
        outDir: 'dist',
        sourcemap: true
    },
    server: {
        host: true,
        port: 5173
    }
});

四、添加原生功能 #

4.1 安装插件 #

bash
# 安装存储插件
npm install @capacitor/preferences

# 安装键盘插件
npm install @capacitor/keyboard

# 安装状态栏插件
npm install @capacitor/status-bar

# 安装启动画面插件
npm install @capacitor/splash-screen

4.2 使用存储插件 #

更新 App.tsx

tsx
import { useState, useEffect } from 'react';
import { Preferences } from '@capacitor/preferences';
import './App.css';

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const STORAGE_KEY = 'todos';

function App() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [inputValue, setInputValue] = useState('');

    useEffect(() => {
        loadTodos();
    }, []);

    useEffect(() => {
        saveTodos();
    }, [todos]);

    const loadTodos = async () => {
        try {
            const { value } = await Preferences.get({ key: STORAGE_KEY });
            if (value) {
                setTodos(JSON.parse(value));
            }
        } catch (error) {
            console.error('加载失败:', error);
        }
    };

    const saveTodos = async () => {
        try {
            await Preferences.set({
                key: STORAGE_KEY,
                value: JSON.stringify(todos)
            });
        } catch (error) {
            console.error('保存失败:', error);
        }
    };

    const addTodo = () => {
        if (inputValue.trim()) {
            setTodos([
                ...todos,
                {
                    id: Date.now(),
                    text: inputValue.trim(),
                    completed: false
                }
            ]);
            setInputValue('');
        }
    };

    const toggleTodo = (id: number) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };

    const deleteTodo = (id: number) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    return (
        <div className="app">
            <header className="header">
                <h1>Todo App</h1>
            </header>

            <div className="input-container">
                <input
                    type="text"
                    value={inputValue}
                    onChange={(e) => setInputValue(e.target.value)}
                    placeholder="添加新任务..."
                    onKeyPress={(e) => e.key === 'Enter' && addTodo()}
                />
                <button onClick={addTodo}>添加</button>
            </div>

            <ul className="todo-list">
                {todos.map(todo => (
                    <li key={todo.id} className={todo.completed ? 'completed' : ''}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span>{todo.text}</span>
                        <button onClick={() => deleteTodo(todo.id)}>删除</button>
                    </li>
                ))}
            </ul>

            {todos.length === 0 && (
                <p className="empty">暂无任务,添加一个吧!</p>
            )}
        </div>
    );
}

export default App;

4.3 配置状态栏和启动画面 #

创建 src/capacitor-setup.ts

typescript
import { StatusBar, Style } from '@capacitor/status-bar';
import { SplashScreen } from '@capacitor/splash-screen';
import { Keyboard } from '@capacitor/keyboard';
import { Capacitor } from '@capacitor/core';

export const setupCapacitor = async () => {
    if (Capacitor.isNativePlatform()) {
        try {
            await StatusBar.setStyle({ style: Style.Dark });
            await StatusBar.setBackgroundColor({ color: '#4a90d9' });
            
            await SplashScreen.hide({
                fadeOutDuration: 500
            });

            Keyboard.addListener('keyboardWillShow', (info) => {
                document.body.style.setProperty('--keyboard-height', `${info.keyboardHeight}px`);
            });

            Keyboard.addListener('keyboardWillHide', () => {
                document.body.style.setProperty('--keyboard-height', '0px');
            });
        } catch (error) {
            console.error('Capacitor setup error:', error);
        }
    }
};

main.tsx 中调用:

tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { setupCapacitor } from './capacitor-setup';
import './index.css';

setupCapacitor();

ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

五、构建和同步 #

5.1 构建Web应用 #

bash
# 构建生产版本
npm run build

5.2 添加平台 #

bash
# 添加iOS平台
npx cap add ios

# 添加Android平台
npx cap add android

5.3 同步资源 #

bash
# 同步所有平台
npx cap sync

# 或单独同步
npx cap sync ios
npx cap sync android

六、iOS运行调试 #

6.1 打开Xcode #

bash
npx cap open ios

6.2 Xcode配置 #

  1. 选择签名团队

    • 打开 App target
    • 进入 Signing & Capabilities
    • 选择你的开发者账号
  2. 配置Info.plist(添加权限)

xml
<!-- ios/App/App/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>需要访问相机来拍照</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册来选择照片</key>

6.3 运行应用 #

方式一:使用Xcode

  1. 选择模拟器或连接的设备
  2. 点击运行按钮

方式二:使用CLI

bash
# 在iOS模拟器运行
npx cap run ios

# 在指定设备运行
npx cap run ios --target "iPhone 14 Pro"

# 列出可用设备
npx cap run ios --list

6.4 调试技巧 #

bash
# 使用Safari开发者工具
# Safari → 开发 → 模拟器 → 网页

# 或使用外部开发服务器
# capacitor.config.json
{
    "server": {
        "url": "http://192.168.1.100:5173",
        "iosScheme": "https"
    }
}

七、Android运行调试 #

7.1 打开Android Studio #

bash
npx cap open android

7.2 Android Studio配置 #

  1. 同步Gradle

    • 打开项目后自动提示同步
    • 或点击 File → Sync Project with Gradle Files
  2. 配置签名(发布时需要)

    • Build → Generate Signed Bundle/APK

7.3 运行应用 #

方式一:使用Android Studio

  1. 选择模拟器或连接的设备
  2. 点击运行按钮

方式二:使用CLI

bash
# 在Android模拟器运行
npx cap run android

# 在指定设备运行
npx cap run android --target <device-id>

# 列出可用设备
npx cap run android --list

7.4 调试技巧 #

bash
# 使用Chrome开发者工具
# chrome://inspect/#devices

# 或使用外部开发服务器
# capacitor.config.json
{
    "server": {
        "url": "http://192.168.1.100:5173",
        "androidScheme": "https",
        "cleartext": true
    }
}

八、热重载开发 #

8.1 配置开发服务器 #

json
// capacitor.config.json
{
    "appId": "com.example.todoapp",
    "appName": "Todo App",
    "webDir": "dist",
    "server": {
        "url": "http://192.168.1.100:5173",
        "iosScheme": "https",
        "androidScheme": "https",
        "cleartext": true
    }
}

8.2 开发流程 #

bash
# 终端1:启动开发服务器
npm run dev -- --host

# 终端2:同步并运行
npx cap sync
npx cap run ios
# 或
npx cap run android

8.3 注意事项 #

  • 确保设备和开发机在同一网络
  • 开发完成后记得移除 server.url 配置
  • 生产构建前需要重新构建Web应用

九、常见问题 #

9.1 iOS问题 #

问题:签名错误

text
解决:
1. 打开Xcode
2. 选择App target
3. Signing & Capabilities → 选择Team

问题:模拟器白屏

bash
# 清理并重新构建
rm -rf ios/App
npx cap add ios
npx cap sync ios

9.2 Android问题 #

问题:Gradle同步失败

bash
# 清理Gradle缓存
cd android
./gradlew clean
cd ..
npx cap sync android

问题:网络请求失败

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<application
    android:usesCleartextTraffic="true"
    ...>

十、项目优化 #

10.1 添加应用图标 #

iOS:

  • 使用Xcode的资源目录
  • 或使用 cordova-res 工具

Android:

  • 替换 android/app/src/main/res/ 下的图标文件
  • 或使用Android Studio的Image Asset工具

10.2 添加启动画面 #

bash
# 安装cordova-res
npm install -g cordova-res

# 生成启动画面
cordova-res ios --skip-config --copy
cordova-res android --skip-config --copy

10.3 配置应用信息 #

iOS (Info.plist):

xml
<key>CFBundleDisplayName</key>
<string>Todo App</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>

Android (strings.xml):

xml
<string name="app_name">Todo App</string>
<string name="title_activity_main">Todo App</string>

十一、总结 #

11.1 开发流程回顾 #

text
1. 创建项目 → npm create vite
2. 添加Capacitor → npm install @capacitor/core @capacitor/cli
3. 开发Web应用 → React/Vue/Angular
4. 构建Web应用 → npm run build
5. 添加平台 → npx cap add ios/android
6. 同步资源 → npx cap sync
7. 运行调试 → npx cap run ios/android

11.2 关键命令 #

命令 说明
npx cap init 初始化Capacitor
npx cap add 添加平台
npx cap sync 同步资源
npx cap open 打开原生IDE
npx cap run 运行应用

11.3 下一步 #

现在你已经创建了第一个Capacitor应用,接下来让我们深入了解 项目结构

最后更新:2026-03-28